背景
前文介绍了Netty服务端的启动流程,服务端启动后可以处理客户端发送的请求,包括连接请求和普通消息。
1.处理连接
当客户端有连接请求到达时,服务器会创建通道并将通道注册到选择器上,处理逻辑与NIO中实现完全一致。
详细流程如下所示:
本章节将分小节对上图进行详细介绍。
1.1 Server阻塞监听
在Netty系列-5 Netty启动流程中介绍过,当Netty服务端启动时,将NioServerSocketChannel作为attachment:
public abstract class AbstractNioChannel extends AbstractChannel {
@Override
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
return;
} catch (CancelledKeyException e) {
//... 异常处理逻辑
}
}
}
}
将this(NioServerSocketChannel对象)作为attachment传递给register方法,当选择器selector被事件唤醒时,可以通过selectionKey.attachment获取NioServerSocketChannel对象。
注意:1.2 将用到这部分内容。
将ServerSocketChannel注册到选择器后,关联的NioEventLoop将陷入阻塞等待状态(调用选择器的选择方法阻塞监听连接请求)。
1.2 server接受连接
当连接请求到达Netty服务器时,NioEventLoop线程从select阻塞中唤醒, 并执行processSelectedKeys方法处理已就绪的事件:
private void processSelectedKeys() {
// ...
processSelectedKeysOptimized();
}
private void processSelectedKeysOptimized() {
for (int i = 0; i < selectedKeys.size; ++i) {
final SelectionKey k = selectedKeys.keys[i];
selectedKeys.keys[i] = null;
final Object a = k.attachment();
processSelectedKey(k, (AbstractNioChannel) a);
//...
}
}
依次遍历就绪的SelectionKey,从SelectionKey中取出attachment,即取出ServerSocketChannel对象(1.1中介绍过),然后调用processSelectedKey方法实际处理每个SelectedKey:
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
try {
int readyOps = k.readyOps();
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
ch.unsafe().forceFlush();
}
// 处理可读、连接事件
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
此时readyOps是Accept事件,因此表达式
(readyOps & (SelectionKey.OP_READ |SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0
为true
进入unsafe.read()方法:
private final List<Object> readBuf = new ArrayList<Object>();
public void read() {
do {
// ...
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
}
allocHandle.incMessagesRead(localRead);
} while (allocHandle.continueReading());
int size = readBuf.size();
for (int i = 0; i < size; i ++) {
readPending = false;
pipeline.fireChannelRead(readBuf.get(i));
}
readBuf.clear();
allocHandle.readComplete();
pipeline.fireChannelReadComplete();
//...省略try-catch异常处理分支
}
逻辑较为清晰:持续调用doReadMessages读取数据并添加到readBuf列表中,直到数据完全读完;然后遍历readBuf列表,将每个消息(元素)以channelRead事件触发到pipeline,消息全部处理完成后,向pipeline提交ChannelReadComplete事件。
这里有个细节需要关注一下,doReadMessages内部将NIO的通道封装为netty的通道:
protected int doReadMessages(List<Object> buf) throws Exception {
// 接收客户端连接,得到SocketChannel对象:serverSocketChannel.accept()得到SocketChannel
SocketChannel ch = SocketUtils.accept(javaChannel());
try {
if (ch != null) {
// 封装SocketChannel对象为NioSocketChannel类型
buf.add(new NioSocketChannel(this, ch));
return 1;
}
} catch (Throwable t) {
logger.warn("Failed to create a new channel from an accepted socket.", t);
try {
ch.close();
} catch (Throwable t2) {
logger.warn("Failed to close a socket.", t2);
}
}
return 0;
}
这里的buf集合存储的是NioSocketChannel类型的元素(消息)。
因此,进入Pipeline的channelRead事件的对象是NioSocketChannel对象。消息在pipeline中传递的顺序由左往右,如下所示:
当消息经过ServerBootstrapAcceptor时被处理:取出消息对象NioSocketChannel,配置NioSocketChannel对象,将NioSocketChannel对象注册到选择器上。
1.3 ServerBootstrapAcceptor作用
ServerBootstrapAcceptor是ChannelInboundHandlerAdapter的子类,属于入站事件处理器。ServerBootstrapAcceptor重写了channelRead和exceptionCaught方法,核心逻辑在channelRead中:
private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter {
// ...
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 步骤1:从消息中提取通道对象,为NioSocketChannel类型
final Channel child = (Channel) msg;
// 步骤2:向NioSocketChannel通道设置handler、配置options和attrs
child.pipeline().addLast(childHandler);
setChannelOptions(child, childOptions, logger);
setAttributes(child, childAttrs);
try {
// 步骤3: 注册到childGroup(NioEventLoopGroup)上
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);
}
}
}
服务器接收连接请求得到NIO的SocketChannel,netty框架封装为NioSocketChannel对象,传递给Pipeline. 当消息沿着Pipeline传递到ServerBootstrapAcceptor的channelRead方法时,将进行以下处理:
[1] 从消息中提取通道对象(即NioSocketChannel对象),消息本身就是通道对象;
[2] 向NioSocketChannel通道设置handler、配置options和attrs,这里的配置来源于ServerBootstrap启动时配置的childHandler, childOptions和childAttrs.
[3] 将NioSocketChannel通道注册到childGroup(NioEventLoopGroup)上,对应传递给ServerBootstrap的workerGroup线程池.
其中:步骤[3]中向NioEventLoopGroup注册NioSocketChannel,即从NioEventLoopGroup选择出一个NioEventLoop,使用NioEventLoop注册NioSocketChannel,这一部分已在Netty系列-2 NioServerSocketChannel和NioSocketChannel介绍中介绍过,不再赘述。有个细节需要注意:向选择器注册NioServerSocketChannel时,attachment是NioServerSocketChannel对象;而向选择器注册NioSocketChannel时,attachment是NioSocketChannel对象。
2.处理消息
当通道有消息可读时,NioEventLoop线程从阻塞中唤醒并处理SelectionKey, 流程与NIO相似。详细流程如下所示:
2.1 可读事件
有可读事件到达时,workerGroup中的某个NioEventLoop将(从select阻塞中)被唤醒,调用processSelectedKeys方法处理就绪的事件。
private void processSelectedKeys() {
//...
processSelectedKeysOptimized();
}
private void processSelectedKeysOptimized() {
for (int i = 0; i < selectedKeys.size; ++i) {
final SelectionKey k = selectedKeys.keys[i];
selectedKeys.keys[i] = null;
final Object a = k.attachment();
//...
processSelectedKey(k, (AbstractNioChannel) a);
//...
}
}
遍历已就绪的事件,调用processSelectedKey处理。
注意:这里从selectedKey取出的attachment对象是NioSocketChannel通道。
继续跟进processSelectedKey方法:
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
try {
int readyOps = k.readyOps();
//... 省略其他分支:SelectionKey.OP_CONNECT和readyOps & SelectionKey.OP_WRITE
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
}
此时,readyOps为SelectionKey.OP_READ,调用unsafe.read()
处理消息。
public final void read() {
final ChannelConfig config = config();
if (shouldBreakReadReady(config)) {
clearReadPending();
return;
}
final ChannelPipeline pipeline = pipeline();
final ByteBufAllocator allocator = config.getAllocator();
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
allocHandle.reset(config);
ByteBuf byteBuf = null;
boolean close = false;
//...
do {
byteBuf = allocHandle.allocate(allocator);
// 读取数据,存入ByteBuf对象
allocHandle.lastBytesRead(doReadBytes(byteBuf));
if (allocHandle.lastBytesRead() <= 0) {
byteBuf.release();
byteBuf = null;
close = allocHandle.lastBytesRead() < 0;
if (close) {
readPending = false;
}
break;
}
allocHandle.incMessagesRead(1);
readPending = false;
// 读取后,向Pipeline触发ChannelRead事件
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
} while (allocHandle.continueReading());
//...
// 消息处理完,向pipeline触发ChannelReadComplete事件
pipeline.fireChannelReadComplete();
}
逻辑较为清晰:持续调用doReadBytes读取数据至ByteBuf对象中,并将每个读取的ByteBuf以channelRead事件提交到pipeline,全部消息处理完成后,向pipeline提交ChannelReadComplete事件。
这里有个细节需要关注一下,doReadBytes内部使用NIO中的通道将数据流写入到ByteBuf中:
public final int setBytes(int index, ScatteringByteChannel in, int length) throws IOException {
try {
return in.read(internalNioBuffer(index, length));
} catch (ClosedChannelException ignored) {
return -1;
}
}
2.2 pipeline处理数据
当数据沿着NioSocketChannel通道的Pipeline传输时,从左到右顺序如下:
Bytebuf类型的消息将沿着解码器Handler->业务Handler->编码器Handler->…->TailContext的顺序处理。
其中HeadContext和TailContext由框架携带,其他Handler由用户根据业务需要开发和引入。
因此,业务Handler中未定义加码器时,第一个处理可读消息的Handler的消息为Bytebuf类型。另外,消息外发时,也需要将业务对象转为Bytebuf类型,才可以正常发出(编码器)。
2.4 扩展
编解码器内容将在Netty系列-7 Netty编解码器中详细介绍。