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 ...
随机推荐
- JavaScript每日学习日记(1)
8.11.2019 1. lastIndexOf() 方法从尾到头进行检索. 2. 有三种提取部分字符串的方法: 2.1 slice(start, end) 如果某个参数为负,则从字符串的结尾开始计 ...
- SpringMVC框架——文件的上传与下载
使用SpringMVC框架做个小练习,需求: 1.单个图片上传并显示到页面中: 2.多个图片上传并显示到页面中: 3.上传文件后下载文件: 1.pom.xml中添加依赖 <!-- 文件上传 -- ...
- hdu3367最大伪森林(并查集)
题目链接:http://icpc.njust.edu.cn/Problem/Hdu/3367/ 题目要求一个连通图的最大伪森林,伪森林是一个最多有一个回路的图.我们只要用Kruskal最大生成树的策略 ...
- nmap端口扫描工具安装和使用方法
nmap(Network Mapper)是一款开源免费的针对大型网络的端口扫描工具,nmap可以检测目标主机是否在线.主机端口开放情况.检测主机运行的服务类型及版本信息.检测操作系统与设备类型等信息. ...
- matplotlib.pyplot.text
matplotlib.pyplot.text(x, y, s, fontdict=None, withdash=<deprecated parameter>, **kwargs)[sour ...
- django中 对Mysql数据库的建表
Django操作Mysql数据库: 1.1 在settings中,配置数据库相关参数,所以无需修改,这里我们看一下: DATABASES = { 'default': { # 这里可以指定使用的数据库 ...
- unix中数据缓冲区高速缓冲的设计
目录 1. 概述 2. 缓冲区的设计 2.1 缓冲区头部 2.2 缓冲区的结构 2.3 缓冲区的检索算法 2.3. 申请一个缓冲区算法 getblk 2.3.2 释放一个缓冲区算法 brelse 2. ...
- [HDU2072]单词数<字符串>
链接:http://acm.hdu.edu.cn/showproblem.php?pid=2072 Problem Description lily的好朋友xiaoou333最近很空,他想了一件没有什 ...
- Eclipse打包jar
对一个包打jar包 右键包名-Export-Jar File-选择所在包的class文件(注意),如果选择java文件会失败-然后Finish 检查jar包是否正确,使用如jd-gui这样的反编译工具 ...
- Java并发基础03. 传统线程互斥技术—synchronized
在多个线程同时操作相同资源的时候,就会遇到并发的问题,如银行转账啊.售票系统啊等.为了避免这些问题的出现,我们可以使用synchronized关键字来解决,下面针对synchronized常见的用法做 ...