本文简单介绍下netty的基本原理,I/O模型,Reactor线程模型以及架构设计等相关知识点。html
Netty是由JBOSS提供的一个Java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具
,用以快速开发高性能、高可靠性的网络服务器和客户端程序。java
Netty 是一个基于NIO的客户、服务器端编程框架
,使用Netty 能够确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty至关简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的socket服务开发。
Netty是由NIO演进而来,使用过NIO编程的用户就知道NIO编程很是繁重,Netty是可以能跟好的使用NIOreact
IO在计算机中指Input/Output,也就是输入和输出
。因为程序和运行时数据是在内存中驻留,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,一般是磁盘、网络
等,就须要IO接口。linux
好比你打开浏览器,访问新浪首页,浏览器这个程序就须要经过网络IO获取新浪的网页。浏览器首先会发送数据给新浪服务器,告诉它我想要首页的HTML,这个动做是往外发数据,叫Output,随后新浪服务器把网页发过来,这个动做是从外面接收数据,叫Input。因此,一般,程序完成IO操做会有Input和Output两个数据流。固然也有只用一个的状况,好比,从磁盘读取文件到内存,就只有Input操做,反过来,把数据写到磁盘文件里,就只是一个Output操做。web
如今操做系统都是采用虚拟存储器,那么对32位操做系统
而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)操做系统的核心是内核,独立于普通的应用程序,能够访问受保护的内存空间,也有访问底层硬件设备(例如负责磁盘IO的设备)的全部权限。为了保证用户进程不能直接操做内核(kernel),保证内核的安全,操做系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
ajax
针对linux操做系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0×00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。编程
是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操做系统结构的基础
。segmentfault
在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操做系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。数组
是操做系统可以进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运做单位
。一条线程指的是进程中一个单一顺序的控制流,一个进程中能够并发多个线程,每条线程并行执行不一样的任务。浏览器
1个程序能够对应多个进程,但1个进程只能对应1个程序。
说白了就是,一个程序能够重复运行,开几个窗口,好比网游的“双开”
,一个进程能够对应多个程序就是一个DLL文件可一被多个程序运用,好比DirectX9的动态连接库,就是,许多游戏都要有它才能运行。
咱们简单总结下:
进程包含线程。
进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。
线程:系统分配处理器时间资源的基本单元,或者说进程以内独立执行的一个单元执行流。线程——程序执行的最小单位。
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复之前挂起的某个进程的执行
。这种行为被称为进程切换。所以能够说,任何进程都是在操做系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另外一个进程上运行,这个过程当中通过下面这些变化:
注:总而言之就是很耗资源
正在执行的进程,因为期待的某些事件未发生,如请求系统资源失败、等待某种操做的完成、新数据还没有到达或无新工做作等,则由系统自动执行阻塞原语(Block),使本身由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也所以只有处于运行态的进程(得到CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的
。
简单理解:一个指向文件自己的指针(由系统所管理的引用标识,该标识能够被系统从新定位到一个内存地址上,间接访问对象 ),值是非负整数。
文件描述符(File descriptor,简称fd)
是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者建立一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写每每会围绕着文件描述符展开。可是文件描述符这一律念每每只适用于UNIX、Linux这样的操做系统。
Windows下的概念
。句柄是Windows下各类对象的标识符,好比文件、资源、菜单、光标等等。文件句柄和文件描述符相似,它也是一个非负整数,也用于定位文件数据在内存中的位置。
大多数文件系统的默认IO都是缓存IO。过程是:数据先被拷贝到操做系统的内核缓冲区(页缓存 page cache)中,而后再拷贝到应用程序的地址空间。
所谓同步,发起一个功能调用的时候,在没有获得结果以前,该调用不返回
,也就是必须一件事一件事的作,等前一件作完了,才能作下一件。
main函数
int main(){
add();
sout();
}
int add(){
return 1+1;
}
main函数 int main(){ ajax(); sout(); } int ajax(){ return 1+1; }
调用发出后,调用者不能马上获得结果,而是实际处理这个调用的函数完成以后,经过状态、通知和回调来通知调用者
。
好比ajax:
请求经过事件触发->服务器处理(这是浏览器仍然能够做其余事情)->处理完毕
(在服务器处理的时候,客户端还能够干其余的事)
指调用结果返回以前,当前线程会被挂起
(CPU不给线程分配时间片),函数只能在获得结果以后才会返回。
(阻塞调用和同步调用的区别)同步调用的时候,当前线程仍然多是激活的,只是在逻辑上当前函数没有返回。例如:在Socket中调用recv函数,若是缓冲区没有数据,这个函数会一直等待,知道数据返回。而在此时,这个线程仍是能够处理其余消息的。
非阻塞调用指在不能马上获得结果以前,该调用不会阻塞当前线程。
同步是指A调用了B函数,B函数须要等处理完事情才会给A返回一个结果。A拿到结果继续执行。
异步是指A调用了B函数,A的任务就完成了,去继续执行别的事了,等B处理完了事情,才会通知A。
阻塞是指,A调用了B函数,在B没有返回结果的时候,A线程被CPU挂起,不能执行任何操做(这个线程不会被分配时间片)
非阻塞是指,A调用了B函数,A不用一直等待B返回结果,能够先去干别的事。
老张爱喝茶,废话不说,煮开水。 出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
所谓同步异步,只是对于水壶而言。
普通水壶,同步;响水壶,异步。
虽然都能干活,但响水壶能够在本身完工以后,提示老张水开了
。这是普通水壶所不能及的。同步只能让调用者去轮询本身(状况2中),形成老张效率的低下。
所谓阻塞非阻塞,仅仅对于老张而言。立等的老张,阻塞;看电视的老张,非阻塞。状况1和状况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。因此通常异步是配合非阻塞使用的,这样才能发挥异步的效用。
recvfrom函数用于从(已链接)套接口上接收数据,并捕获数据发送源的地址。
本函数用于从(已链接)套接口上接收数据,并捕获数据发送源的地址。
(简单理解就是客户端等待服务端给数据的函数)
举个例子,其中的角色,客人(小明)对应内核线程,服务员对应的是用户线程。如今大黄在南亭新开了一家黄焖鸡,小明(客人)以为很新鲜,准备喊上几个基友去南亭搓一顿黄焖鸡。
小明到店里了,若是小明成功点餐则须要通过两个步骤,第一步是思考要点什么吃的,第二步是跟服务员说要吃什么。
该模型的内核线程分为两个阶段
如今黄焖鸡的老板大黄给每一个客人都配一个服务员,只要有一个客人来的话,就在旁边等客人思考吃什么而且进行点餐。只要客人尚未点餐完毕,对应的这个服务员就不能离开去作别的事情。
映射到Linux操做系统中,这就是阻塞的IO模型。在linux中,默认状况下全部的socket都是blocking,一个典型的读操做流程大概是这样:
用户线程(服务员) 内核线程(客人)
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的两个阶段:
准备数据
(对于网络IO来讲,不少时候数据在一开始尚未到达。好比,尚未收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程须要等待,也就是说数据被拷贝到操做系统内核的缓冲区中是须要一个过程的。而在用户进程这边,整个进程会被阻塞
(固然,是进程本身选择的阻塞)。将数据从kernel中拷贝到用户内存
,而后kernel返回结果,用户进程才解除block的状态,从新运行起来。因此,blocking IO的特色就是在IO执行的两个阶段(等待IO(准备IO)和执行IO)都被block了。
如今随着有些大学生月初拿到零花钱,开始浪了,黄焖鸡的生意也变得愈来愈火爆了,来吃饭的客人愈来愈多,要配的服务员也愈来愈多,黄焖鸡的老板大黄心想这不对劲啊,要是忽然同时来100个客人,就要有100个服务员,确定巨亏啊,得想个法子提升效率。这时候老板大黄想到,在客人想点什么吃的时候,服务员彻底能够去作别的事情,例如去给别的桌的客人点餐,只要偶尔过来问下客人是否要点餐了,一旦发现客人须要点餐了,就开始点餐。
映射到Linux操做系统中,这就是非阻塞的IO模型。linux下,能够经过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操做时,流程是这个样子:
因此,nonblocking IO的特色是用户进程须要不断的主动询问kernel数据好了没有。
过了一段时间,客人们好多都反应,大家服务员太烦人了,成天问我是否是能够点餐了,客人说咱们干脆要点餐的时候就叫服务员过来好了,老板大黄心想这样也不错,能提升餐厅的运行效率,赚多点钱,便答应了(这时候的服务员还在客人旁边傻乎乎的站着,只等着客人喊他点餐)。
映射到Linux操做系统中,这就是信号驱动I/O。当数据报准备好的时候,内核会向应用程序发送一个信号,进程对信号进行捕捉,而且调用信号处理函数来获取数据报。
老板大黄巡查店内状况,看到了服务员大多都傻乎乎的站着等客人喊他点餐,做为资本家的大黄,固然是要充分利用劳动力的。因此老板给服务员们开了个会,安排他们一我的负责一个区域(多个客人)的客人点餐需求,等到客人喊他点餐时,就过去点餐。
映射到Linux操做系统中,这就是I/O 多路复用。IO multiplexing就是咱们说的select,poll,epoll
,有些地方也称这种IO方式为event driven IO
。select/epoll的好处就在于单个process就能够同时处理多个网络链接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的全部socket,当某个socket有数据到达了,就通知用户进程
。
当用户进程调用了select,那么整个进程会被block
,而同时,kernel会“监视”全部select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操做,将数据从kernel拷贝到用户进程。
因此,I/O 多路复用的特色是经过一种机制
一个进程能同时等待多个文件描述符
,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就能够返回。
这个图和blocking IO的图其实并无太大的不一样,事实上,还更差一些。由于这里须要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。可是,用select的优点在于它能够同时处理多个connection。
因此,若是处理的链接数不是很高的话,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优点并非对于单个链接能处理得更快,而是在于能处理更多的链接
。)
在IO multiplexing Model中,实际中,对于每个socket,通常都设置成为non-blocking
,可是,如上图所示,整个用户的process实际上是一直被block的。只不过process是被select这个函数block
,而不是被socket IO给block。
后来,随着黄焖鸡的味道你们都比较喜好,口碑逐渐创建了起来,生意愈加地火爆,经常座无虚席,大黄也开始梦想着多久能实现一个小目标。大学城里有些创业团队看到了黄焖鸡的火爆程度,看到服务员有不少时候是在给客人点餐,他们结合本身的专业知识,跟老板说给老板开发个手机点餐系统,客人经过手机就能点餐,服务员只须要查看系统的点餐状况进行上菜就好,老板大黄一看,秒啊,急忙答应了,经过点餐系统,餐厅运行效率更高了,接待的客人也愈来愈多,离一个小目标的梦想也愈来愈近了。
映射到Linux操做系统中,这就是异步 I/O。Linux下的asynchronous IO其实用得不多。先看一下它的流程:
用户进程发起read操做以后,马上就能够开始去作其它的事。而另外一方面,从kernel的角度,当它受到一个asynchronous read以后,首先它会马上返回,因此不会对用户进程产生任何block。而后,kernel会等待数据准备完成,而后将数据拷贝到用户内存,当这一切都完成以后,kernel会给用户进程发送一个signal,告诉它read操做完成了。
用户进程只须要知道内核线程处理的结果
数组(长度1024)存储全部的fd
说的通俗一点就是各个客户端链接的文件描述符也就是套接字,都被放到了一个集合中,调用select函数以后会一直监视这些文件描述符中有哪些可读,若是有可读的描述符那么咱们的工做进程就去读取资源。
select 在一个进程内能够维持最多 1024 个链接
链表存储全部的fd
poll 和 select 的实现很是相似,本质上的区别就是存放 fd 集合的数据结构不同。poll 在select基础上作了增强,能够维持任意数量的链接
。
但 select 和 poll 方式有一个很大的问题就是,咱们不难看出来 select和poll 是经过轮循
的方式来查找是否可读或者可写,打个比方,若是同时有100万个链接都没有断开,而只有一个客户端发送了数据,因此这里它仍是须要循环这么屡次,形成资源浪费。
链表存储ready的fd
不须要遍历所有fd去找ready的,其所有的fd放在一个红黑树
中加以维护。
epoll 是 select 和 poll 的加强版,epoll 同 poll 同样,文件描述符数量无限制
。
epoll是基于内核的反射机制,在有活跃的 socket 时,系统会调用咱们提早设置的回调函数
。而 poll 和 select 都是遍历。
可是也并非全部状况下 epoll 都比 select/poll 好,好比在以下场景:
在大多数客户端都很活跃的状况下,系统会把全部的回调函数都唤醒,因此会致使负载较高
。既然要处理这么多的链接,那倒不如 select 遍历简单有效。
在Java中提供了三种IO模型:BIO、NIO、AIO,模型的选择决定了程序通讯的性能。
BIO
BIO适用于链接数比较小的应用,这种IO模型对服务器资源要求比较高。
NIO
NIO适用于链接数目多、链接时间短的应用,好比聊天、弹幕、服务器间通信等应用。
AIO
AIO适用于链接数目多、链接时间长的应用,好比相册服务器。
无脑建立线程
改用线程池
同步非阻塞模型
,服务器端用一个线程处理多个链接
,客户端发送的链接请求会注册到多路复用器上,多路复用器轮询
到链接有IO请求就进行处理:
NIO的非阻塞模式,使得一个线程从某通道发送请求或者读取数据时,若是目前没有可用的数据,不会使线程阻塞,在数据可读以前,该线程能够作其余的事情。
NIO有三大核心部分:
由图可知:
本质是能够读写数据的内存块,Channel读取或者写入的数据必须经过Buffer:
java.nio.Buffer抽象类的属性:
复制代码
// Invariants: mark <= position <= limit <= capacity private int mark = -1; private int position = 0; private int limit; private int capacity;
读写交换要使用flip方法。
通道是双向的,能够读操做、也能够写操做。
java.nio.channels.Channel接口的经常使用实现类:
FileChannel用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel和SocketChannel用于TCP的数据读写。
Selector选择器使用一个线程来维护。多个Channel会以事件的方式注册到同一个Selector,当有事件发生时,Selector会获取事件,而后针对每一个事件进行响应的处理。这样就没必要为每一个链接建立一个线程,不用维护多线程,也不会有多线程之间的上下文切换致使的系统的开销。
Selector示意图:
异步非阻塞模型
,AIO引入异步通道的概念,使用了Proactor,只有有效的请求才启动线程,特色是先由操做系统完成后,才通知服务器端程序启动线程去处理
,通常适用于链接数较多且链接时间较长的应用。
Reactor线程模型是基于同步非阻塞IO实现的。对于异步非阻塞IO的实现是Proactor模型。
Netty就是基于Reactor线程模型开发的,咱们今天来简单分析下:
Reactor模型中的三种角色及含义: Reactor:将I/O事件分配给对应的handler。 Acceptor:处理客户端新链接,并分派请求处处理器链中。 Handlers:执行非阻塞读写任务。
Reactor经常使用的线程模型有三种
单线程模型简图
单线程模型就是指全部的I/O操做都是在一个线程中处理完成,NIO的线程须要接受客户端的Tcp链接,而且向客户端发送Tcp链接,读取通讯两端的请求或应答,发送请求和应答。
单线程模型详细图解
大体了解了后,让咱们看下这个详细流程,当客户端发起链接,Acceptor负责接收客户端的Tcp请求,链路创建成功后,经过Dispatcher将对应的ByteBuffer派发到指定的Hnadler上进行消息解码,用户Handler经过NIO线程将消息发送给客户端。单线程模型其实就是Acceptor的处理和Handler的处理都处在同一个线程中,当其中的一个Hnadler阻塞时,会致使其它的client和handler没法执行,甚至整个服务不能接受新的请求。
单线程模型缺点:不适用于高负载,高并发的场景。
由于一个NIO线程若是同时处理不少的链路,则机器在性能上没法知足海量的消息的编码,解码,读取和发送。若是NIO线程负载太重,处理速度变慢,会致使大量的客户端请求超时,甚至致使整个通讯模块不可用。
为了解决单线程模型的缺点,设计出了多线程模型。以下简图:
多线程模型简图
如图所示在多线程模型下,用一个专门的NIO线程Acceptor来监听客户端的Tcp请求
,对于网络I/O的读写操做和消息的读取、编码、解码、发送等使用NIO线程池来完成。由于客户端请求数量大于NIO线程池中的线程,一个NIO线程能够同时处理多条链路请求,可是一个链路请求只对应一个NIO线程。Reactor多线程模型可以大多数的使用场景,可是当客户端的并发链接很是的多,或者是服务端须要对客户端进行安全认证等,单个Acceptor线程可能会存在性能不足的问题。
Reactor的主从多线程模型
如图所示,从这个简图能够看出,服务端用于监听和接收客户端链接的再也不是单个线程,而是分配了一个线程池。Acceptor线程池接收了客户端的请求链接并处理完成后(可能包含了权限认证等),后续的I/O操做再由NIO线程池来完成。这样就解决了多线程中客户端请求太多或者须要认证时一个Acceptor可能处理不过来的性能问题。
netty的线程模型是能够经过设置启动类的参数来配置的,设置不一样的启动参数,netty支持Reactor单线程模型、多线程模型和主从Reactor多线程模型
。
NettyServer总体架构图.png
server端启动时绑定本地某个端口,将本身NioServerSocketChannel注册到某个boss NioEventLoop的selector上。
server端包含1个boss NioEventLoopGroup和1个worker NioEventLoopGroup,NioEventLoopGroup至关于1个事件循环组,这个组里包含多个事件循环NioEventLoop,每一个NioEventLoop包含1个selector和1个事件循环线程。
每一个boss NioEventLoop循环执行的任务包含3步:
每一个worker NioEventLoop循环执行的任务包含3步:
NettyClient总体架构图.png
client端启动时connect到server,创建NioSocketChannel,并注册到某个NioEventLoop的selector上。
client端只包含1个NioEventLoopGroup,每一个NioEventLoop循环执行的任务包含3步:
服务端启动时建立了两个NioEventLoopGroup,一个是boss,一个是worker。实际上他们是两个独立的Reactor线程池,一个用于接收客户端的TCP链接
,另外一个用于处理Io相关的读写操做
,或者执行系统/定时任务的task
。
boss线程池做用:
(1)接收客户端的链接,初始化Channel参数
(2)将链路状态变动时间通知给ChannelPipeline
worker线程池做用:
(1)异步读取通讯对端的数据报,发送读事件到ChannelPipeline
(2)异步发送消息到通讯对端,调用ChannelPipeline的消息发送接口
(3)执行系统调用Task
(4)执行定时任务Task
经过配置boss和worker线程池的线程个数以及是否共享线程池等方式,netty的线程模型能够在单线程、多线程、主从线程之间切换。
为了提高性能,netty在不少地方都进行了无锁设计
。好比在IO线程内部进行串行操做,避免多线程竞争形成的性能问题。表面上彷佛串行化设计彷佛CPU利用率不高,可是经过调整NIO线程池的线程参数,能够同时启动多个串行化的线程并行运行,这种局部无锁串行线程设计性能更优。
nettyd的NioEventLoop读取到消息以后,直接调用ChannelPipeline的fireChannelRead(Object msg),只要用户不主动切换线程,一直都是由NioEventLoop调用用户的Handler,期间不进行线程切换,这种串行化设计避免了多线程操做致使的锁竞争,性能角度看是最优的。
Netty 采用了典型的三层网络架构进行设计和开发,其逻辑架构图以下所示。
它由一系列辅助类完成,包括 Reactor 线程 NioEventLoop 及其父类,NioSocketChannel / NioServerSocketChannel 及其父类,Buffer 组件,Unsafe 组件 等。该层的主要职责就是监听网络的读写和链接操做,负责将网络层的数据读取到内存缓冲区,而后触发各类网络事件,例如链接建立、链接激活、读事件、写事件等,将这些事件触发到 PipeLine 中,由 PipeLine 管理的责任链来进行后续的处理。
它负责上述的各类网络事件在责任链中的有序传播,同时负责动态地编排责任链。责任链能够选择监听和处理本身关心的事件,它能够拦截处理事件,以及向前向后传播事件。不一样应用的 Handler 节点 的功能也不一样,一般状况下,每每会开发编解码 Hanlder 用于消息的编解码,能够将外部的协议消息转换成 内部的 POJO 对象,这样上层业务则只须要关心处理业务逻辑便可,不须要感知底层的协议差别和线程模型差别,实现了架构层面的分层隔离。
业务逻辑编排层一般有两类:一类是纯粹的业务逻辑编排,还有一类是其余的应用层协议插件,用于特定协议相关的会话和链路管理。例如,CMPP 协议,用于管理和中国移动短信系统的对接。
架构的不一样层面,须要关心和处理的对象都不一样,一般状况下,对于业务开发者,只须要关心责任链的拦截和业务 Handler 的编排。由于应用层协议栈每每是开发一次,处处运行,因此实际上对于业务开发者来讲,只须要关心服务层的业务逻辑开发便可。各类应用协议以插件的形式提供,只有协议开发人员须要关注协议插件,对于其余业务开发人员来讲,只需关心业务逻辑定制。这种分层的架构设计理念实现了 NIO 框架 各层之间的解耦,便于上层业务协议栈的开发和业务逻辑的定制。
正是因为 Netty 的分层架构设计很是合理,基于 Netty 的各类应用服务器和协议栈开发才可以如雨后春笋般获得快速发展。
《Netty权威指南》
(22 封私信 / 21 条消息) 怎样理解阻塞非阻塞与同步异步的区别? - 知乎
Linux 下的五种 IO 模型详细介绍_Linux_脚本之家
Linux IO模式及 select、poll、epoll详解 - 人云思云 - SegmentFault 思否
(2条消息)从bio到nio到netty实现原理浅析_嘎嘎的博客-CSDN博客_netty nio
深刻了解Netty【一】BIO、NIO、AIO简单介绍 - clawhub - 博客园