Netty学习笔记-入门版

Netty学习笔记

前言

本文简单介绍下netty的基本原理,I/O模型,Reactor线程模型以及架构设计等相关知识点。

什么是Netty

Netty是由JBOSS提供的一个Java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。

Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的socket服务开发。

Netty是由NIO演进而来,使用过NIO编程的用户就知道NIO编程非常繁重,Netty是能够能跟好的使用NIO

IO基础

概念说明

IO简单介绍

IO在计算机中指Input/Output,也就是输入和输出。由于程序和运行时数据是在内存中驻留,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。

比如你打开浏览器,访问新浪首页,浏览器这个程序就需要通过网络IO获取新浪的网页。浏览器首先会发送数据给新浪服务器,告诉它我想要首页的HTML,这个动作是往外发数据,叫Output,随后新浪服务器把网页发过来,这个动作是从外面接收数据,叫Input。所以,通常,程序完成IO操作会有Input和Output两个数据流。当然也有只用一个的情况,比如,从磁盘读取文件到内存,就只有Input操作,反过来,把数据写到磁盘文件里,就只是一个Output操作。

用户空间与内核空间

现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备(例如负责磁盘IO的设备)的所有权限为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。

针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0×00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

进程(Process)

是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础

在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。

线程(thread)

是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

程序和进程

1个程序可以对应多个进程,但1个进程只能对应1个程序。

说白了就是,一个程序可以重复运行,开几个窗口,比如网游的“双开”,一个进程可以对应多个程序就是一个DLL文件可一被多个程序运用,比如DirectX9的动态链接库,就是,许多游戏都要有它才能运行。

我们简单总结下:

进程包含线程。

进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。

线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位。

进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。

从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:

  1. 保存处理机上下文,包括程序计数器和其他寄存器。
  2. 更新PCB信息。
  3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
  4. 选择另一个进程执行,并更新其PCB。
  5. 更新内存管理的数据结构。
  6. 恢复处理机上下文。

注:总而言之就是很耗资源

进程阻塞

正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的

文件描述符

简单理解:一个指向文件本身的指针(由系统所管理的引用标识,该标识可以被系统重新定位到一个内存地址上,间接访问对象 ),值是非负整数。

文件描述符(File descriptor,简称fd)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

文件句柄

Windows下的概念。句柄是Windows下各种对象的标识符,比如文件、资源、菜单、光标等等。文件句柄和文件描述符类似,它也是一个非负整数,也用于定位文件数据在内存中的位置。

缓存IO

大多数文件系统的默认IO都是缓存IO。过程是:数据先被拷贝到操作系统的内核缓冲区(页缓存 page cache)中,然后再拷贝到应用程序的地址空间。

Linux 网络I/O模型

同步、异步、阻塞、非阻塞的概念

同步

所谓同步,发起一个功能调用的时候,在没有得到结果之前,该调用不返回,也就是必须一件事一件事的做,等前一件做完了,才能做下一件。

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返回结果,可以先去干别的事。

举个例子

老张爱喝茶,废话不说,煮开水。 出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。

  1. 老张把水壶放到火上,立等水开。(同步阻塞) 老张觉得自己有点傻
  2. 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞) 老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
  3. 老张把响水壶放到火上,立等水开。(异步阻塞) 老张觉得这样傻等意义不大
  4. 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞) 老张觉得自己聪明了。

所谓同步异步,只是对于水壶而言。

普通水壶,同步;响水壶,异步。虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。

所谓阻塞非阻塞,仅仅对于老张而言。立等的老张,阻塞;看电视的老张,非阻塞。情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。

I/O模型

recvfrom函数用于从(已连接)套接口上接收数据,并捕获数据发送源的地址。
本函数用于从(已连接)套接口上接收数据,并捕获数据发送源的地址。
(简单理解就是客户端等待服务端给数据的函数)

举个例子,其中的角色,客人(小明)对应内核线程,服务员对应的是用户线程。现在大黄在南亭新开了一家黄焖鸡,小明(客人)觉得很新鲜,准备喊上几个基友去南亭搓一顿黄焖鸡。

小明到店里了,如果小明成功点餐则需要经过两个步骤,第一步是思考要点什么吃的,第二步是跟服务员说要吃什么。

该模型的内核线程分为两个阶段

  • 数据准备阶段:(等待点餐)未阻塞,当数据准备完成之后,会主动的通知用户进程数据已经准备完成,对用户进程做一个回调。
  • 数据拷贝阶段:(进行点餐)阻塞用户进程,等待数据拷贝。
阻塞 I/O(blocking IO)

现在黄焖鸡的老板大黄给每个客人都配一个服务员,只要有一个客人来的话,就在旁边等客人思考吃什么并且进行点餐。只要客人还没有点餐完毕,对应的这个服务员就不能离开去做别的事情。

映射到Linux操作系统中,这就是阻塞的IO模型。在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:

用户线程(服务员) 内核线程(客人)

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的两个阶段:

  1. 准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。
  2. 当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

所以,blocking IO的特点就是在IO执行的两个阶段(等待IO(准备IO)和执行IO)都被block了。

非阻塞 I/O(nonblocking IO)

现在随着有些大学生月初拿到零花钱,开始浪了,黄焖鸡的生意也变得越来越火爆了,来吃饭的客人越来越多,要配的服务员也越来越多,黄焖鸡的老板大黄心想这不对劲啊,要是突然同时来100个客人,就要有100个服务员,肯定巨亏啊,得想个法子提高效率。这时候老板大黄想到,在客人想点什么吃的时候,服务员完全可以去做别的事情,例如去给别的桌的客人点餐,只要偶尔过来问下客人是否要点餐了,一旦发现客人需要点餐了,就开始点餐。

映射到Linux操作系统中,这就是非阻塞的IO模型。linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:

  1. 当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。
  2. 一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。

所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。

信号驱动I/O( signal driven IO )

过了一段时间,客人们好多都反应,你们服务员太烦人了,整天问我是不是可以点餐了,客人说我们干脆要点餐的时候就叫服务员过来好了,老板大黄心想这样也不错,能提高餐厅的运行效率,赚多点钱,便答应了(这时候的服务员还在客人旁边傻乎乎的站着,只等着客人喊他点餐)。

映射到Linux操作系统中,这就是信号驱动I/O。当数据报准备好的时候,内核会向应用程序发送一个信号,进程对信号进行捕捉,并且调用信号处理函数来获取数据报。

img

I/O 多路复用( IO multiplexing)

老板大黄巡查店内情况,看到了服务员大多都傻乎乎的站着等客人喊他点餐,作为资本家的大黄,当然是要充分利用劳动力的。所以老板给服务员们开了个会,安排他们一个人负责一个区域(多个客人)的客人点餐需求,等到客人喊他点餐时,就过去点餐。

映射到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。

异步 I/O(asynchronous IO)

后来,随着黄焖鸡的味道大家都比较喜爱,口碑逐渐建立了起来,生意越发地火爆,常常座无虚席,大黄也开始梦想着多久能实现一个小目标。大学城里有些创业团队看到了黄焖鸡的火爆程度,看到服务员有很多时候是在给客人点餐,他们结合自己的专业知识,跟老板说给老板开发个手机点餐系统,客人通过手机就能点餐,服务员只需要查看系统的点餐情况进行上菜就好,老板大黄一看,秒啊,急忙答应了,通过点餐系统,餐厅运行效率更高了,接待的客人也越来越多,离一个小目标的梦想也越来越近了。

映射到Linux操作系统中,这就是异步 I/O。Linux下的asynchronous IO其实用得很少。先看一下它的流程:

用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

用户进程只需要知道内核线程处理的结果

5中I/O模型对比

IO多路复用的三种机制

select

数组(长度1024)存储所有的fd

说的通俗一点就是各个客户端连接的文件描述符也就是套接字,都被放到了一个集合中,调用select函数之后会一直监视这些文件描述符中有哪些可读,如果有可读的描述符那么我们的工作进程就去读取资源。

select 在一个进程内可以维持最多 1024 个连接

poll

链表存储所有的fd

poll 和 select 的实现非常类似,本质上的区别就是存放 fd 集合的数据结构不一样。poll 在select基础上做了加强,可以维持任意数量的连接

但 select 和 poll 方式有一个很大的问题就是,我们不难看出来 select和poll 是通过轮循的方式来查找是否可读或者可写,打个比方,如果同时有100万个连接都没有断开,而只有一个客户端发送了数据,所以这里它还是需要循环这么多次,造成资源浪费。

epoll

链表存储ready的fd

不需要遍历全部fd去找ready的,其全部的fd放在一个红黑树中加以维护。

epoll 是 select 和 poll 的增强版,epoll 同 poll 一样,文件描述符数量无限制

epoll是基于内核的反射机制,在有活跃的 socket 时,系统会调用我们提前设置的回调函数。而 poll 和 select 都是遍历。

但是也并不是所有情况下 epoll 都比 select/poll 好,比如在如下场景:

在大多数客户端都很活跃的情况下,系统会把所有的回调函数都唤醒,所以会导致负载较高。既然要处理这么多的连接,那倒不如 select 遍历简单有效。

NIO入门

引言

在Java中提供了三种IO模型:BIO、NIO、AIO,模型的选择决定了程序通信的性能。

使用场景

  • BIO
    BIO适用于连接数比较小的应用,这种IO模型对服务器资源要求比较高。

  • NIO
    NIO适用于连接数目多、连接时间短的应用,比如聊天、弹幕、服务器间通讯等应用。

  • AIO
    AIO适用于连接数目多、连接时间长的应用,比如相册服务器。

BIO

同步阻塞式

无脑创建线程

伪异步I/O阻塞式

改用线程池

NIO

同步非阻塞模型,服务器端用一个线程处理多个连接,客户端发送的连接请求会注册到多路复用器上,多路复用器轮询到连接有IO请求就进行处理:

NIO的非阻塞模式,使得一个线程从某通道发送请求或者读取数据时,如果目前没有可用的数据,不会使线程阻塞,在数据可读之前,该线程可以做其他的事情。

NIO有三大核心部分:

  • Channel(通道)
  • Buffer(缓冲区)
  • Selector(选择器)


由图可知:

  • 每个Channel对应一个Buffer。
  • Selector对应一个线程,一个线程对应多个Channel。
  • Selector会根据不同的事件,在各个通道上切换。
  • Buffer是内存块,底层是数据。

缓冲区(Buffer)

本质是可以读写数据的内存块,Channel读取或者写入的数据必须通过Buffer:

java.nio.Buffer抽象类的属性:

复制代码

// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;

Buffer-属性.png

读写交换要使用flip方法。

通道(Channel)

通道是双向的,可以读操作、也可以写操作。
java.nio.channels.Channel接口的常用实现类:

FileChannel用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel和SocketChannel用于TCP的数据读写。

选择器(Selector)

Selector选择器使用一个线程来维护。多个Channel会以事件的方式注册到同一个Selector,当有事件发生时,Selector会获取事件,然后针对每个事件进行响应的处理。这样就不必为每个连接创建一个线程,不用维护多线程,也不会有多线程之间的上下文切换导致的系统的开销。
Selector示意图:

AIO

异步非阻塞模型,AIO引入异步通道的概念,使用了Proactor,只有有效的请求才启动线程,特点是先由操作系统完成后,才通知服务器端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。

对比

Reactor线程模型

Reactor线程模型是基于同步非阻塞IO实现的。对于异步非阻塞IO的实现是Proactor模型。

Netty就是基于Reactor线程模型开发的,我们今天来简单分析下:

Reactor模型中的三种角色及含义:
    Reactor:将I/O事件分配给对应的handler。
    Acceptor:处理客户端新连接,并分派请求到处理器链中。
    Handlers:执行非阻塞读写任务。

Reactor常用的线程模型有三种

Reactor单线程模型

单线程模型简图

单线程模型就是指所有的I/O操作都是在一个线程中处理完成,NIO的线程需要接受客户端的Tcp连接,并且向客户端发送Tcp连接,读取通信两端的请求或应答,发送请求和应答。

单线程模型详细图解

大致了解了后,让我们看下这个详细流程,当客户端发起连接,Acceptor负责接收客户端的Tcp请求,链路建立成功后,通过Dispatcher将对应的ByteBuffer派发到指定的Hnadler上进行消息解码,用户Handler通过NIO线程将消息发送给客户端。单线程模型其实就是Acceptor的处理和Handler的处理都处在同一个线程中,当其中的一个Hnadler阻塞时,会导致其它的client和handler无法执行,甚至整个服务不能接受新的请求。

单线程模型缺点:不适用于高负载,高并发的场景。

因为一个NIO线程如果同时处理很多的链路,则机器在性能上无法满足海量的消息的编码,解码,读取和发送。如果NIO线程负载过重,处理速度变慢,会导致大量的客户端请求超时,甚至导致整个通信模块不可用。

Reactor多线程模型

为了解决单线程模型的缺点,设计出了多线程模型。如下简图:

多线程模型简图

如图所示在多线程模型下,用一个专门的NIO线程Acceptor来监听客户端的Tcp请求,对于网络I/O的读写操作和消息的读取、编码、解码、发送等使用NIO线程池来完成。因为客户端请求数量大于NIO线程池中的线程,一个NIO线程可以同时处理多条链路请求,但是一个链路请求只对应一个NIO线程。Reactor多线程模型能够大多数的使用场景,但是当客户端的并发连接非常的多,或者是服务端需要对客户端进行安全认证等,单个Acceptor线程可能会存在性能不足的问题。

主从Reactor多线程模型

Reactor的主从多线程模型

如图所示,从这个简图可以看出,服务端用于监听和接收客户端连接的不再是单个线程,而是分配了一个线程池。Acceptor线程池接收了客户端的请求连接并处理完成后(可能包含了权限认证等),后续的I/O操作再由NIO线程池来完成。这样就解决了多线程中客户端请求太多或者需要认证时一个Acceptor可能处理不过来的性能问题。

netty的线程模型

netty的线程模型是可以通过设置启动类的参数来配置的,设置不同的启动参数,netty支持Reactor单线程模型、多线程模型和主从Reactor多线程模型

server端工作原理

img

NettyServer整体架构图.png

server端启动时绑定本地某个端口,将自己NioServerSocketChannel注册到某个boss NioEventLoop的selector上。
server端包含1个boss NioEventLoopGroup和1个worker NioEventLoopGroup,NioEventLoopGroup相当于1个事件循环组,这个组里包含多个事件循环NioEventLoop,每个NioEventLoop包含1个selector和1个事件循环线程。
每个boss NioEventLoop循环执行的任务包含3步:

  • 第1步:轮询accept事件;
  • 第2步:处理io任务,即accept事件,与client建立连接,生成NioSocketChannel,并将NioSocketChannel注册到某个worker NioEventLoop的selector上;
  • 第3步:处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用eventloop.execute或schedule执行的任务,或者其它线程提交到该eventloop的任务。

每个worker NioEventLoop循环执行的任务包含3步:

  • 第1步:轮询read、write事件;
  • 第2步:处理io任务,即read、write事件,在NioSocketChannel可读、可写事件发生时进行处理;
  • 第3步:处理任务队列中的任务,runAllTasks。

client端工作原理

img

NettyClient整体架构图.png

client端启动时connect到server,建立NioSocketChannel,并注册到某个NioEventLoop的selector上。
client端只包含1个NioEventLoopGroup,每个NioEventLoop循环执行的任务包含3步:

  • 第1步:轮询connect、read、write事件;
  • 第2步:处理io任务,即connect、read、write事件,在NioSocketChannel连接建立、可读、可写事件发生时进行处理;
  • 第3步:处理非io任务,runAllTasks。

服务端启动时创建了两个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 的三层架构设计

Netty 采用了典型的三层网络架构进行设计和开发,其逻辑架构图如下所示。

通信调度层 Reactor

它由一系列辅助类完成,包括 Reactor 线程 NioEventLoop 及其父类,NioSocketChannel / NioServerSocketChannel 及其父类,Buffer 组件,Unsafe 组件 等。该层的主要职责就是监听网络的读写和连接操作,负责将网络层的数据读取到内存缓冲区,然后触发各种网络事件,例如连接创建、连接激活、读事件、写事件等,将这些事件触发到 PipeLine 中,由 PipeLine 管理的责任链来进行后续的处理。

责任链层 Pipeline

它负责上述的各种网络事件在责任链中的有序传播,同时负责动态地编排责任链。责任链可以选择监听和处理自己关心的事件,它可以拦截处理事件,以及向前向后传播事件。不同应用的 Handler 节点 的功能也不同,通常情况下,往往会开发编解码 Hanlder 用于消息的编解码,可以将外部的协议消息转换成 内部的 POJO 对象,这样上层业务则只需要关心处理业务逻辑即可,不需要感知底层的协议差异和线程模型差异,实现了架构层面的分层隔离。

业务逻辑编排层 Service ChannelHandler

业务逻辑编排层通常有两类:一类是纯粹的业务逻辑编排,还有一类是其他的应用层协议插件,用于特定协议相关的会话和链路管理。例如,CMPP 协议,用于管理和中国移动短信系统的对接。

架构的不同层面,需要关心和处理的对象都不同,通常情况下,对于业务开发者,只需要关心责任链的拦截和业务 Handler 的编排。因为应用层协议栈往往是开发一次,到处运行,所以实际上对于业务开发者来说,只需要关心服务层的业务逻辑开发即可。各种应用协议以插件的形式提供,只有协议开发人员需要关注协议插件,对于其他业务开发人员来说,只需关心业务逻辑定制。这种分层的架构设计理念实现了 NIO 框架 各层之间的解耦,便于上层业务协议栈的开发和业务逻辑的定制。

正是由于 Netty 的分层架构设计非常合理,基于 Netty 的各种应用服务器和协议栈开发才能够如雨后春笋般得到快速发展。

参考资料

漫话:如何给女朋友解释什么是Linux的五种IO模型?

《Netty权威指南》

聊聊同步、异步、阻塞与非阻塞 - 简书

(22 封私信 / 21 条消息) 怎样理解阻塞非阻塞与同步异步的区别? - 知乎

linux的五种IO模型 - lovejune - 博客园

Linux 下的五种 IO 模型详细介绍_Linux_脚本之家

Linux IO模式及 select、poll、epoll详解 - 人云思云 - SegmentFault 思否

(2条消息)从bio到nio到netty实现原理浅析_嘎嘎的博客-CSDN博客_netty nio

深入了解Netty【一】BIO、NIO、AIO简单介绍 - clawhub - 博客园

Reactor 线程模型以及在netty中的应用 - balfish - 博客园

Netty框架之Reactor线程模型

一文看懂 Netty 架构设计

posted @ 2020-09-05 17:14  亥码  阅读(1102)  评论(1编辑  收藏  举报