IO事件概述
在上节我们知道Netty启动后会动起一个selector线程监听IO事件,IO事件包括以下几个:
- SelectionKey.OP_READ 读操作位触发读事件
- SelectionKey.OP_WRITE 写操作位触发写事件
- SelectionKey.OP_ACCEPT 接收操作位触发接收事件
- SelectionKey.OP_CONNECT 连接操作位触发连接事件
读事件
读事件即可以发生在客户端也可会发生在服务端,当客户端或服务端注册读事件并接受到远端发送的数据就会触发读事件。
写事件
写事件即可以发生在客户端也可会发生在服务端,写事件可以由外部直接调用触发,当出现写半包时(出现在TCP缓存满的情况),Netty会注册写操作位,待TCP缓存消耗后也会触发写事件。
接收事件
只发生在服务端,服务端启动的时候会注册接收操作位监听客户端的连接。
连接事件
只发生在客户端,客户端启动时会尝试连接服务端,连接是异步的不一定马上成功不成功则需要注册连接操作位监听客户端的连接成功。
下面从服务端的角度介绍Netty启动后,接收客户端连接的流程,以及客户端连接上后服务端的读和写的流程。
接收事件流程
当Selector轮询到接收事件会在NioEventLoop类中的processSelectedKey方法中进行处理,源码如下:
private static void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
...
int readyOps = k.readyOps();
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
if (!ch.isOpen()) {
return;
}
}
...
}
服务端的接收逻辑直接委托给unsafe.read()处理,unsafe中有2个实现类NioMessageUnsafe和NioByteUnsafe,由于服务端启动初始化的Channel用的是NioServerSocketChannel,所以unsafe的实现类是NioMessageUnsafe,下面看下unsafe.read()的实现:
public void read() {
...
try {
for (;;) {
//获取接收结果
int localRead = doReadMessages(readBuf);
//如果接收结果为空直接推出
if (localRead == 0) {
break;
}
//异常情况下返回,tcp协议未用到
if (localRead < 0) {
closed = true;
break;
}
//非自动读,退出并去掉监听客户端的连接事件,变成手工注册,一般不用
if (!config.isAutoRead()) {
break;
}
//每波的最大处理连接请求数默认为16
if (readBuf.size() >= maxMessagesPerRead) {
break;
}
}
} catch (Throwable t) {
exception = t;
}
setReadPending(false);
int size = readBuf.size();
//调用pipeline链处理客户端连接事件
for (int i = 0; i < size; i ++) {
pipeline.fireChannelRead(readBuf.get(i));
}
//清理接收对象
readBuf.clear();
//调用pipeline链处理接收完成事件
pipeline.fireChannelReadComplete();
...
}
接收客户端的处理逻辑主要流程如下:
- 调用doReadMessages方法获取客户端连接,doReadMessages的实现其实就是调用了JDK原生的accept()方法获取与客户端通讯的通道SocketChannel(系统包装成NioSocketChannel类)。
- 获取到SocketChannel后交给pipeline.fireChannelRead方法做进一步处理。
SocketChannel是服务端和客户端通讯的核心操作类,pipeline.fireChannelRead方法在之前讲过是一个调用链,调用用户的配置ChannelHandler,这里系统会调用初始化channel时系统自动注册的ServerBootstrapAcceptor里的channelRead方法(初始化channel流程可以阅读上节内容)
channelRead是Netty的核心代码主要对SocketChannel进一步封装使剥离AcceptorSelector线程,独立出跟客户端通讯IOSelector线程。我们来看下它的实现:
public void channelRead(ChannelHandlerContext ctx, Object msg) {
//获取与客户端通讯的通道SocketChannel(下面叫childChannel)
final Channel child = (Channel) msg;
//将用户配置的处理器childChannel设置到childChannel
child.pipeline().addLast(childHandler);
//将用户配置的系统参数设置到childChannel
for (Entry<ChannelOption<?>, Object> e: childOptions) {
try {
if (!child.config().setOption((ChannelOption<Object>) e.getKey(), e.getValue())) {
logger.warn("Unknown channel option: " + e);
}
} catch (Throwable t) {
logger.warn("Failed to set a channel option: " + child, t);
}
}
//将用户配置的属性设置到childChannel
for (Entry<AttributeKey<?>, Object> e: childAttrs) {
child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
}
try {
//在从线程池里注册childChannel
childGroup.register(child).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
forceClose(child, future.cause());
}
}
});
} catch (Throwable t) {
forceClose(child, t);
}
}
channelRead流程比较简单就是在从线程池注册childChannel,然后从线程池起相应的selector线程处理服务端和客户端的读事件和写事件。