NIO的网络事件
Unsafe是内部接口,聚合在Channel中协助进行网络读写相关的操作,Channel的内部辅助类,不应该被Netty的上层使用者调用,所以被命名为Unsafe。
NIO网络事件主要有OP_ACCEPT、OP_CONNECT、OP_READ、OP_WRITE四种,服务端ServerSocketChannel只支持OP_ACCEPT操作,服务端的SocketChannel只支持OP_READ、OP_WRITE操作,客户端的SocketChannel支持OP_CONNECT、OP_WRITE、OP_READ。
OP_ACCEPT:收到一个客户端的连接请求时就绪
OP_CONNECT:只有客户端SocketChannel会注册该操作
OP_READ:当OS的读缓冲区有数据可读时,搞作就绪
OP_WRITE:当OS的写缓冲区中有空间的空间时,操作就绪
Unsafe处理I/O事件
在NioEventLoop介绍中,说到NioEventLoop线程启动后,run方法中执行了一个死循环用于发现I/O事件。发现事件后会调用NioEventLoop#processSelectedKeys()处理I/O事件:
根据selectedKeys有没有值分为processSelectedKeysOptimized()方法和processSelectedKeysPlain(selector.selectedKeys())方法,实现逻辑差不多,本例中是有值的,进入processSelectedKeysOptimized()方法如下:
因为此时的Channel是NioServerSocketChannel,是AbstractNioChannel的实现类,所以进入
processSelectedKey(k, (AbstractNioChannel) a),代码如下:
在此方法中就看到了对四种I/O时间的处理。
OP_ACCEPT事件处理
当客户端发起connect的时候,服务端会收到OP_ACCEPT事件,此时,服务端调用accept方法获得一个SocketChannel,然后将其封装成一个NioSocketChannel注册到workGroup上。
由上述代码可以看到OP_READ与OP_ACCEPT都是进的同一个if,都是调用的unsafe.read(),但是二者的unsafe并不是用一个对象,即read有不同的实现,处理OP_ACCEPT时间的unsafe为NioMessageUnsafe对象,进入其read方法代码如下:
进入处理读取数据的操作doReadMessages()方法,代码如下:
调用ServerSocketChannel.accept()获取一个SocketChannel并将其封装成一个NioSocketChannel放到readBuf中,NioSocketChannel的初始化与NioServerSocketChannel类似,但是初始化的unsafe不再是NioMessageUnsafe对象而是NioSocketChannelUnsafe(继承自NioByteUnsafe,NioByteUnsafe是channelI/O操作具体实现类)对象,初始化时会将当前的NioServerSocketChannel传入,赋值到NioSockerChannel的parent属性中,将SelectionKey.OP_READ赋值给成员变量readInterestOp等。
将存NioSocketChannel的readBuf作为参数传入pipeline.fireChannelRead()方法,将NioSocketChannel作为ChannelRead事件的传播数据进行传播。
在NioServerSocketChannel启动注册流程中说到NioServerSocketChannel在init的时候新建了一个任务将ServerBootstrapAcceptor加入到了所关联的pipeline中,此处的ChannelRead事件传播会调用ServerBootstrapAcceptor的channelRead方法,代码如下:
可以看到设置NioSocketChannel参数后,将其异步注册到childGroup,并注册一个监听器,在注册完成时回调,主要用于注册失败时关闭此NioSocketChannel。其注册流程与NioServerSocketChannel大致相同,不再赘述。注册过程中完成将NioSocketChannel注册到childGroup中的一个NioEventLoop,将SocketChannel注册到NioEventLoop的Selector上,将SelectionKey.OP_READ设为感兴趣事件等工作。此时Selector就开始监听这个SocketChannel的读事件。
OP_CONNECT事件处理
OP_CONNECT事件是客户端的事件,只执行一次,客户端在连接的时候如果没能获得连接成功的结果,就会将OP_CONNECT设为感兴趣时间,轮询等待事件触发,代码在NioSocketChannel#doConnect方法中:
SelectionKey.OP_CONNECT准备就绪后,将SelectionKey.OP_CONNECT事件从SelectionKey所感兴趣的事件中移除,然后调用unsafe.finishConnect()方法:
此时isActive()返回为false,然后在doFinishConnect()中会调用javaChannel().finishConnect()来标识连接完成,然后执行fulfillConnectPromise(connectPromise,wasActive)方法如下:
再次调用isActive(),此时因为已经调用过javaChannel().finishConnect()所以返回true,然后调用promise.trySuccess()方法看是否有用户异步取消了连接。然后不管连接是否被取消都触发pipeline的fireChannelActive()传播事件,该事件传播会调用到AbstractNioChannel#doBeginRead()方法,将SelectionKey.OP_READ标记为感兴趣事件。
此时Selector会监听NioSocketChannel上是否有可读数据。
OP_READ事件处理
当有数据可以读取时,OP_READ事件被触发,上文中描述过NioSocketChannel将SelectorKey.OP_READ标记为感兴趣事件,所以此时触发的读事件调用的unsafe.read()是NioByteUnsafe#read(),代码如下:
首先获取当前的pipeline。然后获取一个ByteBufAllocater此处默认为PooledByteBufAllocator实现,即基于内存池的堆外内存实现(实现原理参考内存池源码分析),然后获取一个AdaptiveRecvByteBufAllocator对象用于计算一个最优缓冲区大小用于创建缓冲区。
开启一个循环,先从内存池分配一个byteBuf,然后从SocketChannel中将内容写到byteBuf中去,从byteBuf的writerIndex开始写入数据,writerIndex会增加所写入的字节数。并且设置了最大可从SocketChannel读取的数据大小为allocHandle.attemptedBytesRead(),这也是byteBuf的容量大小。
然后判断读到的字节数,如果小于等于0,说明没读到数据释放byteBuf,退出循环。如果读到了数据累加读消息次数,调用pipeline.fireChannelRead()事件,在ChannelPipeline中传播,此时可以看到,触发fireChannelRead()事件是在每一次循环都会触发一次,所以需要对收到的数据举行编解码来分割数据包。然后判断是否需要继续循环,判断依据是已读字节数、已读操作数等诸多变量,不再展开描述。
循环结束后调用一次allocHandle.readComplete()来记录本次读循环的数据信息以用于预测下一次读事件触发时,应该分配多大的ByteBuf容量更加合理些。然后触发pipeline.fireChannelReadComplete()事件,并在PipelineChannel中传播,此事件在循环外触发,因此每次OP_READ只会传播一次ChannelReadComplete事件。然后判断连接是否已关闭,是则执行closeOnRead(pipeline)。
在finally中,如果本次操作读到有效数据且用户配置设置为非自动读取,会将OP_READ从感兴趣事件中移除,希望读取数据自行添加。
OP_WRITE事件处理
一般不会注册OP_WRITE事件,只有当写缓冲区已满导致数据无法全部写入时注册OP_WRITE事件,当写缓冲区出现空闲时间触发改时间,将未完成的写数据操作完成,写完后要将此事件注销。