NIO学习总结

1、核心组件

1.1、Channel(通道)

​ 通道类型与BIO的流,但是通道可以同时进行读写,可以实现异步读写数据,从缓冲区读取数据,也可以写入数据到缓冲区。常见的Channel有:FileChannel(文件相关),DatagramChannel(UDP数据读写),ServerSocketChannel和SocketChannel(TCP数据读写);

  • ServerSocketChannel:在服务器端监听新的客户端Socket连接;
// 获取SocketChannel对象
public static SocketChannel open();
// 设置通道为非堵塞 false为非堵塞
public final SelectableChannel configureBlocking(boolean block);
// 注册到selector中并选择一个监听事件
public final SelectionKey register(Selector sel, int ops);
// 接受一个连接,并返回这个连接对应的通道对象
public abstract SocketChannel accept()
  • SocketChannel:网络IO通道,具体负责读写操作。NIO把缓冲区的数据写入通道,或者将通道的数据读到缓冲区。
// 获取SocketChannel对象
public static SocketChannel open();
// 设置通道为非堵塞 false为非堵塞
public final SelectableChannel configureBlocking(boolean block);
// 连接服务器
public abstract boolean connect(SocketAddress remote);
// 如果上述连接操作失败,需要用下面的方法完成连接操作
public abstract boolean finishConnect();
// 读取数据
public abstract int read(ByteBuffer dst);
// 写入数据
public final long write(ByteBuffer[] srcs);
// 注册到selector中并选择一个监听事件 最后一个参数可以设置共享数据
public final SelectionKey register(Selector sel, int ops, Object att);

1.2、Buffer(缓冲区)

​ 缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松的使用内存块,缓冲区对象内置了一些机制(position,limit,mark),能够跟踪和记录缓冲区的状态变化情况。Channel提供了从文件、网络读取数据的渠道,但是读取和写入的数据必须经由Buffer。

 // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
clear() // 清除此缓冲区,将各个标识重置到初始化位置,但是数据并没有清除。
flip()// 反转此缓冲区。一般读写切换时使用

1.3、Selector(选择器)

  • Java的NIO,用非阻塞的IO方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector。
  • Selector能够检测到多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相对应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
  • 只有在连接真正的有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不比为每一个连接都创建一个线程,不用去维护多个线程。
  • 避免了多线程之间的上下文切换导致的开销。
// 获取一个Selector对象
public static Selector open();
// 监控所有注册的通道,当其中有IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间
public abstract int select(long timeout);
// 从内部集合中获取到所有的SelectionKey(有事件发生的)
public abstract Set<SelectionKey> selectedKeys();
// 不堵塞 立刻返还
public abstract int selectNow();
// 所有注册到selector中的SelectionKey的集合
public abstract Set<SelectionKey> keys();

在这里插入图片描述
说明:

  1. 当客户端连接时,会通过ServerSocketChannel获取到SocketChannel;
  2. 讲SocketChannel注册到Selector上,register(Selector sel, int ops,Object att),Selctor可以注册多个SocketChannel;
  3. 注册后会返回一个SelectionKey,会和该Selector关联;
  4. Selector进行监听select方法,返回有事件发生的通道的个数;
  5. 进一步得到各个SelectionKey(有事件发生);
  6. 再通过SelectionKey反向获取SocketChannel,方法Channel;
  7. 可以通过得到的channel,完成业务处理。

SelectorKey:表示Selector和网络通道的注册关系,共四种

  • int OP_READ = 1 << 0。值为1,代表读操作;

  • int OP_WRITE = 1 << 2。值为4,代表写操作;

  • int OP_CONNECT = 1 << 3。值为8,代表连接已建立;

  • int OP_ACCEPT = 1 << 4。值为16,代表有新的网络连接可以accept。

  • NIO提供了MappedByteBuffer可以直接在内存(堆外内存)中修改(ps:如果使用不当的话,可能导致jvm oom 具体表现 dump日志非常小)。

	    RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\nio.txt", "rw");
	    FileChannel fileChannel = randomAccessFile.getChannel();
	   	/**
         * 参数1:FileChannel.MapMode.READ_WRITE 使用的读写模式
         * 参数2:0:可以直接修改的其实位置
         * 参数3:5:是映射到内存中的大小
         * 可以修改的位置 是 文件的 0 - 5的位置 (包头不包尾)
         */
        MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5);

        map.put(2,(byte)'H');
        randomAccessFile.close();
  • NIO还支持通过多个Buffer(Buffer数组)完成读写操作,即Scattering(分散)和Gatering(聚集)。

2、NIO和零拷贝

2.1、零拷贝简介

  1. 零拷贝是网络编程的关键,很多性能优化都离不开;
  2. 在Java程序中,常用的零拷贝有mmap(内存映射)和sendFile。

传统IO示意图:
在这里插入图片描述
传统IO需要经过4次拷贝,3次状态切换。
第一次:从硬盘经过DMA(direct memory access 直接内存拷贝)拷贝到 kernel buffer(内核buffer);
第二次:从 kernel buffer拷贝至 user buffer;
第三次:从 user buffer 拷贝至 socket buffer;
第四次:从 socket buffer 拷贝至 协议栈。

第一次切换:用户态 -> 内核态
第二次切换:内核态 -> 用户态
第三次切换:用户态 -> 内核态

2.2、mmap优化

  1. mmap通过内存映射,将文件映射到内核缓冲区,同时用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。
  2. 图解在这里插入图片描述
  3. 第一次拷贝: DMA拷贝,从硬件拷贝到内核空间(
    因为user buffer 与kernel buffer共享数据 ,所以不需要将数据从kernel buffer 拷贝到 user buffer , 数据可以直接在内核空间修改
    第二次拷贝: kernel buffer 中的数据经过 cpu 拷贝到 socket buffer
    第三次拷贝: socket buffer 过DMA拷贝到protocol engine

2.3、sendFile优化

  1. Linux2.1之后,提供了sendFile函数,基本原理:数据根本不需要经过用户态,直接从内核缓冲区进入到 socket buffer。同时,由于和用户态完全无关,就减少了一次上下文切换;
  2. Linux2.4之后,提供的sendFile函数做了一些修改,避免了从内核缓冲区拷贝到Socket Buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝,真正意义实现了零拷贝(sendFile技术还是有少量的数据(例如数据的大小,偏移量等)使用了cpu拷贝,从kernel buffer 拷贝到 socket buffer ,但是数据量很少,可忽略 );
  3. 图解在这里插入图片描述

小结

我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据);

零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU 缓存伪共享以及无CPU校验和计算。

mmap和sendFile的区别:

  • mmap适合小数据量的读写,sendFile 适合大文件传输。
  • mmap需要4次上下文切换,3次数据拷贝;sendFile 需要3次上下文切换,最少两次数据拷贝。
  • sendFile 可以利用DMD 方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区);

NIO虽好,但是同样存在一些弊端

  1. NIO 的类库和API 繁杂,使用麻烦:需要熟练掌握Selector,ServerSocketChannel、SocketChannel、ByteBuffer等;
  2. 需要具备其他的额外技能:要熟悉Java多线程编程,因为NIO 编程设计到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序;
  3. 开发工作量和难度非常大:例如客户端面临断连重连、网络闪退、半包读写、失败缓存、网络堵塞和异常流的处理等等;
  4. JDK NIO的Bug:例如臭名昭著的Epoll Bug,它会导致Selector空轮询,最终导致CPU 100%。直至JDK1.7该问题仍存在,没有被根本解决。

Netty很好的解决了NIO的问题,下一篇博客介绍Netty框架

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值