Netty是如何处理新连接接入事件的?

更多技术分享可关注我
前言
前面的分析从Netty服务端启动过程入手,一路走到了Netty的心脏——NioEventLoop,又总结了Netty的异步API和设计原理,现在回到Netty服务端本身,看看服务端对客户端新连接接入的处理是怎么样的过程。
Java NIO处理新连接的编码模板
首先,对于新连接接入,从NIO层面有一个宏观的印象:
1、通过I/O多路复用器——Selector检测客户端新连接
对应到Netty,新连接通过服务端的NioServerSocketChannel(底层封装的JDK的ServerSocketChannel)绑定的I/O多路复用器(由NioEventLoop线程驱动)轮询OP_ACCEPT(=16)事件
2、轮询到新连接,就创建客户端的Channel
对应到Netty就是NioSocketChannel(底层封装JDK的SocketChannel)
3、为新连接分配绑定新的Selector
对应到Netty,就是通过线程选择器,从它的第二个线程池——worker线程池中挑选一个NIO线,在这个线程中去执行将JDK的SocketChannel注册到新的Selector的流程,将Netty封装的NioSocketChannel作为附加对象也绑定到该Selector
4、向客户端Channel绑定的Selector注册I/O读、或者写事件
对应到Netty,就是默认注册读事件,因为Netty的设计理念是读优先。以后本条Channel的读写事件就由worker线程池中的NIO线程管理
以上4步,其实就是对下面一段JDK NIO demo的抽象和封装,并解决了一些bug的过程,如下:

接下来的几篇文章会逐步拆解每个步骤,并学习Netty的设计思路。
简单复习Netty的多线程Reactor架构
前面分析过NioEventLoopGroup和线程池对应,NioEventLoop实例和NIO线程对应,一个EventLoop实例将由一个永远都不会改变的Thread驱动其内部的run方法(和Runnable的run不是一个)。
简单说,Netty服务端创建的boss和worker就是两个线程池,对于一个服务器的端口,bossGroup里只会启动一个NIO线程用来处理该端口上的客户端新连接的检测和接入流程。
具体的说,Netty会在服务端的Channel的pipeline上,默认创建一个新连接接入的handler,只用于服务端接入客户端新连接,而workerGroup里有多个NIO线程(默认2倍的CPU核数个),负责已建立的Channel上的读写事件的检测、注册或者处理,等操作。当boss线程池的那一个NIO线程检测到新连接后就可以稍做休息(或者继续检测处理新连接),此时worker线程池就开始忙碌,如下图所示:

细节回顾可以参考:Netty的线程调度模型分析(1)
下面开始总结,boss线程和worker线程池之间是如何配合的。
再看JDK的select方法
在总结之前,个人认为有必要先回顾JDK的select,必须正确理解I/O多路复用器——Selector上所谓的轮询一次,返回就绪的Channel数目的真正意义,即这个过程有一个前提是自从上次select后开始计算的。这样干巴巴的解释可能不太清楚,下面举个例子,比如有两个已经建立的Channel,分别是A和B,而且A和B分别注册到了一个Selector上,接着在该Selector调用select():
第一次调用select(),发现只有A有I/O事件就绪,select会立即返回1,然后处理之
第二次调用select(),发现另一个通道B也有I/O事件就绪,此时select()还是返回1——即是自上次select后开始计算的
还有一点注意:如果第一次轮询后,对A没有做任何操作,那么就有两个就绪的Channel。
另外还要知道,select返回后可通过其返回值判断有没有Channel就绪,如果有就绪的Channel,那么可以使用selectedKeys()方法拿到就绪的Channel及其一些属性。下面看selectedKeys()的使用:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
当给Selector注册Channel时,调用的register()方法会返回一个SelectionKey对象,这个对象代表了注册到该Selector的Channel,可以遍历这个集合来访问就绪的通道。
以上,前面的线程调度模型都分析过,回忆这个图:

细节回顾可以参考:
Netty处理新连接接入事件的源码分析
前面文章总结了NioEventLoopGroup实例化时,如果外部没有配置,那么会默认创建一个线程执行器——ThreadPerTaskExcutor,一个NioEventLoop组成的数组(线程池),还有一个线程选择器——chooser。
又知道当实例化NioEventLoop并填充底层线程数组时,Netty会为每个NioEventLoop创建并绑定一个I/O多路复用器——Selector和一个异步任务队列——MPSCQ,接下来又总结了Netty的NioEventLoop线程启动的触发时机有两个:
宏观上,服务端绑定端口时会触发boss线程池里的一个NIO线程启动,即用户代码调用bind方法。如果深入bind方法内部,那么会发现NIO线程第一次启动的精确时机是为JDK的ServerSocketChannel注册I/O多路复用器的时候——Netty会封装这个注册逻辑为一个异步task,使用NIO线程驱动,如果没有启动,那么就启动之,以后的Channel绑定端口的逻辑也会被封装为异步task,复用已经启动的这个NIO线程
新连接接入时会触发worker线程池里的NIO线程启动。线程池的线程选择器会为新连接绑定一个worker里的NIO线程,第一次接入或者线程池的线程还没完全启动完毕,就会顺势启动
总之,Netty服务端启动后,服务端的Channel已经绑定到了boss线程池的NIO线程中,并不断检测是否有OP_ACCEPT事件发生,直到检测出有该事件发生就处理之,即boss线程池里的NioEventLoop线程只做了两件事:
1、轮询OP_ACCEPT事件
2、检测到OP_ACCEPT事件后就处理该事件,处理过程其实就是客户端Channel(新连接)接入的过程
下面继续回顾NioEventLoo线程的事件循环的核心方法——run,它在NIO线程启动时开始运行:

在这之前,先在run方法打断点:然后启动实验用的最小版Netty服务端的demo,之后分别在三个客户端使用telnet命令对其顺序发送3个请求,模拟客户端3个新连接接入的过程,下面进入run跟踪源码:

1、首先调用Netty封装的select方法,前面分析过当有客户端新连接接入,即代表已经触发了OP_ACCEPT事件,Selector的select方法会立即返回1,如下:

这里要理解JDK的select方法返回值到底是什么。select()方法会返回注册的interest的I/O事件已经就绪的那些通道的数目,抠字眼,首先得看是哪些Channel注册在了当前I/O多路复用器上,其次,看这些Channel上注册的interest的I/O事件是否就绪,如上代码的局部变量selectedKeys==1,但是我实验的客户端连接是3个,这里可能会有疑问,selectedKeys为何不是3呢?
因为当前绑定在boss线程上的I/O多路复用器只注册了服务端的Channel,即底层只有一个ServerSocketChannel,且当前注册的interest的I/O事件只有OP_ACCEPT,故无论多少个新连接接入,这里都只会返回1。
还有一个误区:不要认为Selector的select返回值是已准备就绪的Channel的总数,其实它返回的是从上一个select()调用后进入就绪状态的Channel的数量。
继续分析:轮询出有感兴趣的I/O事件就绪的Channel后,会break循环,回到外部的run方法,开始处理这个I/O事件,这里就是处理新连接的接入事件,核心方法之前也分析过,就是processSelectedKeys:

在详细的细节可以参考:
这个方法有两个变体,前面文章也分析过原因,我选择有代表性的processSelectedKeysOptimized,看里面的processSelectedKey(key,channel)方法,这才真正到了Netty处理I/O事件的方法入口,如下:

如下是processSelectedKey方法的实现:

首先看黄色1处,取出ServerSocketChannel的unsafe对象,前面也总结过,Netty封装的Channel的底层都会有一个Unsafe对象与之绑定,Unsafe是个内部接口,聚合在Channel接口内部,作用是协助Channel进行网络I/O的操作,因为它的设计初衷就是Channel的内部辅助类,不应该被Netty的使用者调用,所以被命名为Unsafe,而不是说这个类的API都是不安全的。
继续执行到黄色2处,会判断当前Channel是否打开,其实就是判断的ServerSocketChannel。一切顺利继续执行黄色3处,看到了熟悉的NIO API,下面专门看黄色3处后面的一堆代码:

在黄色3处,k内部的readyOps集合是该Channel已经准备就绪的I/O操作的集合,OP_ACCEPT这个宏是16,所以这里的readyOps变量为16。
接着马上会执行到黄色4处的if判断逻辑,由于readyOps为16,这里通过判断,进入if内部,执行黄色5处的代码。该处逻辑是一个read操作,很好理解。当NioEventLoop的run方法里轮询到ServerSocketChannel的accept事件后,服务端第一步就是对其执行读操作,这是很自然的想法。因为这是服务端,所以下面会进入到NioMessageUnsafe实例的read方法:

在黄色1处,首先保证是NioEventLoop线程在执行,如果是外部线程执行的,那么无效。接下来,会获取服务端Channel的Config和默认创建的服务端Channel的pipeline。在黄色2处有一个RecvByteBufAllocator.Handle allocHandle变量,它获取了RecvByteBuf分配器Handle,顾名思义就是设置接收的缓冲区大小,简单说是通过二分算法获取一个不会浪费空间,但是又足够大小的缓冲区,是一种性能优化的策略,以后分析Netty内存图像时在深入。
接着在黄色2处的下一行是一个重置配置的方法,目的是重置已累积的所有计数器,并为下一个读取循环读取多少消息/字节数据提供建议。Netty默认一次读取16个新连接,如下:

然后继续看NioMessageUnsafe实例的read方法,在黄色3处,进入一个do-while循环:

首先调用doReadMessages方法,在do—while循环中读取一个个的客户端新连接,并将读取到的新连接用readBuf这个集合存储,readBuf就是NioMessageUnsafe类内部的一个普通的ArrayList。
下面进入doReadMessages方法,如下该方法内部逻辑似曾相识。

首先,在黄色1处封装了JDK的NIO API,即获取客户端的socket——NIO对应的是SocketChannel,完成该操作意味着TCP/IP协议栈完成了TCP的三次握手,TCP的逻辑链路正式建立,然后,在黄色2处,Netty将客户端Channel封装为自己的客户端channel——NioSocketChannel。因为这里明确了是服务端在处理accept事件,故不需要反射创建NioSocketChannel,直接实例化即可,后续在详细分析Netty的客户端channel创建过程。最后,封装的Channel保存到readBuf这个ArrayList中,doReadMessages方法返回1。
回到上层的do-while循环:

doReadMessages返回的localRead==1,说明本次读取新连接成功,do-while的一次循环读新连接完毕,会继续读下一个新连接,直到全部读完,或者达到阈值。也就是说Netty在读取新连接时也权衡了性能,如果连接太多,那么Netty不会一直卡在这里处理,它默认do-while循环处理16个,这个逻辑在黄色5处的判断条件里,超过阈值就退出do-while。
下面看黄色5处的判断逻辑——即continueReading()方法,简单看下:

Netty设计理念是读优先,会给服务端Channel自动注册OP_READ事件——也就是isAutoRead()方法会返回true,那个maxMessagePerRead默认配置的是16,即每一次集中处理accept事件时,最多读取的连接数为16个,是权衡了性能而设计的,这个可以由用户配置。
继续回看NioMessageUnsafe实例的read方法,如果有新连接,那么继续do-while循环,直到发生异常,或者读取的新连接数量达到了阈值,或者已经没有新连接可读,doReadMessages返回0,退出do-while循环。这里说明一下,正常情况doReadMessages里的accept一定不会阻塞,因为只有当Channel里有就绪的I/O事件,换句话说,有数据可以读,才会进入accept环节,本质是因为Netty服务端为NIO模型配置的是非阻塞I/O,即Netty会自动对各个Channel有如下的配置:

而且,如果服务端Channel有就绪的I/O事件,那么accept()一定会返回客户端Channel,除非实例化Netty的客户端Channel——NioSocketChannel时出现异常。
如果doReadMessages返回0,那么就会break出do-while循环,接下来大动脉——Netty的pipeline就该干活了,如下NioMessageUnsafe实例的read方法的后面的源码:

在黄色6处,遍历保存客户端新Channel的集合——readBuf,然后将每个新连接传播出去——调用pipeline.fireChannelRead(),将每条新连接沿着服务端Channel的pipeline传递,交给Channel后续的入站handler,而黄色7处,会传播一个读操作完成的事件——fireChannelReadComplete();后续会逐渐的拆解并详细分析pipeline的设计,这里知道即可。
至此,Netty服务端检测处理客户端新连接的过程分析完毕。
做个小结
1、权衡性能,NIO线程一次处理的新连接不能太多,Netty默认是一次最多处理16个
2、Netty的pipeline机制和读取新连接后的衔接过程——触发和传递
3、Selector的select返回值的理解
4、深刻理解同步非阻塞,即NIO模式下,accept方法为什么不会阻塞
Netty是如何处理新连接接入事件的?的更多相关文章
- Netty服务端NioEventLoop启动及新连接接入处理
一 Netty服务端NioEventLoop的启动 Netty服务端创建.初始化完成后,再向Selector上注册时,会将服务端Channel与NioEventLoop绑定,绑定之后,一方面会将服务端 ...
- Netty 学习(八):新连接接入源码说明
Netty 学习(八):新连接接入源码说明 作者: Grey 原文地址: 博客园:Netty 学习(八):新连接接入源码说明 CSDN:Netty 学习(八):新连接接入源码说明 新连接的接入分为3个 ...
- Netty源码 新连接处理
上文我们阐述了Netty的Reactor模型.在Reactor模型的第二阶段,Netty会处理各种io事件.对于客户端的各种请求就是在这个阶段去处理的.本文便来分析一个新的连接是如何被处理的. 代码的 ...
- Netty 服务端:新连接接入
本文主要分析服务端新连接的接入过程,主要分为以下 3 各步骤: select 操作: processSelectedKeys 操作. 1. select 操作 在分析 select 操作前,先要回顾一 ...
- Netty服务端接收的新连接是如何绑定到worker线程池的?
更多技术分享可关注我 前言 原文:Netty服务端接收的新连接是如何绑定到worker线程池的? 前面分析Netty服务端检测新连接的过程提到了NioServerSocketChannel读完新连接后 ...
- 恶劣的网络环境下,Netty是如何处理写事件的?
更多技术分享可关注我 前言 前面,在Netty在接收完新连接后,默认为何要为其注册读事件,其处理I/O事件的优先级是什么?这篇文章,分析到了Netty处理I/O事件的优先级——读事件优先,写事件仅仅是 ...
- Netty-新连接接入源码解读
本片博文来看Netty的服务端是如何处理新连接接入问题的 什么是新连接接入?以及新连接接入前,Netty处于什么状态 netty的服务端NioServerSocketChannel初始化,注册在Bos ...
- Netty源码分析第3章(客户端接入流程)---->第2节: 处理接入事件之handle的创建
Netty源码分析第三章: 客户端接入流程 第二节: 处理接入事件之handle的创建 上一小节我们剖析完成了与channel绑定的ChannelConfig初始化相关的流程, 这一小节继续剖析客户端 ...
- Netty 4.0 新的特性及需要注意的地方
Netty 4.0 新的特性及需要注意的地方 这篇文章和你一起过下Netty的主发行版本的一些显著的改变和新特性,让你在把你的应用程序转换到新版本的时候有个概念. 项目结构改变 Netty的包名从or ...
随机推荐
- Java-迭代器(新手)
//导入的包.import java.util.ArrayList;import java.util.Collection;import java.util.Iterator;//创建的一个类.pub ...
- K:剑指offer-56 题解 谁说数字电路的知识不能用到算法中?从次数统计到数字电路公式推导,一文包你全懂
前言: 本题解整理了一位大佬在leetcode中的代码的方法,该博文致力于让所有人都能够能够看懂该方法.为此,本题解将从统计数字出现次数的解题方式开始讲起,再推导出逐位统计的解题方式,期望以循序渐进的 ...
- C语言经典练习题:水仙花数
废话不说直接进入正题:传说中的水仙花数就是一个三位数的各个位数的三次方之和就是这个数本身例如153=1*1*1+5*5*5+3*3*3. 好,开始解题:首先,题中已经说了水仙花数是一个三位数,也就是说 ...
- CompTIA Security+ 常见知识点
前言: Security+ 认证是一种中立第三方认证,其发证机构为美国计算机行业协会CompTIA: 是和CISSP.CISA等共同包含在内的国际IT业热门认证之一,和CISSP偏重信息安全管理相比, ...
- 解决 Mac Android Studio Gradle Sync 慢的问题
1.启动Android Studio 2.从项目的 gradle/wrapper/gradle-wrapper.properties 目录中找到 distributionUrl 这个字段,查看后面对应 ...
- 终极解决方案之——Centos7由于误删或更新python导致 No module named yum
之前由于不懂yum和python之间的关系,因为一直在学python3,看到系统里/usr/lib下的python2我就直接删了,结果... 可能还有人是因为python升级的原因,即系统自带的pyt ...
- 1036. 跟奥巴马一起编程(15) Java版
美国总统奥巴马不仅呼吁所有人都学习编程,甚至以身作则编写代码,成为美国历史上首位编写计算机代码的总统.2014年底,为庆祝"计算机科学教育周"正式启动,奥巴马编写了很简单的计算机代 ...
- Kannada-MNIST:一个新的手写数字数据集
TLDR: 我正在传播2个数据集: Kannada-MNIST数据集:28x28灰度图像:60k 训练集 | 10k测试集 Dig-MNIST:28x28灰度图像:10240(1024x10)(见下图 ...
- Codeforces 631 (Div. 2) C. Dreamoon Likes Coloring 思维or构造
https://codeforces.com/contest/1330/problem/C 给n个格子染色,有m种颜色,要求最后的所以格子被染色,并且有m种颜色. 染色要求:每种颜色有一个值li,选择 ...
- C++STL(一)——string类
STL--string类 初始化 string的赋值 string的连接 string的性质描述 遍历 字符指针和string的转化 查找.替换.交换 字符串的拼接 区间删除. 插入 大小写转换 比较 ...