本篇内容主要整理了 Netty 框架底层原理、细节实现、实际操作等相关的内容
优点
Netty 是一个基于 NIO 的客户端/服务器编程框架,提供异步的、事件驱动的编程框架。所有 IO 操作都是异步非阻塞的,通过Future-Listener
机制,用户可以方便地主动获取或者通过通知机制获得 IO 操作结果。
与 JDK 原生 NIO 相比,Netty 提供了相对十分简单易用的 API,既支持异步服务也支持阻塞 IO 的服务,优点总结:
- 封装了简单易用的 API 降低了开发门槛
- 支持多种编解码功能,支持多种主流协议
- 定制能力强,可以通过
ChannelHandler
对通信框架进行灵活扩展 - 性能高,社区成熟迭代快
高并发 IO 基础原理
整理系统调用基础原理。
Linux 底层相关
Linux 中一切都看作是文件,包括普通文件,目录文件,字符设备文件(如键盘,鼠标…),块设备文件(如硬盘,光驱…),套接字等等。文件描述符,当应用程序请求内核打开/新建一个文件时,内核会返回一个文件描述符 File descriptor 用于对应这个打开/新建的文件,fd 本质上就是一个非负整数,是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。
在 Linux 系统中通过 select/epoll 系统调用实现一个进程监视多个 fd ,当某个 fd 就绪(一般是内核缓冲区可读/可写)内核就把就绪状态返回给应用程序,应用程序根据就绪的状态进行相应的 IO 系统调用。
目前支持IO多路复用的系统调用有 select、epoll 等。
select 系统调用几乎在所有的操作系统上都有支持,具有良好的跨平台特性。
epoll 是在 Linux 2.6 内核中提出的,是 select 系统调用的 Linux 增强版本。
IO 调用原理
用户程序进行 IO 的读写实际是依赖于底层的 read & write 两大系统调用,其中 read 系统调用并不是直接把数据从物理设备读取到内存中,write 系统调用也不是直接把数据写入到物理设备,两种操作都会涉及缓冲区。用户程序在其进程缓冲区内操作数据,通过 read & write 系统调用与操作系统内核缓冲区交互。操作系统内核来完成数据在内核缓冲区和物理设备(如磁盘)之间的交换。
主要的 IO 模型
整理四种主要的 IO 模型
同步阻塞 IO(Blocking IO)
- 用户程序发起 read 调用后线程进入阻塞挂起状态
- 内核收到请求后等待数据(从磁盘、网卡)全部进入内核缓冲区后,将数据复制到用户缓冲区,然后返回结果
- 用户程序恢复运行,解析数据
优点:开发简单,阻塞期间不占用 CPU 资源;缺点:每个连接都需占用独立的线程浪费资源,线程上下文切换开销很大。不适合高并发场景。
同步非阻塞 IO(Non-blocking IO)
Linux 的 socket 连接默认是同步阻塞,也可以指定为同步非阻塞。
- 用户程序发起同步非阻塞的 socket read 调用后线程进入阻塞挂起状态
- 情况1:内核缓冲区数据没有准备好,立即返回调用失败;此时用户程序应尝试重新发起调用
- 情况2:内核缓冲区数据已处理好,将数据复制到用户缓冲区,然后返回结果
- 用户程序恢复运行,解析数据
应用程序需要发起轮询 IO 系统调用,直到完成 IO 系统调用为止。
优点:每次发起的 IO 系统调用在内核等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。 同步非阻塞IO的缺点:不断地轮询内核,这将占用大量的 CPU 时间,导致效率低下。不适合高并发场景。
IO 多路复用(IO Multiplexing)
IO 多路复用通过 select/epoll 系统调用实现单个应用程序的线程不断地轮询成百上千的 socket 连接,当某个或者某些socket网络连接有 IO 就绪的状态,就返回对应的可以执行的读写操作。发起一个 IO 多路复用的 read 系统调用流程如下:
- 将需要 read 操作的 socket fd 注册到 select/epoll 选择器中,如
java.nio.channels.Selector
- 调用选择器的查询方法,内核会返回一个就绪的 fd 列表。
- 用户线程获得已就绪状态的 fd 发起 read 系统调用,用户线程阻塞,内核将数据从内核缓冲区复制到用户缓冲区
- 复制完成后内核返回结果,用户线程解除阻塞
和 NIO 模型相似,多路复用 IO 是通过 select/epoll 轮询查找已就绪的 fd。注册在选择器上的 socket 连接一般都设置成为同步非阻塞模型
优点:使用 select/epoll 时一个选择器查询线程可以同时处理成千上万个连接(Connection),系统不必创建大量的线程。Java 的 NIO 就是使用的 IO 多路复用模型。
缺点:本质上 select/epoll 系统调用是阻塞式的,属于同步IO,都需要在读写事件就绪后由系统调用本身负责进行读写,也就是说这个读写过程是阻塞的。
如何彻底地解除线程的阻塞,就必须使用异步 IO 模型。
异步IO(Asynchronous IO)
用户线程通过系统调用向内核注册某个 IO 操作,内核在整个 IO 操作(包括数据准备、数据复制)完成后,通知用户程序执行后续的业务操作。
在整个内核的数据处理过程中,包括内核将数据从网络物理设备(网卡)读取到内核缓冲区、将内核缓冲区的数据复制到用户缓冲区,用户程序都不需要阻塞。
- 用户线程发起了异步 IO 类型的 read 系统调用,立刻就可以开始去做其他的事,用户线程不阻塞
- 内核开始准备数据,等数据准备完成后把数据从内核缓冲区复制到用户缓冲区(用户空间的内存)
- 内核会给用户线程发送一个信号(Signal),或者回调用户线程注册的回调接口,通知用户线程 read 操作完成了
- 用户线程读取用户缓冲区的数据,完成后续的业务操作
特点:在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。用户线程需要接收内核的IO操作完成的事件,或者用户线程需要注册一个IO操作完成的回调函数。正因为如此,异步IO有的时候也被称为信号驱动IO。
缺点:应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统内核,对内核实现要求较高。
优点:理论上来说异步 IO 是真正的异步输入输出,它的吞吐量高于 IO 多路复用模型的吞吐量。
目前 Windows 通过 IOCP 实现了真正的异步 IO,而在 Linux 系统下,异步 IO 模型在 2.6 版本才引入,目前并不完善,在性能上没有明显的优势。Linux 系统大多数的高并发服务器端的程序还是采用 IO 多路复用模型。
JDK NIO
三大组件
- 通道 Channel
- 缓冲区 Buffer
- 选择器 Selector
Reactor 反应器模式
应用实例:Nginx,Redis,Netty
反应器模式由Reactor反应器线程、Handlers处理器两大角色组成:
-
Reactor反应器线程的职责:负责响应IO事件,并且分发到Handlers处理器。
-
Handlers处理器的职责:非阻塞的执行业务处理逻辑。
…