Netty 组件介绍
BootStrap
Netty 中的 BootStrap 分为两种:一种是客户端的 BootStrap;一种是服务端的 ServerBootStrap。
客户端的
BootStrap初始化客户端,该
BootStrap只有一个EventLoopGroup,用于连接远程主机服务端的
ServerBootStrap用于绑定本地端口,一般存在两个
EventLoopGroup(一个用于包含单例的ServerChannel,用于接收和分发请求;另一个是包含了所有创建的 Channel,处理服务器接受到的所有来自客户端的连接)。
启动流程
BootStrap 的启动流程(以 ServerBootStrap 为例):

具体的代码如下所示:
NioEventLoopGroup group = new NioEventLoopGroup(); // 创建一个 NioEventLoopGroupServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(group).channel(NioServerSocketChannel.class) // 设置 Channel 的类型为 NioServerSocketChannel.localAddress(new InetSocketAddress(9098)) // 监听本地的 9098 端口.option(ChannelOption.TCP_NODELAY, true) // 设置启用 Nagle 算法.option(ChannelOption.SO_BACKLOG, 1024) // 设置最大等待连接队列长度.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {// 对当前的 Channel 中的 pieline 添加一系列的 Handlerch.pipeline().addLast(new ProtobufDecoder(null));ch.pipeline().addLast(new ProtobufEncoder());}});
Channel
以客户端连接到服务端为例
客户端 Channel 的初始化过程
通过
BootStrap实例对象设置对应的Channel的类型,即上文的bootStrap.channel(NioSocketChannel.class)就是将当前的BootStrap的Channel类型设置为NioSocketChannel。这个过程只是指定了Channel的类型,同时设置了对应的ChannelFactory,并没有真正实例化Channel通过调用
Bootstrap的connect()方法完成Channel的实例化。具体调用链(在Boostrap):connect()——>doResolveAndConnect()——>initAndRegister()——>channelFactory.newChannel()完成 Channel 的初始化工作在
Channel的初始化过程中,首先会触发NioSocketChannel的构造器,使用默认的selector来创建一个NioSocketChannel,然后依次调用父类的方法进行一系列的初始化操作。
实例化的具体流程如下图所示:
服务端 Channel 的初始化过程
服务端的 Channel 初始化与客户端的 Channel 类似,不同的地方在于服务端的 Channel 是绑定到对应的主机和端口,而 客户端的 Channnel 是需要连接到服务器的 Channel。
客户端 Channel 的注册过程
在调用 BootStrap 的 bind() 方法的过程中,会调用到 initRegister() 方法,在这个方法中会对 Channel 进行初始化;在调用 register() 方法之后,会再次调用 EventLoopGroup 对象的 register(Channel channel) 方法将 Channel 注册到对应的 EventLoopGroup 中。具体源代码如下所示:
// 移除了处理异常等不相关的代码final ChannelFuture initAndRegister() {Channel channel = null;// 使用对应的 ChannelFactory 对象创建一个 Channelchannel = channelFactory.newChannel();init(channel);// 将 Channel 注册到对应的 EventLoopGroup 中ChannelFuture regFuture = config().group().register(channel);return regFuture;}
调用 EventLoopGroup 的 register(Channel channel) 方法会调用 AbstractUnsafe 的 register(EventLoop eventLoop, ChannelPromise promise) 方法来进行注册。具体源代码如下所示:
public final void register(EventLoop eventLoop, final ChannelPromise promise) {// 省略一部分检查的代码AbstractChannel.this.eventLoop = eventLoop;if (eventLoop.inEventLoop()) {register0(promise);}// 省略一部分代码。。。}
register0 源代码如下所示:
private void register0(ChannelPromise promise) {// 省略一些无用的代码doRegister();// 省略一些无用的代码}
由于当前使用的是 NioSocketChannel,因此会调用到 AbstractNioChannel 的 doRegister() 方法,具体的源代码如下所示:
protected void doRegister() throws Exception {boolean selected = false;for (;;) {try {/*javaChannel() :由于这里注册的 Channel 是 NioSocketChannel,因此会得到当前正要注册的 NioSocketChannel通过调用 Channel 的 register() 方法,将这个 Channel 注册到对应的 Selector 中,这里的 selector 对应着 NioEventLoop,即分配到的 EventLoop 对象,到此 Channel 的注册完成*/selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);return;} catch (CancelledKeyException e) {// 省略一部分异常捕获的代码}}}
总的流程图如下所示:
服务端 Channel 的注册过程
同样地,服务端的 Channel 的注册和客户端的也十分类似,不同的地方在于服务端的 Channel 会注册到“主” EventLoopGroup ,而客户端的 Channel 则只是注册到一个普通的 EventLoopGroup。
客户端 Channel 的连接过程

服务端 Channel 接受连接的过程
服务端的 Channel 在启动时会首先创建一个 NioServerSocketChannel 并注册到 “主” EventLoopGroup,用于创建对应的处理对应的连接请求。处理连接的请求在初始化 NioServerSocketChannel 的时候就已经准备好了,初始化服务端的 Channel 的源代码如下所示:
// 该代码位于 ServerBootStrap@Overridevoid init(Channel channel) {// 获取当前 Channel 的 PipelineChannelPipeline p = channel.pipeline();// 这里的 childGroup 是 “从”—EvenLoopGroup,用于真正处理请求final EventLoopGroup currentChildGroup = childGroup;final ChannelHandler currentChildHandler = childHandler;final Entry<ChannelOption<?>, Object>[] currentChildOptions = newOptionsArray(childOptions);final Entry<AttributeKey<?>, Object>[] currentChildAttrs = newAttributesArray(childAttrs);p.addLast(new ChannelInitializer<Channel>() {@Overridepublic void initChannel(final Channel ch) {final ChannelPipeline pipeline = ch.pipeline();ChannelHandler handler = config.handler();// 将在 bootStrap 中添加的 ChannelHandler 对象添加到 Pipeline 的末尾if (handler != null) {pipeline.addLast(handler);}ch.eventLoop().execute(new Runnable() {@Overridepublic void run() {// ServerBootstrapAcceptor 中重写了 channelRead() 方法,每个连接的请求都会首先调用这个方法以读取请求的内容pipeline.addLast(new ServerBootstrapAcceptor(ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));}});}});}
ServerBootstrapAcceptor 中 channelRead() 的源代码如下所示:
@Override@SuppressWarnings("unchecked")public void channelRead(ChannelHandlerContext ctx, Object msg) {final Channel child = (Channel) msg;// childHandler 由 ServerBootStrap 对象进行指定child.pipeline().addLast(childHandler);/*注意这里的 childGroup 对应的是 “从”—EventLoopGroup,由于 “从”—EventLoopGroup 是处理请求的,因此在这里得到的请求(即 msg)会为它新创建一个 Channel 注册到“从”—EventLoopGroup 的某个 EventLoop 中这个请求转发到 “从”—EventLoopGroup 的工作是由 “主”—EventLoopGroup 来完成的*/childGroup.register(child).addListener();// 省略部分异常检查代码。。。。}
channelRead() 的工作已经介绍完了,现在问题是 “主”—EventLoopGroup 是如何处理请求的了。这个和使用 NIO 进行网络编程有关系,当绑定当前的一个 NioServerSocketChannel时,Netty 的底层会监听对应的事件。当 NioServerSocketChannel 的状态为 SelectionKey.OP_ACCEPT时,表示当前的 NioServerSocketChannel 是可以接收连接请求的,当一个请求到达时,便会执行对应的请求,NioServerSocketChannel 执行对应请求的源代码如下所示:
/*因为每个 EventLoop 都是通过单个线程的方式来处理对应的任务的,同样地,NioServerSocketChannel 也是通过 EventLoop 来进行任务处理的该方法位于 NioEventLoop 中,因为初始化 NioServerSocketChannel 时指定了 NioEventLoop 来处理每个任务*/@Overrideprotected void run() {for (;;) {// 省略一大段异常处理和其它不是很关键的代码processSelectedKeys();}}
processSelectedKeys 源代码如下所示:
private void processSelectedKeys() {if (selectedKeys != null) {processSelectedKeysOptimized();} else {processSelectedKeysPlain(selector.selectedKeys());}}
尽管存在一个判断条件,但实际上,由于当前处理的 Channel 是 NioServerSocketChannel,最终都会调用 processSelectedKey 方法进行进一步的处理。具体的源代码如下所示:
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {// 省略一大段异常检查的代码int readyOps = k.readyOps();// 这里就是 NioServerSocketChannel 在可以读取时要进行处理的代码块。当当前的 Channel 是可读的或者是可接收请求是则执行对应的逻辑if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {/*这里的 Unsafe 对象由 NioServerSocketChannel 在实例化时创建,具体由 AbstractNioMessageChannel 通过重写 newSafe() 方法来实现这里的 Unsafe 类为 AbstractNioMessageChannel 的内部类*/unsafe.read();}}
unsafe.read()方法的源代码:
@Overridepublic void read() {// 核心代码如下,已经省略大部分其它不太重要的代码do {/*doReadMessages 由具体的子类来实现,在这里具体的子类为 NioServerSocketChannel每次调用 doReadMessages 都将创建一个新的 SocketChannel,即一个新的连接放入 readBuf 列表中*/int localRead = doReadMessages(readBuf);if (localRead == 0) {break;}if (localRead < 0) {closed = true;break;}allocHandle.incMessagesRead(localRead);} while (continueReading(allocHandle));int size = readBuf.size();// 将读取到的数据进行相应的处理for (int i = 0; i < size; i ++) {/*由于只有一个服务线程处理请求数据,因此就不会因为同时有多个请求进行访问而造成数据混乱*/readPending = false;pipeline.fireChannelRead(readBuf.get(i));}readBuf.clear();allocHandle.readComplete();pipeline.fireChannelReadComplete();}
doReadMessages() 方法源代码如下所示:
@Overrideprotected int doReadMessages(List<Object> buf) throws Exception {// 根据当前的 NioServerSocketChannel 创建一个新的 SocketChannelSocketChannel ch = SocketUtils.accept(javaChannel());if (ch != null) {// 可以看到,每次读取时都会为当前读取的数据段分配一个新的 NioSocketChannel 来进行相应的任务处理buf.add(new NioSocketChannel(this, ch)); // 新分配的 NioSocketChnnel 的return 1;}// 省略部分异常检测代码return 0;}
至此,NioServerSocketChannel 是如何分发请求到 NioSocketChannel 就已经非常清楚了
接收请求的具体流程如下所示:

ChannelPipeline
ChannelPipeline 是 ChannelHandler 链的容器,用于存储一系列的 ChannelHandler。
每当一个新的 Channel 被创建了,都会建立一个新的 ChannelPipeline,并且这个 ChannelPipeline 会绑定到 Channel 上。具体对应关系如下图所示:

ChannelPipeline 是双向的,但是同一时刻只能有一个方向的任务进行,如下图所示:

当一个入站事件被触发时,将会按照 ChannelInboundHandler 的顺序进行对应的处理。Netty 一般会认为入站的开始位置为 ChannelPiepeline 的开始位置,及上图上 SocketChannel 为ChannelPipeline 的开始位置,因此在添加对应的 ChannelHandler 时需要注意这一点。
ChannelPipeline 的初始化
初始化 ChannelPipeline 的源代码如下所示:
/*这里的 Channel 是之前初始化的 NioServerSocketChannel*/protected DefaultChannelPipeline(Channel channel) {this.channel = ObjectUtil.checkNotNull(channel, "channel");succeededFuture = new SucceededChannelFuture(channel, null);voidPromise = new VoidChannelPromise(channel, true);tail = new TailContext(this); // 上问 ChannelPipeline 示意图中的 head 节点,这是一个哑节点head = new HeadContext(this); // Pipeline 中的尾节点,同样也是一个哑节点// 组成一个双向链表,注意上文提到一个 Pipeline 可以是双向的,因此这里的数据结构采用双向链表head.next = tail;tail.prev = head;}
即初始化 ChannelPipeline 时,会维护一个由 ChannelHandlerContext 组成的双向链表,这个链表的作用是对相应的 Channel 进行i相关的事件处理。
ChannelHandler
服务端的 ChannelHandler
由于服务端存在两种类型的 EventLoopGroup,一个用于接收和分发请求,一个用于真正处理请求,因此 ChannelHandler 也分为两种,一类是用于接收请求时使用,一类是用于处理请求时使用。
ServerBootStrap通过 handler(ChannelHandler handler) 方法来指定接收请求是要执行的 ChannelHandler,这里的ChannelHandler添加是发生在初始化 Channel 的过程中(注意是初始化 Channel 而不是实例化 Channel)。具体的源代码如下所示:
@Overridevoid init(Channel channel) { // 这里的 Channel 是实例化之后的 Channel,即 NioServerSocketChannelChannelPipeline p = channel.pipeline();final EventLoopGroup currentChildGroup = childGroup;final ChannelHandler currentChildHandler = childHandler;final Entry<ChannelOption<?>, Object>[] currentChildOptions = newOptionsArray(childOptions);final Entry<AttributeKey<?>, Object>[] currentChildAttrs = newAttributesArray(childAttrs);p.addLast(new ChannelInitializer<Channel>() {@Overridepublic void initChannel(final Channel ch) {final ChannelPipeline pipeline = ch.pipeline();// 这里的 handler 是通过 ServerBootStrap 调用 handler(...) 来指定的ChannelHandler handler = config.handler();if (handler != null) {pipeline.addLast(handler);}ch.eventLoop().execute(new Runnable() {@Overridepublic void run() {pipeline.addLast(new ServerBootstrapAcceptor(ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));}});}});}
初始化之后,服务端 NioServerSocketChannel 的 Pipeline 的底层 Handler 结构如下图所示:

当接收到一个新的客户端请求时,会调用 ServerBootstrapAcceptor.channelRead 方法,具体源代码如下图所示:
public void channelRead(ChannelHandlerContext ctx, Object msg) {// 这里的 child 是分配到 workGroup 中的一个 Channel,不是服务端处理连接的 NioServerSocketChannelfinal Channel child = (Channel) msg;// 这里的 chilHandler 是在 ServerBootStrap 对象中通过 childHandler() 方法设置的child.pipeline().addLast(childHandler);// 省略一部分不重要的代码}
EventLoop
EventLoop 的初始化
NioEvenLoopGroup有几个构造器,但是最终都是调用父类 MultithreadEventLoopGroup 的构造器
调用父类的 MultithreadEventLoopGroup 构造器初始化:
private static final int DEFAULT_EVENT_LOOP_THREADS;static {// 初始化 EventLoop 线程数,这里设置的线程数为处理器的数量 * 2DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));if (logger.isDebugEnabled()) {logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);}}// 确定 EventLoop 的线程数,再交给父类进行构造protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);}
再次调用父类的构造函数进行初始化:
protected MultithreadEventExecutorGroup(int nThreads,Executor executor,EventExecutorChooserFactory chooserFactory,Object... args) {if (executor == null) {executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());}children = new EventExecutor[nThreads];for (int i = 0; i < nThreads; i ++) {children[i] = newChild(executor, args);}chooser = chooserFactory.newChooser(children);}
主要任务:
创建一个大小为
nThreads的EventExecutor数组通过调用
newChild来初始化 children 数组的每个元素根据
nThreads的大小,创建不同的EventExecutorChooser,如果nThreads是 2 的整数幂,则使用PowerOfTwoEventExecutorChooser,否则,使用GenericEventExecutorChooser。它们的功能够一样,都是从 children 数组中选出一个合适的EventExecutor实例源代码如下:
public EventExecutorChooser newChooser(EventExecutor[] executors) {if (isPowerOfTwo(executors.length)) {return new PowerOfTwoEventExecutorChooser(executors);} else {return new GenericEventExecutorChooser(executors);}}
总的初始化流程:
- EventLoopGroup(实际上是
MultithreadEventExecutorGroup)内部维护了一个类型为EventExecutor的 children 数组,其大小为nThreads,这样就创建了一个线程池 - 在
MultithreadEventLoopGroup中会确定要选择的EventLoop线程数,默认为可用的处理器大小 * 2 MultithreadEventExecutorGroup会调用newChild方法来初始化 children 数组的每个元素
具体的流程如下所示:

Netty 组件介绍的更多相关文章
- Netty组件介绍(转)
http://www.tuicool.com/articles/mEJvYb 为了更好的理解和进一步深入Netty,我们先总体认识一下Netty用到的组件及它们在整个Netty架构中是怎么协调工作的. ...
- Netty快速入门(09)channel组件介绍
书接上回,继续介绍组件. ChannelHandler组件介绍 ChannelHandler组件包含了业务处理核心逻辑,是由用户自定义的内容,开发人员百分之九十的代码都是ChannelHandler. ...
- 从netty-example分析Netty组件续
上文我们从netty-example的Discard服务器端示例分析了netty的组件,今天我们从另一个简单的示例Echo客户端分析一下上个示例中没有出现的netty组件. 1. 服务端的连接处理,读 ...
- 开源免费且稳定实用的.NET PDF打印组件itextSharp(.NET组件介绍之八)
在这个.NET组件的介绍系列中,受到了很多园友的支持,一些园友(如:数据之巅. [秦时明月]等等这些大神 )也给我提出了对应的建议,我正在努力去改正,有不足之处还望大家多多包涵.在传播一些简单的知识的 ...
- 免费开源的.NET多类型文件解压缩组件SharpZipLib(.NET组件介绍之七)
前面介绍了六种.NET组件,其中有一种组件是写文件的压缩和解压,现在介绍另一种文件的解压缩组件SharpZipLib.在这个组件介绍系列中,只为简单的介绍组件的背景和简单的应用,读者在阅读时可以结合官 ...
- 免费高效实用的.NET操作Excel组件NPOI(.NET组件介绍之六)
很多的软件项目几乎都包含着对文档的操作,前面已经介绍过两款操作文档的组件,现在介绍一款文档操作的组件NPOI. NPOI可以生成没有安装在您的服务器上的Microsoft Office套件的Excel ...
- 免费开源的DotNet任务调度组件Quartz.NET(.NET组件介绍之五)
很多的软件项目中都会使用到定时任务.定时轮询数据库同步,定时邮件通知等功能..NET Framework具有“内置”定时器功能,通过System.Timers.Timer类.在使用Timer类需要面对 ...
- 免费开源的DotNet二维码操作组件ThoughtWorks.QRCode(.NET组件介绍之四)
在生活中有一种东西几乎已经快要成为我们的另一个电子”身份证“,那就是二维码.无论是在软件开发的过程中,还是在普通用户的日常中,几乎都离不开二维码.二维码 (dimensional barcode) , ...
- 最好的.NET开源免费ZIP库DotNetZip(.NET组件介绍之三)
在项目开发中,除了对数据的展示更多的就是对文件的相关操作,例如文件的创建和删除,以及文件的压缩和解压.文件压缩的好处有很多,主要就是在文件传输的方面,文件压缩的好处就不需要赘述,因为无论是开发者,还是 ...
- 高效而稳定的企业级.NET Office 组件Spire(.NET组件介绍之二)
在项目开发中,尤其是企业的业务系统中,对文档的操作是非常多的,有时几乎给人一种错觉的是”这个系统似乎就是专门操作文档的“.毕竟现在的很多办公中大都是在PC端操作文档等软件,在这些庞大而繁重的业务中,单 ...
随机推荐
- spark修改控制台输出日志级别
spark修改控制台输出日志级别 修改conf/log4j.properties cd $SPARK_HOME/conf cp log4j.properties.template ./log4j.pr ...
- 微服务使用openfeign调用单点的会话失效问题
项目Springcloud,认证中心方式实现SSO使用开源框架Sa-Token 本身的单独访问每个客户端服务的单点就没有问题.然后单点通过Fegin调用就不好使了! 主要使用的Sa-Token的微服务 ...
- slate源码解析(三)- 定位
接口定义 能够对于文字.段落乃至任何元素的精准定位 并做出增删改查,都是在开发一款富文本编辑器时一项最基本也是最重要的功能之一.让我们先来看看Slate中对于如何在文档树中定位元素是怎么定义的[源码] ...
- 使用Java统计gitlab代码行数
一.背景: 需要对当前公司所有的项目进行代码行数的统计 二. 可实现方式 1.脚本:通过git脚本将所有的项目拉下来并然后通过进行代码行数的统计 样例: echo 创建项目对应的文件夹 mkdir 项 ...
- 10月TIOBE榜Java跌出前三!要不我转回C#吧
前言 Java又要完了,又要没了,你没看错,10月编程语言榜单出炉,Java跌出前三,并且即将被C#超越,很多资深人士预测只需两个月,Java就会跌出前五. 看到这样的文章,作为一名Java工程师我感 ...
- CF1352D
题目简化和分析: 这题可以直接按照题意进行模拟,当然有些细节需要注意. 翻译的不足:这里的回合指任意一个人吃掉都算,而不是双方一个回合,最后一个人即使不满足也算一个回合. 我们可以采用两个指针模拟两个 ...
- 全局关闭Unity编译的CS警告
实现方式 Editor和Game的全局CSharp编译配置文件名: Assets/mcs.rsp 添加如下内容可屏蔽对应的警告信息 -nowarn:1234 常用内容 CS0219 未使用的publi ...
- C# -WebAPIOperator.cs
说明:一个用C#编写的WebAPI操作类,只写了Get Post 部分. using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System ...
- 【scipy 基础】--积分和微分方程
对于手工计算来说,积分计算是非常困难的,对于一些简单的函数,我们可以直接通过已知的积分公式来求解,但在更多的情况下,原函数并没有简单的表达式,因此确定积分的反函数变得非常困难. 另外,相对于微分运算来 ...
- STM32F407 MCO输出的配置问题
当前使用IDE: RT-Thread Studio 版本: 2.1.0 构建ID: 202103221400 配置如下: int MCO1_GPIO_INIT(void) { GPIO_InitTyp ...