【Netty系列_6】新连接处理与客户端channel实例创建&初始化&注册到selector&设置感兴趣事件

2,881 阅读10分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情

在本文,我们将以实操的形式,来一步一步分析netty是怎么检测新连接和创建客户端连接的以及初始化和注册的

一、检测新连接

首先我们启动main方法,看下bossGroupworkerGroup 对应的NioEventLoop的对象哈希值。

疑问?为什么要看NioEventLoop对应的哈希值呢?因为轮询i/o事件以及处理i/o事件都是在NioEventLoop中进行的,(说白了 NioEventLoop他才是真正干活的)bossGroupworkerGroup只是NioEventLoop的(抽象)或者我觉得叫它 NioEventLoop 线程组也可以

image.png 上图可以看到

bossGroup里边的NioEventLoop的对象哈希值是: 1337

workerGroup里边的NioEventLoop的对象哈希值是: 1336

我们发起个连接请求,来看看服务端会做些什么事情 image.png 服务端(入口在SingleThreadEventExecutor类的 run()方法的while(true)循环中,我们跳过select()方法(因为select就是负责 拿到accept/read/write类的 channel,并不负责后续的i/o事件处理 )然后我们debug直接来到processSelectedKey方法) image.png

从上图可以看出,由于是发起的新连接,所以被 对accept感兴趣的NioServerSocketChannel给监听到。

image.png 从上图可以看出,SelectionKey里边的attachment变量其实保存的就是 ch这个对象(这个attachment后续我们可能会用到,所以我们这里提一嘴),可以看到由于是accept事件 值对应的是16 所以再往下走会进入unsafe.read()这个方法中

public void read() {
    assert AbstractNioMessageChannel.this.eventLoop().inEventLoop();

    ChannelConfig config = AbstractNioMessageChannel.this.config();
    ChannelPipeline pipeline = AbstractNioMessageChannel.this.pipeline();
    Handle allocHandle = AbstractNioMessageChannel.this.unsafe().recvBufAllocHandle();
    allocHandle.reset(config);
    boolean closed = false;
    Throwable exception = null;

    try {
        int localRead;
        try {
            do {
            //进行客户端连接(NioSocketChannel)的创建 
                localRead = AbstractNioMessageChannel.this.doReadMessages(this.readBuf);
                if (localRead == 0) {
                    break;
                }
                if (localRead < 0) {
                    closed = true;
                    break;
                }
                //计数 用于下边的判断
                allocHandle.incMessagesRead(localRead);
            } while(allocHandle.continueReading());//判断当前读的新连接数量是否超过上限了(默认一次轮询16个)
        } catch (Throwable var11) {
            exception = var11;
        }

        localRead = this.readBuf.size();

        for(int i = 0; i < localRead; ++i) {
            AbstractNioMessageChannel.this.readPending = false;
            pipeline.fireChannelRead(this.readBuf.get(i));
        }

        this.readBuf.clear();
        allocHandle.readComplete();
        //将会回调各个inbound类型的handler的 channelRead方法
        pipeline.fireChannelReadComplete();
        if (exception != null) {
            closed = AbstractNioMessageChannel.this.closeOnReadError(exception);
            pipeline.fireExceptionCaught(exception);
        }

        if (closed) {
            AbstractNioMessageChannel.this.inputShutdown = true;
            if (AbstractNioMessageChannel.this.isOpen()) {
                this.close(this.voidPromise());
            }
        }
    } finally {
        if (!AbstractNioMessageChannel.this.readPending && !config.isAutoRead()) {
            this.removeReadOp();
        }

    }

}

我们来到doReadMessages(List<Object> buf)方法

image.png 可以从上图看出,其实ch就是一条客户端socket连接,而this是一条服务端socket连接

补充:看下doReadMessage里边的 this.javaChannel().accept()干了啥

public SocketChannel accept() throws IOException {
    acceptLock.lock();
    try {
        int n = 0;
        //创建文件描述符和接收到的客户端socket进行绑定
        FileDescriptor newfd = new FileDescriptor();
        InetSocketAddress[] isaa = new InetSocketAddress[1];

        boolean blocking = isBlocking();
        try {
            begin(blocking);
            do {
            //接收新连接,将给定的文件描述符与socket客户端(isaa)进行绑定,并设置套接字客户端的地址
                n = accept(this.fd, newfd, isaa);
            } while (n == IOStatus.INTERRUPTED && isOpen());
        } finally {
            end(blocking, n > 0);
            assert IOStatus.check(n);
        }

        if (n < 1)
            return null;

        // newly accepted socket is initially in blocking mode
        IOUtil.configureBlocking(newfd, true);

        InetSocketAddress isa = isaa[0];
        //初始化客户端socketchannel
        SocketChannel sc = new SocketChannelImpl(provider(), newfd, isa);

        // check permitted to accept connections from the remote address
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            try {
                sm.checkAccept(isa.getAddress().getHostAddress(), isa.getPort());
            } catch (SecurityException x) {
                sc.close();
                throw x;
            }
        }
        return sc;

    } finally {
        acceptLock.unlock();
    }
}

从上边可以看出其实就是创建了一个socket连接,同时跟据(accept(this.fd, newfd, isaa)的)入参也能观察出,其实每个socket连接就是对应的某个文件描述符!

ok客户端连接创建完毕,接下来就是实例化他了,在是实例化过程中,我们会看到各种对这个连接的初始化配置。

其实我个人理解 doReadMessage中的 ch(SocketChannel java中的类 )是java中的与物理socket一一对应的实例对象,而NioSocketChannel其实是netty把ch又包了一层,可以方便netty对其操作,和拓展。

二、为检测到的新连接创建实例对象(NioSocketChannel)

接下来我们进入到 new NioSocketChannel(this, ch)这段逻辑中,看看客户端channel NioSocketChannel是如何被实例化的。

image.png

我们先看super(parent, socket);

设置该客户端channle对应的服务端channel image.png

设置该channel为非阻塞的,并且将感兴趣事件设置为1 即对read类事件感兴趣。 image.png

接下来我们看下都是咋配置的,配了些啥

this.config = new NioSocketChannel.NioSocketChannelConfig(this, socket.socket());

禁用 tcp nagle算法 (关闭该算法配置后,一些小的数据包也会被快速的发送出去) image.png

最后,我们看到 NioSocketChannel对象创建并配置完成,最终添加到了 List buf (可以把他理解成存储客户端连接实例(NioSocketChannel)的数组 ) 中去。

image.png

回调实现了inbound类型的handlerchannelRead方法

image.png

其实本节和【Netty系列_3】Netty源码分析之服务端channel 有相似之处,只不过这里是创建客户端chanel(NioSocketChannel) 而那篇文章是创建服务端channel(NioServerSocketChannel)

三、客户端channel的注册(后补充内容)

发布完文章我总感觉少点啥,后来想了想 客户端channel也是channel啊,他不得按照nio那套逻辑,注册到某个选择器上吗?要知道nio都是这么玩的,包括在前边讲解服务端channel创建时,创建完后人也得注册到selecter选择器上,在轮询时是轮询的选择器的select方法,不注册到选择器,NioEventLoop咋处理嘛,(如果不注册到选择器相当于nio这套规则没发玩了,那还了的?于是有了这段补充)

image.png

由于这里我们重启了main所以这里再次记录下bossGroup和workerGroup对应的NioEventLoop的哈希值

bossGroup NioEventLoop对应哈希值: 1329
workerGroup NioEventLoop 对应哈希值:1337

接着我们看下客户端channel是如何注册到选择器的

发起连接请求 image.png

观察客户端channel (NioSocketChannel)是如何注册的,注册到哪个selector上? image.png 从上图可以看到,NioSocketChannel(也就是this,他其实是在本文第二小节创建并初始化的)对应的NioEventLoop 哈希值是 1337 也就是workerGroup里边的 NioEventLoop ,而该channel是被注册到了该channel对应的NioEventLoop中的selector中去了。

另外上图这个调用链很长,而且还是异步的,我们先来看下触发这个channel注册的初始入口, 如下图:

image.png

看下客户端channel注册selector的调用链:

image.png

由于这个调用连接很长,如果不看的话很容易逻辑上出现断层,所以我们接下来跟进下,看下他在注册前做了写什么工作

image.png

这篇文章中,我们有提到过 "ServerBootstrapAcceptor只是赋值,后续我们将知道其作用"

image.png

上图可以看出,ServerBootstrapAcceptor在初始化服务端channel时候只是单纯的把传进的参数保存起来而已,那保存起来的东西什么时候用呢,就是在本节注册客户端channel时用的(从上上图可以看出,其实this.childGroup的this就是在构建服务端channel时候,创建的ServerBootstrapAcceptor实例对象(由此我们知道 ServerBootstrapAcceptor的作用是用来创建客户端channel的 ))

image.png

我们看下next()方法做了什么

image.png 到这里我相信各位知道了,这不就是根据这个小算法this.idx.getAndIncrement() & this.executors.length - 1 来选出一个NioEventLoop实例,那么选出这个NioEventLoop实例是要干啥呢?????我们接着看

image.png

断点来到了这里,我们可以推断出

this.childGroup.register(child).addListener(new ChannelFutureListener() {
    public void operationComplete(ChannelFuture future) throws Exception {
        if (!future.isSuccess()) {
            ServerBootstrap.ServerBootstrapAcceptor.forceClose(child, future.cause());
        }

    }
});

这里的this.childGroup其实对应的实例是MultithreadEventLoopGroup,然后在之后的注册方法register中 ,调用supper.next()方法(最终掉了 this.chooser.next();方法 通过选择官(这里为了和selector区分,我们叫它选择官) 来选择一个可用的NioEventLoop实例)并返回,而上图这个this,其实就是选择官chooser选择后的某一个具体的NioEventLoop,然后调用该实例的register方法,对入参(即客户端channel)进行注册(注册到选择出的这个NioEventLoop实例对应的选择器selector上去)

接着我们看下,注册客户端channel之前都做了些什么操作 image.png 上图可以看出,将上边选择官选出来的NioEventLoop实例,赋值到了该客户端channel对应的实例变量 eventLoop 上去。然后调用register0进行注册

image.png

我们看下register0方法干了啥 image.png

上图中的 AbstractChannel.this.doRegister();方法(其实就是将该客户端channel绑定到该channel对应的NioEventLoop上的selector中去)

protected void doRegister() throws Exception {
    boolean selected = false;

    while(true) {
        try {
            this.selectionKey = this.javaChannel().register(this.eventLoop().selector, 0, this);
            return;
        } catch (CancelledKeyException var3) {
            if (selected) {
                throw var3;
            }

            this.eventLoop().selectNow();
            selected = true;
        }
    }
}

客户端channel绑定选择器selector完成后就是触发以下两种回调了:

channelRegistered
channelActive

ok到这里,客户端channel的注册逻辑就讲完了。(其实他的注册和服务端channel的注册是一个方法,只是调用时机不同,总之是channel(不管服务端channel还是客户端channel)就得注册到某个选择器(selector)上去,而选择器一般都是在NioEventLoop中放着,而一个channel对象中,又都有一个NioEventLoop实例对象(通过chooser算出来的,并在之后的register0方法中赋值给客户端channel的eventLoop实例对象),接下来channel在真正注册selector时,就会拿出channel里边的NioEventLoop实例对象里边的selector实例,来进行注册。 ) 就像下边这段代码一样:

this.selectionKey = this.javaChannel().register(this.eventLoop().selector, 0, this);

四、为客户端channel设置感兴趣事件(read事件)

我们都知道,一般服务端channel也就是NioServerSocketChannel 一般都是对accept事件感兴趣,而客户端channel 即 NioSocketChannel 则是对读事件感兴趣,因为当内核队列中有数据时,我们要读取呀。 接下来我们简单看下是咋设置的。

  • 由于中间调用链比较长,我们简单截取几个重要的步骤看看。 image.png 继续
    image.png 继续 image.png 继续(注意下边debug中的 this对象是客户端channel 并且他的哈希值是 1673 我们暂且记住这个哈希值,稍后会提到 ) image.png

紧挨着的上图就是为该channel设置感兴趣事件了,从方法名(beginRead)我们也能略知一二。其实这个this(也就是这个客户端channel对象 NioSocketChannel是在本文的 (第二节)中创建的,还记得吗?当时在父类的构造方法中将其实例变量赋值为1 ,没想到弯弯绕绕最终在这个beginRead方法中设置感兴趣事件时候终于使用到了!)

ok,走完beginRead方法,该channel就开始真正的可以进行数据传输了!我们随便输入点什么,看看效果,如下

image.png

  • select轮询到一个有数据的channel image.png

  • 处理这个channel(通过debug我们知道,这个channel就是是那个客户端channel 哈希值是1673! ) image.png

  • 接下来就是读取该channel上的数据到byteBuffer,然后回调channelRead等方法了,我们就不展开了。

总结

  • 本文内容不多,主要是debug 跟进了下新连接的检测,以及如何处理检测到的新连接,我们大致梳理下:

    1. SingleThreadEventExecutor类的 run()方法的while(true)循环中 ,会不停的调用select方法来轮询有没有新连接,以及有没有读写类型的(连接)channel就绪(有数据)。一旦发现有accept/read/write 这类的channel就绪了,那么统统放到 该channel注册的selecter对应的NioEventLoop实例中的 selectedKeys 变量 中去。
    2. 在接下来 ,会调用 processSelectedKey方法来处理这些就绪的channel 集合,根据不同channel感兴趣事件不同,来执行不同的处理逻辑。本文中,我们演示了新连接接入的事件(accept),可以看到accept事件其实主要脉络就是通过java的 api -> accept()方法 创建了一条物理socket连接。
    3. 然后通过 SocketChannel抽象为实例对象,并在返回后,Netty又将其包了一层,比如(配置非阻塞,设置channel的id ,设置该客户端channel对应的父channel,以及关闭nagle算法等等)然后,将其(NIoSocketChannel实例)添加到List类型的数组(readBuf)中去,然后返回数值 1 ,
    4. 之后再doReadMessage方法中将会跳出do while循环,遍历readBuf数组,并一一调用 实现了channelRead方法的inbound类型的handler,将事件传播下去。至此一条新客户端连接(NioSocketChannel)就创建完毕了。
    5. 在doReadMessage方法的do while执行后,会调用pipeline.fireChannelRead(this.readBuf.get(i))方法,该方法经过一顿操作(从第三节最后一张调用链图,我们可以看到很深的调用而且还是异步(大致就是通过chooser选择出一个NioEventLoop然后放到客户端channel的实例变量eventLoop上去,供后边的注册channel到选择器时使用)),最终会掉到AbstractNioChannel类的doRegister方法来对客户端channel(即步骤3中创建的NioSocketChannel)进行注册选择器的操作(而该客户端channel最终是被注册到了该channeld对象里边的NioEventLoop实例里边的selector上去了)。
    6. 在注册channel到nioeventLoop对应的selector后,会进入
      if (firstRegistration) {
          AbstractChannel.this.pipeline.fireChannelActive();
      }
      
      这个方法,而这个方法除了会回调 各个inbound类型的handlerchannelActive方法外,还会为代表该channelselectKey (每个channel在注册到selector后,都返回一个selectKey我个人把他理解为channel的句柄 设置感兴趣事件为(read事件),通过beginRead方法中的代码selectionKey.interestOps(interestOps | this.readInterestOp)我们得出此结论。至此,该channel可以收发数据了!