RPC框架复习
1.Netty
1.IO
阻塞IO:在应用调用recvfrom读取数据时,其系统调用直到数据包到达且被复制到应用缓冲区中或者发送错误时才返回,在此期间一直会等待,进程从调用到返回这段时间内都是被阻塞的,称为阻塞IO。
1.2 非阻塞IO
非阻塞IO是在应用调用recvfrom读取数据时,如果该缓冲区没有数据的话,就会直接返回一个EWOULDBLOCK错误,不会让应用一直等待中。在没有数据的时候会即刻返回错误标识,那也意味着如果应用要读取数据就需要不断的调用recvfrom请求,直到读取到它数据要的数据为止。
1.3 IO复用模型
如果在并发的环境下,可能会N个人向应用B发送消息,这种情况下我们的应用就必须创建多个线程去读取数据,每个线程都会自己调用recvfrom 去读取数据。那么此时情况可能如下图:
可以有一个或者多个线程监控多个网络请求(fd文件描述符,linux系统把所有网络请求以一个fd来标识),这样就可以只需要一个或几个线程就可以完成数据状态询问的操作,当有数据准备就绪之后再分配对应的线程去读取数据,这么做就可以节省出大量的线程资源出来,这个就是IO复用模型的思路
进程通过将一个或多个fd传递给select,阻塞在select操作上,select帮我们侦测多个fd是否准备就绪,当有fd准备就绪时,select返回数据可读状态,应用程序再调用recvfrom读取数据
1.4 信号驱动IO模型
提出原因:select是采用轮询的方式来监控多个fd,这种无脑的轮询显得没有必要,极大的浪费性能,大部分的轮询都是无效的。
首先开启套接口信号驱动IO功能,并通过系统调用sigaction执行一个信号处理函数,此时请求即刻返回,当数据准备就绪时,就生成对应进程的SIGIO信号,通过信号回调通知应用线程调用recvfrom来读取数据。
1.5 异步IO
提出原因:不管是IO复用还是信号驱动,我们要读取一个数据总是要发起两阶段的请求,第一次发送select请求,询问数据状态是否准备好,第二次发送recevform请求读取数据。
应用只需要向内核发送一个read 请求,告诉内核它要读取数据后即刻返回;内核收到请求后会建立一个信号联系,当数据准备就绪,内核会主动把数据从内核复制到用户空间,等所有操作都完成之后,内核会发起一个通知告诉应用
2.BIO,NIO,AIO
2.1BIO
每当有一个连接到来,经过协调器的处理,就开启一个对应的线程进行接管。如果连接有1000条,那就需要1000个线程。线程资源是非常昂贵的,除了占用大量的内存,还会占用非常多的CPU调度时间,所以BIO在连接非常多的情况下,效率会变得非常低。
2.2NIO
2.2.1 Select
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。
调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。
当select函数返回后,需要通过遍历fdset,来找到就绪的描述符
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。
select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。
2.2.2 Poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。
struct pollfd { int fd; /* file descriptor */ short events; /* requested events to watch */ short revents; /* returned events witnessed */ };
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。
和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket
。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
2.2.3 Epoll
epoll最初在2.5.44内核版本出现,后续在2.6.x版本中对代码进行了优化使其更加简洁,先后面对外界的质疑在后续增加了一些设置来解决隐藏的问题,所以epoll也已经有十几年的历史了。
epoll对select中存在的问题都逐一解决,简单来说epoll的优势包括:
-
对fd数量没有限制(当然这个在poll也被解决了)
-
抛弃了bitmap数组实现了新的结构来存储多种事件类型
-
无需重复拷贝fd 随用随加 随弃随删
-
采用事件驱动避免轮询查看可读写事件
epoll出现之后大大提高了并发量,对于C10K问题轻松应对,即使后续出现了真正的异步IO,也并没有(暂时没有)撼动epoll的江湖地位,主要是因为epoll可以解决数万数十万的并发量,已经可以解决现在大部分的场景了,异步IO固然优异,但是编程难度比epoll更大,权衡之下epoll仍然富有生命力。
2.3 AIO
Java AIO,全程 Asynchronous IO,是异步非阻塞的IO。是一种非阻塞异步的通信模式。
IO是Java1.7加入的,理论上性能是会提升的,但它现在发展的不太好。那部分对数据进行自动读取的操作,总得有地方实现,不在框架里,就得在内核里。
2.Netty
1.Netty架构
在Netty中,Boss线程对应着对连接的处理和分派,相当于mainReactor;Work线程 对应着subReactor,使用多线程负责读写事件的分发和处理
这种模式的基本工作流程为:
-
Reactor 主线程 MainReactor 对象通过 select 监听客户端连接事件,收到事件后,通过 Acceptor 处理客户端连接事件。
-
当 Acceptor 处理完客户端连接事件之后(与客户端建立好 Socket 连接),MainReactor 将连接分配给 SubReactor。(即:MainReactor 只负责监听客户端连接请求,和客户端建立连接之后将连接交由 SubReactor 监听后面的 IO 事件。)
-
SubReactor 将连接加入到自己的连接队列进行监听,并创建 Handler 对各种事件进行处理。
-
当连接上有新事件发生的时候,SubReactor 就会调用对应的 Handler 处理。
-
Handler 通过 read 从连接上读取请求数据,将请求数据分发给 Worker 线程池进行业务处理。
-
Worker 线程池会分配独立线程来完成真正的业务处理,并将处理结果返回给 Handler。Handler 通过 send 向客户端发送响应数据。
-
一个 MainReactor 可以对应多个 SubReactor,即一个 MainReactor 线程可以对应多个 SubReactor 线程。
1. Reactor 线程模型(核心架构)
-
BossGroup(主线程池) 负责接收客户端连接(Accept 事件),将新连接注册到
WorkerGroup
的某个EventLoop
上。 -
WorkerGroup(工作线程池) 处理连接的 I/O 读写(Read/Write 事件),每个
EventLoop
绑定一个线程,通过轮询机制管理多个 Channel。
2. Channel(通道)
-
代表一个网络连接(如 Socket),所有 I/O 操作通过 Channel 进行。
-
支持多种协议(TCP/UDP/HTTP 等),通过
ChannelPipeline
处理数据流。
3. EventLoop(事件循环)
-
每个
EventLoop
绑定一个独立线程,负责监听和处理其管理的 Channel 事件。 -
采用单线程多路复用模型,避免线程切换开销,保证事件处理的顺序性。
4. ChannelPipeline(责任链)
-
数据处理流水线,由一系列
ChannelHandler
组成(如编解码、业务逻辑)。 -
入站(Inbound):处理输入数据(如读取请求),顺序为
Head → Handler1 → Handler2 → Tail
。 -
出站(Outbound):处理输出数据(如发送响应),顺序为
Tail → Handler2 → Handler1 → Head