Netty系列-2 NioServerSocketChannel和NioSocketChannel介绍

背景

本文介绍Netty的通道组件NioServerSocketChannel和NioSocketChannel,从源码的角度介绍其实现原理。

1.NioServerSocketChannel

Netty本质是对NIO的封装和增强,因此Netty框架中必然包含了对于ServerSocketChannel的构建、配置以及向选择器注册,如下所示:

java 复制代码
// 创建ServerSocketChannel对象
ServerSocketChannel serverSocketChannel = SelectorProvider.provider().openServerSocketChannel();

// ServerSocketChannel通道设置为非阻塞
serverSocketChannel.configureBlocking(false);

// 将ServerSocketChannel通道注册至选择器
serverSocketChannel.register(Selector, opts, attachment);

// 接收客户端连接得到SocketChannel通道
SocketChannel socketChannel = serverSocketChannel.accept();

其中的构建和配置过程发生在NioServerSocketChannel的实例化过程。

1.1 NioServerSocketChannel构造函数

NioServerSocketChannel实例化过程包含了对serverSocketChannel的创建以及配置

Netty启动时,通过反射调用NioServerSocketChannel的无参构造函数创建NioServerSocketChannel对象.

java 复制代码
private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider();

public NioServerSocketChannel() {
    this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}

public NioServerSocketChannel(ServerSocketChannel channel) {
    super(null, channel, SelectionKey.OP_ACCEPT);
    config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}

DEFAULT_SELECTOR_PROVIDER是Provider对象,用于创建通道和选择器,newSocket方法返回一个ServerSocketChannel对象,如下所示:

java 复制代码
private static ServerSocketChannel newSocket(SelectorProvider provider) {
    try {
        return provider.openServerSocketChannel();
    } catch (IOException e) {
        throw new ChannelException("Failed to open a server socket.", e);
    }
}

NioServerSocketChannel中还维护了一个config对象用于储存该通道相关的配置,后续通过通道对象的config()方法获取该config对象。

继续调用父类的构造方法:

java 复制代码
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
    super(parent);
    this.ch = ch;
    this.readInterestOp = readInterestOp;
    try {
        ch.configureBlocking(false);
    } catch (IOException e) {
        try {
            ch.close();
        } catch (IOException e2) {
            logger.warn("Failed to close a partially initialized socket.", e2);
        }

        throw new ChannelException("Failed to enter non-blocking mode.", e);
    }
}

// super(parent)内容如下:
protected AbstractChannel(Channel parent) {
    this.parent = parent;
    id = newId();
    unsafe = newUnsafe();
    pipeline = newChannelPipeline();
}

因此NioServerSocketChannel中包含如下属性:

1\] SelectableChannel ch:实际为ServerSocketChannel类型,即NIO中的服务端通道类型,并将其配置为非阻塞类型,以便后续向选择器注册; \[2\] int readInterestOp: 值固定为SelectionKey.OP_ACCEPT,表示仅处理连接事件; \[3\] pipeline: Netty的Pipeline组件,每个channel都有一个属于自己的Pipeline对象; \[4\] unsafe: 对底层IO进行了封装,实际的读写操作在该类中进行处理; \[5\] 其他: id唯一ID标识,parent固定为空。 ### 1.2 NioServerSocketChannel注册 > NioServerSocketChannel包含了ServerSocketChannel对象,向选择器注册NioServerSocketChannel本质是将ServerSocketChannel注册到选择器 在Netty启动流程流程中,依次构造ServerSocketChannel, 并注册到选择器上,具体逻辑为: ```java // NioServerSocketChannel的父类AbstractNioChannel中 // 删除try-catch异常逻辑 protected void doRegister() throws Exception { boolean selected = false; for (;;) { selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this); return; } } ``` 其中: javaChannel()获取NioServerSocketChannel对象的ServerSocketChannel属性;eventLoop().unwrappedSelector()为NioEventLoop这个线程绑定的选择器;此处的this表明将ServerSocketChannel注册到选择器上时,将当前的NioServerSocketChannel对象作为attachment保存到SelectionKey中,并使用`volatile SelectionKey selectionKey;`属性保存了注册结果。 说明:后续选择器会执行select而阻塞,当该选择器被IO事件唤醒时,可通过SelectionKey的attachment获取NioServerSocketChannel对象,从而可以获取包括ServerSocketChannel、Pipeline、Config等其他所有相关信息。 ### 1.3 NioServerSocketChannel处理连接 章节1.1中提到了NioServerSocketChannel的unsafe属性,unsafe用于封装底层具体的IO行为,具体的实现类为NioMessageUnsafe. 当有连接请求到达NioServerSocketChannel后,进入NioMessageUnsafe的read()方法中(详细的调用流程和线程处理关系在后续Netty的消息处理流程中介绍, 这里仅对read方法实现逻辑进行说明),read方法省去内存分配优化策略以及异常处理逻辑后的主线逻辑如下: ```java private final class NioMessageUnsafe extends AbstractNioUnsafe { private final List readBuf = new ArrayList(); @Override public void read() { // ... final ChannelPipeline pipeline = pipeline(); do { // ... doReadMessages(readBuf); } while (allocHandle.continueReading()); int size = readBuf.size(); for (int i = 0; i < size; i ++) { readPending = false; pipeline.fireChannelRead(readBuf.get(i)); } readBuf.clear(); pipeline.fireChannelReadComplete(); } } ``` readBuf是一个列表类型,用于存放解析后的消息对象,解析完成后,依次遍历readBuf,并调用pipeline.fireChannelRead将消息对象发送至Netty的Pipeline组件(后面单独介绍)。 解析逻辑在doReadMessages方法中: ```java protected int doReadMessages(List buf) throws Exception { SocketChannel ch = SocketUtils.accept(javaChannel()); try { if (ch != null) { 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; } // SocketUtils.accept(javaChannel())代码逻辑: public static SocketChannel accept(final ServerSocketChannel serverSocketChannel) throws IOException { // 删除try-catch异常逻辑 return AccessController.doPrivileged(new PrivilegedExceptionAction() { @Override public SocketChannel run() throws IOException { return serverSocketChannel.accept(); } }); } ``` javaChannel()得到ServerSocketChannel对象,serverSocketChannel.accept()得到客户端通道对象SocketChannel。将当前服务端通道NioServerSocketChannel对象和得到的客户端通道对象SocketChannel作为参数构造NioSocketChannel对象。 ## 2.NioSocketChannel 与NioServerSocketChannel相似,NioSocketChannel也是Netty对NIO中ServerSocketChannel的封装和增强。本章节内容将包含SocketChannel的构建、配置、向选择器注册以及读取数据,如下所示: ```java // 得到SocketChannel对象 SocketChannel socketChannel = serverSocketChannel.accept(); // SocketChannel通道设置为非阻塞 socketChannel.configureBlocking(false); // 将SocketChannel通道注册至选择器 socketChannel.register(Selector, opts, attachment); // 从SocketChannel通道读取数据值缓冲区 socketChannel.read(ByteBuffer) ``` ### 2.1 NioSocketChannel构造函数 > 每个客户端连接对应一个通道,即一个NioSocketChannel对象。 Netty收到客户端连接时,会调用NioSocketChannel构造函数创建通道对象,如下所示: ```java public NioSocketChannel(Channel parent, SocketChannel socket) { super(parent, socket); config = new NioSocketChannelConfig(this, socket.socket()); } ``` parent为NioServerSocketChannel对象,socket为NIO中SocketChannel对象。NioSocketChannel与NioServerSocketChannel相似,维持了一个config配置类用于存放和读取通道的配置信息。 继续沿着super调用父类的构造方法: ```java protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) { super(parent, ch, SelectionKey.OP_READ); } protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) { super(parent); this.ch = ch; this.readInterestOp = readInterestOp; try { ch.configureBlocking(false); } catch (IOException e) { try { ch.close(); } catch (IOException e2) { logger.warn("Failed to close a partially initialized socket.", e2); } throw new ChannelException("Failed to enter non-blocking mode.", e); } } protected AbstractChannel(Channel parent) { this.parent = parent; id = newId(); unsafe = newUnsafe(); pipeline = newChannelPipeline(); } ``` 上述构造过程逻辑较为简单,为NioSocketChannel创建一个Unsafe对象和Pipeline对象;以及将ch属性即SocketChannel设置为非阻塞。 ### 2.2 注册选择器 NioServerSocketChannel接收客户端连接构造出NioSocketChannel对象,并通过Pipeline.fireChannelRead触发Inbound读事件后,通过Pipiline进入ServerBootstrapAcceptor处理器的channelRead方法: ```java public void channelRead(ChannelHandlerContext ctx, Object msg) { final Channel child = (Channel) msg; // ... childGroup.register(child).addListener(new ChannelFutureListener() {//...}); } ``` 由章节1可知msg消息为NioSocketChannel,childGroup为线程池NioEventLoopGroup对象(workgroup)。 `childGroup.register(child)`表示将NioSocketChannel注册到workgroup的一个线程中,经过Unsafe对象最终会进入NioSocketChannel的doRegister方法: ```java @Override protected void doRegister() throws Exception { // ... selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this); // ... } ``` javaChannel()为NioSocketChannel的ch属性,即SocketChannel通道对象;eventLoop().unwrappedSelector()为选择器;this为NioSocketChannel对象本身;返回的SelectionKey也作为属性保存在NioSocketChannel类中。 说明:后续选择器会执行select而阻塞,当有可读消息到达时被唤醒。可通过SelectionKey得到NioSocketChannel对象,从而得到相关的SocketChannel、Pipeline、Config等其他所有相关信息。 ### 2.3 读取消息 当有可读时间到达时,NioEvetLoop会从阻塞中被唤醒,从而执行processSelectedKeys处理IO事件: ```java 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); } } ``` 遍历已就绪的IO事件,调用processSelectedKey方法处理,此时k为NIO的SelectionKey对象,而attachment为NioSocketChannel对象。 ```java private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) { final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe(); int readyOps = k.readyOps(); //... if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) { unsafe.read(); } // ... } ``` 根据SelectionKey和NioSocketChannel对象的readyOps确定此时IO事件为可读消息,进入unsafe.read(): ```java @Override public final void read() { final ChannelConfig config = config(); final ChannelPipeline pipeline = pipeline(); final ByteBufAllocator allocator = config.getAllocator(); ByteBuf byteBuf = null; boolean close = false; // ... do { // ... // 1.分配ButeBuf缓冲对象 byteBuf = allocHandle.allocate(allocator); // 2.将数据读取到ButeBuf缓冲对象 allocHandle.lastBytesRead(doReadBytes(byteBuf)); if (allocHandle.lastBytesRead() <= 0) { byteBuf.release(); byteBuf = null; break; } readPending = false; // 3.向Pipeline传递可读消息 pipeline.fireChannelRead(byteBuf); byteBuf = null; // 直到读取完所有消息内容 } while (allocHandle.continueReading()); // ... // 触发消息读取完成事件 pipeline.fireChannelReadComplete(); // ... } ``` 代码较为清晰,重点包含3个步骤:创建ByteBuf缓冲对象(Netty自定义的,而非NIO的ByteBuffer); 将消息读取到ButeBuf对象,向Pipeline触发可读事件(在Pipeline的Handler中传递并处理消息);其中,核心逻辑在于doReadBytes(byteBuf): ```java @Override protected int doReadBytes(ByteBuf byteBuf) throws Exception { // ... return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead()); } ``` javaChannel()是NIO的SocketChannel对象,继续跟进ByteBuf的writeBytes方法进入: ```java @Override public int writeBytes(ScatteringByteChannel in, int length) throws IOException { //... int writtenBytes = setBytes(writerIndex, in, length); //... return writtenBytes; } @Override public final int setBytes(int index, ScatteringByteChannel in, int length) throws IOException { try { return in.read(internalNioBuffer(index, length)); } catch (ClosedChannelException ignored) { return -1; } } ``` 可以看到底层逻辑在于`in.read(internalNioBuffer(index, length))`, 返回一个ByteBuffer对象,in此时为SocketChannel, 即本质是调用NIO通道的API将数据读取至缓冲区: SocketChannel.read(ByteBuffer). ### 2.3 响应消息 Netty中Pipeline的任何一个Handler中都可以发送响应消息,响应消息也会沿着Pipeline的流水线传递,并经过网卡传递出去: ```java @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ctx.writeAndFlush("hello"); } ``` 注意:需要在此Handler前添加StringEncoder编码器,将String类型转为ByteBuf类型,否则会抛出异常。因为NioSocketChannel的Unsafe对象也维持在了Pipeline的HeadContext对象中,所有的消息最终会经过Unsafe的write方法,而Unsafe只会处理ByteBuf类型消息,其他类型会抛出异常。 追踪`ctx.writeAndFlush("hello")`进入`invokeWriteAndFlush`方法: ```java void invokeWriteAndFlush(Object msg, ChannelPromise promise) { // ... invokeWrite0(msg, promise); invokeFlush0(); // ... } ``` 依次调用invokeWrite0和invokeFlush0实现写操作和刷盘操作, 分别进入Unsafe对象的write和flush方法: ```java public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { unsafe.write(msg, promise); } public void flush(ChannelHandlerContext ctx) { unsafe.flush(); } ``` unsafe最终调用doWrite方法实现IO功能: ```java protected void doWrite(ChannelOutboundBuffer in) throws Exception { SocketChannel ch = javaChannel(); int writeSpinCount = config().getWriteSpinCount(); do { // ... ByteBuffer buffer = nioBuffers[0]; int attemptedBytes = buffer.remaining(); final int localWrittenBytes = ch.write(buffer); --writeSpinCount; // ... } while (writeSpinCount > 0); incompleteWrite(writeSpinCount < 0); } ``` 核心逻辑在与ch.write(buffer),其中ch和buffer分别是NIO的SocketChannel和ByteBuffer, 即Netty向客户端发送消息底层仍是借助NIO的API.

相关推荐
ZenosDoron1 分钟前
Linux 中,rm -r 和 -f
linux·运维·服务器
WarPigs10 分钟前
Windows IIS开启和配置服务器
运维·服务器
半斤八两21116 分钟前
个人服务器发送消息至飞书
服务器
pengyi87101541 分钟前
共享 IP 池多人使用 分层权限与配额管理方案
运维·服务器·网络
楼兰公子1 小时前
读取rpi摄像头
linux·服务器·算法
李景琰1 小时前
Debian12安装配置Mqtt之EMQX
linux·运维·服务器
接着奏乐接着舞2 小时前
3D Tiles tileset.jso 数据格式
运维·服务器·3d
李小白202002022 小时前
RK3568 linux6.1 死机
linux·运维·服务器
FreeGo~2 小时前
Linux 系统编程 进程篇 (五)
java·linux·服务器
nbwenren2 小时前
办公AI实测:Gemini3、GPT-4o、Claude3.5谁更强?
服务器·数据库·php