Netty
问题,程序TestByteBuf中程序一直显示执行,如何关闭ByteBuf的缓冲池呢?
Netty中为什么使用group.shutdownGracefully()程序还在运行
1,NIO基础
- Selector
-
selector的作用就是配合一个线程来管理多个channel,获取这些channel上发生的事件,这些channel工作在非阻塞模式下,不会让线程吊死在一个channel上,适合连接数特别多,但流量低的场景。
-
调用selector的select方法会阻塞直到channel发生了读写就绪事件,这些事件发生,select方法就会返回这些事件交给thread来处理
-
Buffer
-
ByteBuffer.allocate(16).getClass(): java.nio.HeapByteBuffer -java堆内存,读写效率较低,受到GC的影响
-
ByteBuffer.allocateDirect(16).getClass(): java.nio.HeapByteBuffer -直接内存,读写效率高(少一次拷贝),不会受到GC的影响,分配内存的效率低一些
-
以太网的payload,指除去协议首部之外实际传输的数据
FileChannel 只能工作在阻塞模式下
- channel.position(),获取当前位置
- channel.transferTo() //效率高,底层会利用操作系统的零拷贝进行优化,最大可传2g
String FROM = "helloword/data.txt";
String TO = "helloword/to.txt";
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
from.transferTo(0, from.size(), to);
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("transferTo 用时:" + (end - start) / 1000_000.0);
Path
-
jdk7引入了Path和Paths类
- Path用来表示文件路径
- Paths是工具类,用来获取Path实例
-
Files
- Files.walkFileTree()
- Files.walk()
NIO实现非阻塞Socket通信
-
绑定的事件类型可以有
- connect - 客户端连接成功时触发
- accept - 服务器端成功接受连接时触发
- read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况
- write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况
-
select 方法, selector.select();
- 没有事件发生,线程阻塞,有事件,线程才会恢复运行
- select 在事件未处理时,它不会阻塞, 事件发生后要么处理,要么取消,不能置之不理
处理消息的边界
-
-
一种思路是固定消息长度,数据包大小一样,服务器按预定长度读取,缺点是浪费带宽
-
另一种思路是按分隔符拆分,缺点是效率低
-
TLV 格式,即 Type 类型、Length 长度、Value 数据,类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer,缺点是 buffer 需要提前分配,如果内容过大,则影响 server 吞吐量
- Http 1.1 是 TLV 格式
- Http 2.0 是 LTV 格式
-
attachment,可以将一个Buffer绑定到一个channel上
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(16); // attachment
// 将一个 byteBuffer 作为附件关联到 selectionKey 上
SelectionKey scKey = sc.register(selector, 0, buffer);
- ByteBuffer 大小分配
- 每个 channel 都需要记录可能被切分的消息,因为 ByteBuffer 不能被多个 channel 共同使用,因此需要为每个 channel 维护一个独立的 ByteBuffer
- ByteBuffer 不能太大,比如一个 ByteBuffer 1Mb 的话,要支持百万连接就要 1Tb 内存,因此需要设计大小可变的 ByteBuffer
- 一种思路是首先分配一个较小的 buffer,例如 4k,如果发现数据不够,再分配 8k 的 buffer,将 4k buffer 内容拷贝至 8k buffer,优点是消息连续容易处理,缺点是数据拷贝耗费性能,参考实现 http://tutorials.jenkov.com/java-performance/resizable-array.html
- 另一种思路是用多个数组组成 buffer,一个数组不够,把多出来的内容写入新的数组,与前面的区别是消息存储不连续解析复杂,优点是避免了拷贝引起的性能损耗
多路复用
- 单线程可以配合Selector完成对多个Channel可读写事件的监控,这称之为多路复用
- 多路复用仅针对网络IO,普通文件IO没法利用多路复用
水平触发,边缘触发
- 在linux的IO多路复用有水平触发,边缘触发两种模式,这两种模式的区别如下:
- 水平触发,如果文件的描述符已经就绪可以非阻塞的执行IO操作了,此时会触发通知,允许在任意时刻重复检测IO的状态,没有必要每次描述符就绪后 尽可能多的执行IO。select,poll就属于水平触发
- 边缘触发,如果文件的描述符自上次状态改变后有新的IO活动到来,此时会触发通知,在收到一个IO事件通知后要尽可能多的执行IO操作,因为如果在一次通知中没有执行完IO那么就需要等到下一次新的IO活动到来才能获取到就绪的描述符。
利用多线程优化 NIO
stream vs channel
- stream不会自动缓冲数据,channel会利用系统提供的发送缓冲区,接收缓冲区(更为底层)
- stream仅支持阻塞API,channel同时支持阻塞,非阻塞API,网络channel可配合selector实现多路复用
- 二者均为全双工,即读写可以同时进行
IO模型
-
当调用一次channel.read或stream.read后,会切换至操作系统内核态来完成真正数据读取,而读取又分为两个阶段,分别为:
- 等待数据阶段
- 复制数据阶段
-
阻塞IO
-
非阻塞IO
-
多路复用
-
同步:线程自己去获取结果
-
异步:线程自己不去获取结果,而是由其他线程送结果(至少有两个线程)
-
零拷贝
- 传统IO问题
- 传统的IO将一个文件通过socket写出
File f = new File("helloword/data.txt");
RandomAccessFile file = new RandomAccessFile(file, "r");
byte[] buf = new byte[(int)f.length()];
file.read(buf);
Socket socket = ...;
socket.getOutputStream().write(buf);
内部工作流程:
- java本身并不具备IO读写能力,因此read方法调用后,要从java程序的用户态切换至内核态,去调用操作系统(Kernel)的读能力。将数据读入内核缓冲区。这期间用户线程阻塞,操作系统使用DMA(Direct Memory Access)来实现文件读,期间也不会使用cpu
DMA也可以理解为硬件单元,用来解放cpu完成文件IO - 从内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即 byte[] buf),这期间 cpu 会参与拷贝,无法利用 DMA
- 调用 write 方法,这时将数据从用户缓冲区(byte[] buf)写入 socket 缓冲区,cpu 会参与拷贝
- 接下来要向网卡写数据,这项能力 java 又不具备,因此又得从用户态切换至内核态,调用操作系统的写能力,使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu
可以看到中间环节较多,java的IO实际不是物理设备级别的读写,而是缓存的复制,底层的真正读写是操作系统完成的。
- 用户态与内核态的切换发生了3次,这个操作比较重量级
- 数据拷贝了共四次
NIO优化
-
通过DirectByteBuffer
- ByteBuffer.allocate(10) HeapByteBuffer 使用的还是 java 内存
- ByteBuffer.allocateDirect(10) DirectByteBuffer 使用的是操作系统内存
-
大部分步骤与优化前相同,不再赘述。唯有一点:java 可以使用 DirectByteBuf 将堆外内存映射到 jvm 内存中来直接访问使用
- 这块内存不受jvm垃圾回收的影响,因此内存地址固定,有助于IO读写
- java 中的 DirectByteBuf 对象仅维护了此内存的虚引用,内存回收分成两步
- DirectByteBuf 对象被垃圾回收,将虚引用加入引用队列
- 通过专门线程访问引用队列,根据虚引用释放堆外内存
- 减少了一次数据拷贝,用户态和内核态的切换次数没有减少
-
进一步优化(底层采用了 linux 2.1 后提供的 sendFile 方法),java 中对应着两个 channel 调用 transferTo/transferFrom 方法拷贝数据
-
- java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu
- 数据从内核缓冲区传输到 socket 缓冲区,cpu 会参与拷贝
- 最后使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu
可以看到
- 只发生了一次用户态与内核态的切换
- 数据拷贝了 3 次
进一步优化(linux 2.4)
- java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu
- 只会将一些offset和length信息拷入socket缓冲区,几乎无消耗
- 使用DMA将内核缓冲区的数据写入网卡,不会使用cpu
整个过程仅只发生了一次用户态与内核态的切换,数据拷贝了 2 次。所谓的【零拷贝】,并不是真正无拷贝,而是在不会拷贝重复数据到 jvm 内存中,零拷贝的优点有:
- 更少的用户态与内核态的切换
- 不利用cpu计算,减少cpu缓存伪共享
- 零拷贝适合小文件传输
AIO
-
AIO用来解决数据复制阶段的阻塞问题
- 同步意味着,在进行读写操作时,线程需要等待结果,还是相当于闲置
- 异步意味着,在进行读写操作时,线程不必等待结果,而是将来由操作系统来通过回调方式由另外的线程来获得结果
-
异步模型需要底层操作系统提供支持
- Windows系统通过IOCP实现了真正的异步IO
- Linux系统异步IO在2.6版本引入,但其底层实现还是用多路复用模拟了异步IO,性能没有优势
-
默认文件AIO使用的都是守护线程,在JAVA中有两类线程:User Thread(用户线程),Daemon Thread(守护线程),用个比较通俗的比喻,任何一个守护线程都是整个jvm中所有非守护线程的保姆;只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。
2,Netty入门
2.1 入门程序
-
Netty是一个异步的,基于事件驱动的网络应用框架,用于快速开发可维护,高性能的网络服务器和客户端。
-
Netty vs NIO,工作量大,bug大
- 需要 自己构建协议
- 解决TCP传输问题,如粘包,半包
- epoll空轮询导致100%
- 对 API 进行增强,使之更易用,如 FastThreadLocal => ThreadLocal,ByteBuf => ByteBuffer
-
入门 程序
- 服务端
// 1. 启动器,负责组装 netty 组件,启动服务器
new ServerBootstrap()
// 2. BossEventLoop, WorkerEventLoop(selector,thread), group 组 (一个selector+一个thread就叫一个EventLoop)
.group(new NioEventLoopGroup())
// 3. 选择 服务器的 ServerSocketChannel 实现
.channel(NioServerSocketChannel.class) //Netty支持好几种事件,还有OIO,其实就是BIO
// 4. boss 负责处理连接 worker(child) 负责处理读写,决定了 worker(child) 能执行哪些操作(handler)
.childHandler(
// 5. channel 代表和客户端进行数据读写的通道, Initializer 初始化,负责添加别的 handler
new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// 6. 添加具体 handler
ch.pipeline().addLast(new LoggingHandler());
ch.pipeline().addLast(new StringDecoder()); // 将 ByteBuf 转换为字符串
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { // 自定义 handler
@Override // 读事件
public voi