Netty 学习之路(一)
前言
好几年没写博客了,全是自己本地做笔记。最近项目中遇到在线即时通讯、事件通知等一系列需求,遂开始学习十几年前但现在依然很火的经典开源框架 – Netty,并记录下来分享分享。
I/O 模型
学习 Netty 之前,必须要理解就是这个 I/O 模型了;废话不多说,开整
BIO(Blocking IO)
以流的方式处理数据,基于字节流和字符流
Java BIO 是传统的的 Java IO 编程,类和接口在 java.io 中
- 服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,但如果这个连接不做任何事,会造成不必要的线程开销(可以使用线程池机制改善,实现多客户连接,但不能减少线程的个数)
- 适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择
- 连接过后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费
编程流程
- 服务端启动一个 ServerSocket
- 客户端启动 Socket 连接对服务器通信,默认情况下服务器端对每个客户建立一个线程与之通讯
- 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待或者被拒绝
- 如果有响应,客户端线程会等待请求结束后,才继续执行
NIO (Non Blocking IO)
以块的方式处理数据,基于 Channel 和 Buffer
JDK1.4 之后输入/输出的新特性,是同步非阻塞的,类在 java.nio 下
- NIO 是面向缓冲区、或者面向块编程,可以做到用一个线程处理多个操作(一个线程监听多个客户端通道)。而不需要类似100个请求就开辟100个线程
- 一个线程从某个通道请求或者读取数据,但它仅能得到目前可用的数据;如果没有数据可用,就什么都不会做,而不是保持线程阻塞
- HTTP2.0 使用了多路复用,做到同一个连接并发处理多个请求,是 HTTP1.0 的好几个数量级
- NIO 包括三大核心组件
名称 | 描述 |
---|---|
Selector选择器 | 一个 Selector(类比注册中心) 对应一个线程,管理着多个 Channel;Selector 切换到哪个 Channel 是由事件决定的,Selector 会根据不同的事件在 Channel 上切换 |
Channel通道 | 一个 Channel 都对应一个 Buffer;Channel 是双向的,可以反应底层操作系统的情况(例如 Linux 底层的操作系统是双向的) |
Buffer缓冲区 | Buffer 是一个内存块,底层是一个数组;数据的读写都是通过 Buffer,和 BIO(输入流/输出流) 本质上不同(BIO 不是双向) |
Selector 选择器
能够检测多个注册的通道上是否有事件发生(多个 channel 以事件的方式可以注册到同一个 Selector)
当客户端连接时
- 通过 ServerSocketChannel 得到 Socketchannel
- 将 SocketChannel 注册到 Selector 上,使用 register(Selector sel, int ops) 方法;一个 Selector 上可以注册多个 SocketChannel
- 每一次注册后返回 selectionKey, 被 Selector 作成集合的形式管理起来
- Selector 此时可以进行监听, 使用方法 Select 方法,并返回有事件发生的通道个数,进而得到各个 SelectionKey
事件名称 | 事件行为 |
---|---|
OP_CONNECT | 客户端连接事件 |
OP_ACCEPT | 接受客户端注册事件 |
OP_READ | 客户端读事件 |
OP_WRITE | 客户端写事件 |
- 再通过 SelectionKey 可以反向获取 SocketChannel,通过得到的 channel 完成业务读写处理
Channel 连接通道
NIO 中的接口
通道可以同时进行读写数据、实现异步读写数据、从缓冲区读写数据
常用类名 | 作用 |
---|---|
FileChannel | 用于文件数据读写 |
DatagramChannel | 用于 UDP 数据读写 |
ServerSocketChannel | 用于 TCP 数据读写 |
SocketChannel | 用于 TCP 数据读写 |
Buffer 容器对象
名称 | 类型 | 描述 |
---|---|---|
mark | int | 标记 |
position | int | 位置,下一个要被读写的元素的索引,每次读写缓冲区数据时都会改变值,为下一次读写做准备 |
limit | int | 表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作,且极限是可以修改的 |
capacity | int | 容量,即可以容纳的最大数据量,在缓冲区创建时被设定并且不能改变 |
hb | [] | 数据承载结构 |
注意事项
- put 时的类型,get 时需要按照对应的类型获取
- MappedByteBuffer 让文件直接在内存中修改(堆外内存)
- Scattering,将数据写入到 Buffer 时,可以采用 Buffer 数组,依次写入(分散)
- Gathering,从 Buffer 读取数据时,可以采用 Buffer 数组,依次读(聚合)
零拷贝
注意:不是不拷贝,而是没有 CPU 的拷贝
在 Java 中常用的两种拷贝方式:mmap(内存映射)、sendFile
- 传统 I/O 的文件拷贝方式(4次拷贝3次切换)
- 首先从硬件 (Hard Drive)DMA Copy(direct memory access 直接内存拷贝),即不使用 CPU
- 拷贝至内核(kernal buffer)
- 再将内核中的数据使用 CPU Copy 至 user buffer,此时数据可以修改
- 再使用 CPU Copy 至 Socket buffer
- 最后使用 DMA Copy 至 protocol engine(协议引擎/栈)
- mmap 优化(3次拷贝3次切换)
适合数据、文件较小
通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据;这样在网络传输时,就可以减少内核空间到用户空间的拷贝次数
- 首先从硬件 (Hard Drive)DMA Copy(direct memory access 直接内存拷贝),即不使用 CPU
- 由于共享了内核空间,因此此时可以在 kernal buffer 中进行数据修改
- 再通过 CPU Copy 至 Socket Buffer
- 最后使用 DMA Copy 至 protocol engine(协议引擎/栈)
- sendFile(3次拷贝2次切换)
Linux 2.1 版本提供的 sendFile 函数,适合大文件
数据根本不经过用户态,直接从内核缓冲区进去 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换
其优势在于
- 更少的数据 复制
- 更少的上下文切换
- 更少的 CPU 缓存伪共享
- 无 CPU 校验和计算
- 首先从硬件 (Hard Drive)DMA Copy(direct memory access 直接内存拷贝),即不使用 CPU
- 此时,减少了user context 的切换
- 直接从 kernal buffer 通过 CPU Copy 至 Socket Buffer
- 再使用 DMA Copy 至 protocol engine(协议引擎/栈)
- 但是,在 Linux 2.4 时做了修改,实现了零拷贝;减少了从 kernal buffer 经过 CPU 至 Socket Buffer 的过程,直接从 kernal buffer 通过 DMA Copy 至 protocol engine,从而减少了一次数据拷贝(kernal buffer 的 length、offset 还是会 copy 至 socket buffer,但是信息量很少,消耗很低,可以忽略)
AIO(Asynchronous I/O)
JDK7 引入了 Asynchronous I/O。涉及到两种模式 Reactor 和 Proactor;而 Java 的 NIO 就是 Reactor
AIO 也被称呼为 NIO2.0,异步非阻塞 IO。引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,特点是先由操作系统完成后才通知服务端开启线程去处理,适用于连接数较多、连接时间较长的应用场景