概念
- 基于TCP、异步、事件驱动的网络框架;
- TCP/IP --> 原生JDK(Java的IO和网络编程) --> NIO --> Netty;
应用场景
- Netty是异步高性能的通信框架,往往被RPC框架使用;
- 阿里云分布式框架Dubbo;
- 游戏;
- 地图框架
- AVRO(RPC),使用Netty Service进行二次封装;
- Akka;
- Flink;
- Spark;
书籍
- 『Netty in Action』实战
- 『Netty权威指南』,知识点多,但其基于Netty5,目前已经过时不在维护;
I/O模型
- I/O模型决定性能
- Java BIO,同步阻塞,造成线程浪费或连接浪费;
- Java NIO,同步非阻塞,一个线程处理多个连接(维护一个Selector),并把连接注册到一个选择器中,多路复用模型,事件驱动;后期连接多了就不止一个线程而是多个线程以及多个Selector(BOSS);
- Java AIO(NIO2.0),引入异步通道,采用了Proactor模式,目前还没有广泛应用,JDK1.7引入,有效的请求才启动线程,特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用;
场景辨析
BIO
连接数较少,且架构较固定,对服务器资源要求较高,并发局限于应用,JDK 1.4以前的唯一选择;
NIO
连接数目较多连接较短的操作,比如聊天服务器、弹幕系统、服务器之间的通讯,JDK 1.4之后支持;
AIO
连接数目较多且连接较长(重操作)的架构,比如相册服务器,调用OS参与并发操作,JDK 7之后支持;
『Netty基于NIO』
BIO详解
传统的BIO可以通过线程池进行改善,实现多个客户连接服务器。
流程
- 启动ServerSocket
- 建立一个线程与每个连接进行通讯
- 客户发出请求后咨询服务器线程是否有响应,没有会等待或者判断被拒绝
- 如果有响应,客户端会等待请求结束后继续执行(阻塞)
NIO详解
同步非阻塞,JDK1.4之后,Channel、Buffer、Selector三个核心组件。面向缓冲区、或者面向块编程的,增加了处理的灵活性。
组件说明
Channel
类似于Socket,数据通道;
Selector
寻找不同的Channel的注册事见以及查看是否由Channel就绪;
Buffer
缓冲区
三大组件的关系
- 每个Channel对应一个Buffer
- 每个Selector对应一个线程,一个线程对应多个Channel(连接)
- 程序切换到哪个Channel由事件决定,Event是一个重要的概念
- Selector会根据不同的事件在不同的通道切换
- Buffer是一个内存块,底层是一个数组
- 数据的读写时是通过Buffer,与BIO不同的是,BIO的读写有分别对应的流,而NIO的Buffer是双向的,但需要通过flip进行切换
- Channel是双向的,可以返回底层操作系统的状况,比如Linux,底层的操作系统通道是双向的
Buffer详解
- Capacity,容量,创建后不可变
- Limit,当前终点,可变
- Position,位置,下一个要被读或者写的元素索引,起点
- Mark,Mark()调用后记录上一次读操作的位置,reset()后Postition就会恢复到Mark的位置,默认为-1
- clear(),标记恢复,但内容还在
- flip(),反转操作,读/写
- hasRemaining(),类似hasNext(),告知当前位置和限制之间是否有元素
- isReadOnly(),判断是否为只读缓冲区
ByteBuffer
- allocateDirect(int capacity)//创建直接缓冲区
- allocate(int capacity)//设置缓冲区的初始容量
- get( );//从当前位置position上get,get之后,position会自动+1
- get (int index);//从绝对位置get
- put (byte b);//从当前位置上添加,put之后,position会自动+1
- put (int index, byte b);//从绝对位置上put
Channel详解
基本介绍
- 双工,一个通道既可读,也可写;(stream则是单工的)
- 可实现异步读写数据;
- 可以从缓冲区读数据,也可以写数据;
- 是NIO中的一个接口;
- 常见的Channel类:FileChanne、DatagramChannel、ServerSocketChannel 和 SocketChannel;(ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket)
- ServerSocketChannelImpl:ServerSocketChannel 的实现类
- SocketChannelImpl:SocketChannel 的实现类
- FileChanneImpl:FileChanne的实现类
Buffer和Channel使用细节
- put和get的类型要对应,否则会抛BufferUnderflowException异常,比如调换了get的顺序(类比序列化的过程!);
- 可以把普通的Buffer转成只读Buffer;
- MappedByteBuffer,可以让文件直接在内存(堆外内存,指虚拟机外的内存)中修改,而不需要额外拷贝一次;(RandomAccessFile是java提供的对文件类容进行访问的类。即可读也可写)
- NIO支持通过多个Buffer完成读写操作(Scattering和Gathering)
Selector详解
- selector能够检测注册的通道是否有事件的发生(单线程管理多个通道、一个Selector对应多个Channel)
- Selector是一个抽象类,open() 得到一个选择器对象,select(long) 监控所有注册的通道,当其中有IO操作可以进行时,将对应的 SelectionKey 加入到内部集合中并返回
- select()阻塞,select(long) 有超时时间
- selectKeys()是返回内部集合得所有Key,而select()是监听当中有事件发生所返回得事件selectkeys
流程
- 当客户端连接时,会通过ServerSocketChannel 得到 SocketChannel
- Selector 进行监听 select 方法, 返回有事件发生的通道的个数.
- 将socketChannel注册到Selector上, register(Selector sel, int ops), 一个selector上可以注册多个SocketChannel
- 注册后返回一个 SelectionKey, 会和该Selector 关联(集合)
- 进一步得到各个 SelectionKey (有事件发生)
- 在通过 SelectionKey 反向获取 SocketChannel , 方法 channel()
- 可以通过 得到的 channel , 完成业务处理
理解:selector是一个公共的轮询器,selectionKey是轮询中的每个item,他内部能映射到对应的channel,注册则是seletor.add(selectionkey),selector.bind(selectorkey);事件发生则是通过seletor获取selectionkey,然后再获取channel
SelectionKeys解释
channel和selector的注册关系
常用方法
零拷贝
- 网络编程优化得关键
- 常见得零拷贝:mmap(内存映射)、sendFile
传统IO
Ps: DMA拷贝(direct memory access):直接内存拷贝,不使用CPU。
- 把硬件的数据做一次DMA拷贝,数据进入内核kernel中
- 内核做一次CPU拷贝,数据进入用户buffer中
- 用户态下做一次CPU拷贝,数据进入socket buffer中
- 之后socket buffer做一次DMA拷贝,数据进入协议栈protocol engine中
传统IO经历了四次拷贝,三次用户态、内核态之间的切换
mmap优化
mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数。
- 示意图:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nMOSDM5q-1606353783856)(C:\Users\hasee\AppData\Roaming\Typora\typora-user-images\image-20200811171809699.png)]
三次拷贝,三次用户态、内核态之间的切换
sendFile
Linux 2.1 版本 提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换
- 示意图:
三次拷贝,两次用户态、内核态之间的切换
PS:所谓零拷贝指没有CPU拷贝,是从操作系统角度看的
Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝,实现真正的零拷贝,但socket buffer还存在的原因是 kernel buffer的部分数据还是需要存到socket buffer中,但可以忽略不计。
- 示意图:
两次拷贝,两次用户态、内核态切换
再次梳理
- 我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据);
- 零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算;
mmap和sendFile的区别
- mmap 适合小数据量读写,sendFile 适合大文件传输;
- mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝;(Ps: 这里的上下文切换,开始的也算一次)
- sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区);
transferTo在win中每次只能传递8m,所以要做分段处理
AIO详解
- JDK 7 引入了 Asynchronous I/O,即 AIO。在进行 I/O 编程中,常用到两种模式:Reactor和 Proactor。Java 的 NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理;
- AIO 即 NIO2.0,叫做异步不阻塞的 IO。AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用;
- 目前 AIO 还没有广泛应用,Netty 也是基于NIO, 而不是AIO;
- 参考:<<Java新一代网络编程模型AIO原理及Linux系统AIO介绍>> http://www.52im.net/thread-306-1-1.html
总结
BIO、NIO、AIO对比:
BIO | NIO | AIO | |
---|---|---|---|
IO模型 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
编程难度 | 简单 | 复杂 | 复杂 |
可靠性 | 差 | 好 | 好 |
吞吐量 | 低 | 高 | 高 |