Netty系列-2 NioServerSocketChannel和NioSocketChannel介绍

本文主要是介绍Netty系列-2 NioServerSocketChannel和NioSocketChannel介绍,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

背景

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

1.NioServerSocketChannel

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

// 创建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对象.

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对象,如下所示:

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对象。
继续调用父类的构造方法:

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, 并注册到选择器上,具体逻辑为:

// 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方法省去内存分配优化策略以及异常处理逻辑后的主线逻辑如下:

private final class NioMessageUnsafe extends AbstractNioUnsafe {private final List<Object> readBuf = new ArrayList<Object>();@Overridepublic 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方法中:

protected int doReadMessages(List<Object> 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<SocketChannel>() {@Overridepublic SocketChannel run() throws IOException {return serverSocketChannel.accept();}});
}

javaChannel()得到ServerSocketChannel对象,serverSocketChannel.accept()得到客户端通道对象SocketChannel。将当前服务端通道NioServerSocketChannel对象和得到的客户端通道对象SocketChannel作为参数构造NioSocketChannel对象。

2.NioSocketChannel

与NioServerSocketChannel相似,NioSocketChannel也是Netty对NIO中ServerSocketChannel的封装和增强。本章节内容将包含SocketChannel的构建、配置、向选择器注册以及读取数据,如下所示:

// 得到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构造函数创建通道对象,如下所示:

public NioSocketChannel(Channel parent, SocketChannel socket) {super(parent, socket);config = new NioSocketChannelConfig(this, socket.socket());
}

parent为NioServerSocketChannel对象,socket为NIO中SocketChannel对象。NioSocketChannel与NioServerSocketChannel相似,维持了一个config配置类用于存放和读取通道的配置信息。
继续沿着super调用父类的构造方法:

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方法:

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方法:

@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事件:

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对象。

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():

@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):

@Override
protected int doReadBytes(ByteBuf byteBuf) throws Exception {// ...return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
}

javaChannel()是NIO的SocketChannel对象,继续跟进ByteBuf的writeBytes方法进入:

@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的流水线传递,并经过网卡传递出去:

@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方法:

void invokeWriteAndFlush(Object msg, ChannelPromise promise) {// ...invokeWrite0(msg, promise);invokeFlush0();// ...
}

依次调用invokeWrite0和invokeFlush0实现写操作和刷盘操作, 分别进入Unsafe对象的write和flush方法:

public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {unsafe.write(msg, promise);
}public void flush(ChannelHandlerContext ctx) {unsafe.flush();
}

unsafe最终调用doWrite方法实现IO功能:

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.

这篇关于Netty系列-2 NioServerSocketChannel和NioSocketChannel介绍的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1127385

相关文章

Python进阶之Excel基本操作介绍

《Python进阶之Excel基本操作介绍》在现实中,很多工作都需要与数据打交道,Excel作为常用的数据处理工具,一直备受人们的青睐,本文主要为大家介绍了一些Python中Excel的基本操作,希望... 目录概述写入使用 xlwt使用 XlsxWriter读取修改概述在现实中,很多工作都需要与数据打交

java脚本使用不同版本jdk的说明介绍

《java脚本使用不同版本jdk的说明介绍》本文介绍了在Java中执行JavaScript脚本的几种方式,包括使用ScriptEngine、Nashorn和GraalVM,ScriptEngine适用... 目录Java脚本使用不同版本jdk的说明1.使用ScriptEngine执行javascript2.

Python实现NLP的完整流程介绍

《Python实现NLP的完整流程介绍》这篇文章主要为大家详细介绍了Python实现NLP的完整流程,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 编程安装和导入必要的库2. 文本数据准备3. 文本预处理3.1 小写化3.2 分词(Tokenizatio

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

性能测试介绍

性能测试是一种测试方法,旨在评估系统、应用程序或组件在现实场景中的性能表现和可靠性。它通常用于衡量系统在不同负载条件下的响应时间、吞吐量、资源利用率、稳定性和可扩展性等关键指标。 为什么要进行性能测试 通过性能测试,可以确定系统是否能够满足预期的性能要求,找出性能瓶颈和潜在的问题,并进行优化和调整。 发现性能瓶颈:性能测试可以帮助发现系统的性能瓶颈,即系统在高负载或高并发情况下可能出现的问题

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

Hadoop数据压缩使用介绍

一、压缩原则 (1)运算密集型的Job,少用压缩 (2)IO密集型的Job,多用压缩 二、压缩算法比较 三、压缩位置选择 四、压缩参数配置 1)为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器 2)要在Hadoop中启用压缩,可以配置如下参数

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

图神经网络模型介绍(1)

我们将图神经网络分为基于谱域的模型和基于空域的模型,并按照发展顺序详解每个类别中的重要模型。 1.1基于谱域的图神经网络         谱域上的图卷积在图学习迈向深度学习的发展历程中起到了关键的作用。本节主要介绍三个具有代表性的谱域图神经网络:谱图卷积网络、切比雪夫网络和图卷积网络。 (1)谱图卷积网络 卷积定理:函数卷积的傅里叶变换是函数傅里叶变换的乘积,即F{f*g}

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言