Netty

Netty

问题,程序TestByteBuf中程序一直显示执行,如何关闭ByteBuf的缓冲池呢?
Netty中为什么使用group.shutdownGracefully()程序还在运行

文章目录

1,NIO基础

  • Selector
    • selector的作用就是配合一个线程来管理多个channel,获取这些channel上发生的事件,这些channel工作在非阻塞模式下,不会让线程吊死在一个channel上,适合连接数特别多,但流量低的场景。
      在这里插入图片描述

    • 调用selector的select方法会阻塞直到channel发生了读写就绪事件,这些事件发生,select方法就会返回这些事件交给thread来处理

Buffer
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
NIO实现非阻塞Socket通信
  • 绑定的事件类型可以有

    • connect - 客户端连接成功时触发
    • accept - 服务器端成功接受连接时触发
    • read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况
    • write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况
  • Java.NIO中的Selector和SelectionKey详解

  • 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);

内部工作流程:
在这里插入图片描述

  1. java本身并不具备IO读写能力,因此read方法调用后,要从java程序的用户态切换至内核态,去调用操作系统(Kernel)的读能力。将数据读入内核缓冲区。这期间用户线程阻塞,操作系统使用DMA(Direct Memory Access)来实现文件读,期间也不会使用cpu
    DMA也可以理解为硬件单元,用来解放cpu完成文件IO
  2. 内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即 byte[] buf),这期间 cpu 会参与拷贝,无法利用 DMA
  3. 调用 write 方法,这时将数据从用户缓冲区(byte[] buf)写入 socket 缓冲区,cpu 会参与拷贝
  4. 接下来要向网卡写数据,这项能力 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 方法拷贝数据

  • 在这里插入图片描述

  1. java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu
  2. 数据从内核缓冲区传输到 socket 缓冲区,cpu 会参与拷贝
  3. 最后使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu

可以看到

  • 只发生了一次用户态与内核态的切换
  • 数据拷贝了 3 次

进一步优化(linux 2.4)

在这里插入图片描述

  1. java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu
  2. 只会将一些offset和length信息拷入socket缓冲区,几乎无消耗
  3. 使用DMA将内核缓冲区的数据写入网卡,不会使用cpu

整个过程仅只发生了一次用户态与内核态的切换,数据拷贝了 2 次。所谓的【零拷贝】,并不是真正无拷贝,而是在不会拷贝重复数据到 jvm 内存中,零拷贝的优点有:

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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值