我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情
在本文,我们将以实操的形式,来一步一步分析netty是怎么检测新连接和创建客户端连接的以及初始化和注册的
一、检测新连接
首先我们启动main方法,看下bossGroup和workerGroup 对应的NioEventLoop的对象哈希值。
疑问?为什么要看
NioEventLoop对应的哈希值呢?因为轮询i/o事件以及处理i/o事件都是在NioEventLoop中进行的,(说白了NioEventLoop他才是真正干活的)bossGroup和workerGroup只是NioEventLoop的(抽象)或者我觉得叫它NioEventLoop线程组也可以
上图可以看到
bossGroup里边的NioEventLoop的对象哈希值是: 1337
workerGroup里边的NioEventLoop的对象哈希值是: 1336
我们发起个连接请求,来看看服务端会做些什么事情
服务端(入口在
SingleThreadEventExecutor类的 run()方法的while(true)循环中,我们跳过select()方法(因为select就是负责 拿到 有 accept/read/write类的 channel,并不负责后续的i/o事件处理 )然后我们debug直接来到processSelectedKey方法)
从上图可以看出,由于是发起的新连接,所以被 对accept感兴趣的NioServerSocketChannel给监听到。
从上图可以看出,
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)方法
可以从上图看出,其实
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是如何被实例化的。
我们先看super(parent, socket);
设置该客户端channle对应的服务端channel
设置该channel为非阻塞的,并且将感兴趣事件设置为1 即对read类事件感兴趣。
接下来我们看下都是咋配置的,配了些啥
this.config = new NioSocketChannel.NioSocketChannelConfig(this, socket.socket());
禁用 tcp nagle算法 (关闭该算法配置后,一些小的数据包也会被快速的发送出去)
最后,我们看到 NioSocketChannel对象创建并配置完成,最终添加到了 List buf (可以把他理解成存储客户端连接实例(NioSocketChannel)的数组 ) 中去。
回调实现了inbound类型的handler的channelRead方法
其实本节和【Netty系列_3】Netty源码分析之服务端channel 有相似之处,只不过这里是创建客户端chanel(
NioSocketChannel) 而那篇文章是创建服务端channel(NioServerSocketChannel)
三、客户端channel的注册(后补充内容)
发布完文章我总感觉少点啥,后来想了想 客户端channel也是channel啊,他不得按照nio那套逻辑,注册到某个选择器上吗?要知道nio都是这么玩的,包括在前边讲解服务端channel创建时,创建完后人也得注册到selecter选择器上,在轮询时是轮询的选择器的select方法,不注册到选择器,NioEventLoop咋处理嘛,(如果不注册到选择器相当于nio这套规则没发玩了,那还了的?于是有了这段补充)
由于这里我们重启了main所以这里再次记录下bossGroup和workerGroup对应的NioEventLoop的哈希值
bossGroup NioEventLoop对应哈希值: 1329
workerGroup NioEventLoop 对应哈希值:1337
接着我们看下客户端channel是如何注册到选择器的
发起连接请求
观察客户端channel (NioSocketChannel)是如何注册的,注册到哪个selector上?
从上图可以看到,
NioSocketChannel(也就是this,他其实是在本文第二小节创建并初始化的)对应的NioEventLoop 哈希值是 1337 也就是workerGroup里边的 NioEventLoop ,而该channel是被注册到了该channel对应的NioEventLoop中的selector中去了。
另外上图这个调用链很长,而且还是异步的,我们先来看下触发这个channel注册的初始入口, 如下图:
看下客户端channel注册selector的调用链:
由于这个调用连接很长,如果不看的话很容易逻辑上出现断层,所以我们接下来跟进下,看下他在注册前做了写什么工作
在这篇文章中,我们有提到过 "ServerBootstrapAcceptor只是赋值,后续我们将知道其作用"
上图可以看出,ServerBootstrapAcceptor在初始化服务端channel时候只是单纯的把传进的参数保存起来而已,那保存起来的东西什么时候用呢,就是在本节注册客户端channel时用的(从上上图可以看出,其实this.childGroup的this就是在构建服务端channel时候,创建的ServerBootstrapAcceptor实例对象(由此我们知道 ServerBootstrapAcceptor的作用是用来创建客户端channel的 ))
我们看下next()方法做了什么
到这里我相信各位知道了,这不就是根据这个小算法
this.idx.getAndIncrement() & this.executors.length - 1 来选出一个NioEventLoop实例,那么选出这个NioEventLoop实例是要干啥呢?????我们接着看
断点来到了这里,我们可以推断出
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之前都做了些什么操作
上图可以看出,将上边选择官选出来的NioEventLoop实例,赋值到了该客户端channel对应的实例变量
eventLoop 上去。然后调用register0进行注册
我们看下register0方法干了啥
上图中的 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 则是对读事件感兴趣,因为当内核队列中有数据时,我们要读取呀。 接下来我们简单看下是咋设置的。
- 由于中间调用链比较长,我们简单截取几个重要的步骤看看。
继续
继续
继续(注意下边debug中的 this对象是客户端channel 并且他的哈希值是 1673 我们暂且记住这个哈希值,稍后会提到 )
紧挨着的上图就是为该channel设置感兴趣事件了,从方法名(beginRead)我们也能略知一二。其实这个this(也就是这个客户端channel对象 NioSocketChannel是在本文的 (第二节)中创建的,还记得吗?当时在父类的构造方法中将其实例变量赋值为1 ,没想到弯弯绕绕最终在这个beginRead方法中设置感兴趣事件时候终于使用到了!)
ok,走完beginRead方法,该channel就开始真正的可以进行数据传输了!我们随便输入点什么,看看效果,如下
-
select轮询到一个有数据的channel
-
处理这个channel(通过debug我们知道,这个channel就是是那个客户端channel 哈希值是1673! )
-
接下来就是读取该channel上的数据到byteBuffer,然后回调channelRead等方法了,我们就不展开了。
总结
-
本文内容不多,主要是debug 跟进了下新连接的检测,以及如何处理检测到的新连接,我们大致梳理下:
- 在
SingleThreadEventExecutor类的 run()方法的while(true)循环中 ,会不停的调用select方法来轮询有没有新连接,以及有没有读写类型的(连接)channel就绪(有数据)。一旦发现有accept/read/write 这类的channel就绪了,那么统统放到 该channel注册的selecter对应的NioEventLoop实例中的selectedKeys变量 中去。 - 在接下来 ,会调用
processSelectedKey方法来处理这些就绪的channel 集合,根据不同channel感兴趣事件不同,来执行不同的处理逻辑。本文中,我们演示了新连接接入的事件(accept),可以看到accept事件其实主要脉络就是通过java的 api -> accept()方法 创建了一条物理socket连接。 - 然后通过
SocketChannel抽象为实例对象,并在返回后,Netty又将其包了一层,比如(配置非阻塞,设置channel的id ,设置该客户端channel对应的父channel,以及关闭nagle算法等等)然后,将其(NIoSocketChannel实例)添加到List类型的数组(readBuf)中去,然后返回数值 1 , - 之后再
doReadMessage方法中将会跳出do while循环,遍历readBuf数组,并一一调用 实现了channelRead方法的inbound类型的handler,将事件传播下去。至此一条新客户端连接(NioSocketChannel)就创建完毕了。 - 在doReadMessage方法的do while执行后,会调用
pipeline.fireChannelRead(this.readBuf.get(i))方法,该方法经过一顿操作(从第三节最后一张调用链图,我们可以看到很深的调用而且还是异步(大致就是通过chooser选择出一个NioEventLoop然后放到客户端channel的实例变量eventLoop上去,供后边的注册channel到选择器时使用)),最终会掉到AbstractNioChannel类的doRegister方法来对客户端channel(即步骤3中创建的NioSocketChannel)进行注册选择器的操作(而该客户端channel最终是被注册到了该channeld对象里边的NioEventLoop实例里边的selector上去了)。 - 在注册channel到nioeventLoop对应的selector后,会进入
这个方法,而这个方法除了会回调 各个if (firstRegistration) { AbstractChannel.this.pipeline.fireChannelActive(); }inbound类型的handler的channelActive方法外,还会为代表该channel的selectKey(每个channel在注册到selector后,都返回一个selectKey我个人把他理解为channel的句柄) 设置感兴趣事件为(read事件),通过beginRead方法中的代码selectionKey.interestOps(interestOps | this.readInterestOp)我们得出此结论。至此,该channel可以收发数据了!
- 在