NIO:解开非壅闭I/O高并发编程的秘密

[复制链接]
发表于 5 小时前 | 显示全部楼层 |阅读模式
流与块

Standard IO是对字节省的读写,在举行IO之前,起首创建一个流对象,流对象举行读写操纵都是按字节 ,一个字节一个字节的来读或写。而NIO把IO抽象成块,类似磁盘的读写,每次IO操纵的单位都是一个块,块被读入内存之后就是一个byte[],NIO一次可以读或写多个字节。
I/O 与 NIO 最告急的区别是数据打包和传输的方式,I/O 以流的方式处理惩罚数据,而 NIO 以块的方式处理惩罚数据。
面向流的 I/O 一次处理惩罚一个字节数据: 一个输入流产生一个字节数据,一个输出流消耗一个字节数据。为流式数据创建过滤器非常轻易,链接几个过滤器,以便每个过滤器只负责复杂处理惩罚机制的一部门。倒霉的一面是,面向流的 I/O 通常相称慢。
面向块的 I/O 一次处理惩罚一个数据块,按块处理惩罚数据比按流处理惩罚数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简朴性。
I/O 包和 NIO 已经很好地集成了,java.io.* 已经以 NIO 为根本重新实现了,以是如今它可以利用 NIO 的一些特性。比方,java.io.* 包中的一些类包罗以块的情势读写数据的方法,这使得纵然在面向流的体系中,处理惩罚速率也会更快。
Java对IO多路复用的支持

NIO 经常被叫做非壅闭 IO,重要是由于 NIO 在网络通讯中的非壅闭特性被广泛利用。但着实应该叫new IO,是相较于传统IO来说的。

Java NIO 中的 Selector 类是基于操纵体系提供的 I/O 多路复用机制实现的,而在 Linux 上,这个机制就是 epoll。
关于触发模式

  • Java NIO 的 Selector 默认利用的是水平触发模式(Level-Triggered, LT)。这意味着当一个文件形貌符(在 Java 中通常是 SocketChannel 或 ServerSocketChannel)变得可读或可写时,Selector 会连续关照,直到该文件形貌符上的变乱被处理惩罚。这与 epoll 的水平触发模式是划一的。
  • 固然 epoll 也支持边沿触发模式(Edge-Triggered, ET),但 Java NIO 的 Selector 并没有直接提供对边沿触发模式的支持。如果须要利用边沿触发模式,通常须要直接利用底层的体系调用(如通过 JNI 调用 epoll 的边沿触发模式),但这超出了标准 Java NIO 库的范围。
关于水平触发和边沿触发的区别可以看这篇文章,总结一下:

  • Java NIO 在 Linux 上利用 epoll 作为底层的 I/O 多路复用机制。
  • Java NIO 的 Selector 默认利用 epoll 的水平触发模式。
  • Java NIO 不直接支持 epoll 的边沿触发模式,须要通过其他方式实现。
因此,如果在 Linux 上利用 Java NIO 的 Selector,它利用的是 epoll 的水平触发模式。
三大组件

通道

被创建的一个应用步伐和操纵体系交互变乱、通报内容的渠道(注意是毗连到操纵体系)。一个通道会有一个专属的文件状态形貌符。那么既然是和操纵体系举行内容的通报,那么阐明应用步伐可以通过通道读取数据,也可以通过通道向操纵体系写数据。
通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。通道与流的差异之处在于,流只能在一个方向上移动(一个流必须是 InputStream 大概 OutputStream 的子类),而通道是双向的,可以用于读、写大概同时用于读写。
JAVA NIO 框架中,自有的Channel通道包罗:

全部被Selector(选择器)注册的通道,只能是继续了SelectableChannel类的子类。如上图所示

  • FileChannel: 从文件中读写数据;
  • DatagramChannel: 通过 UDP 读写网络中数据;
  • SocketChannel: TCP Socket套接字的监听通道,一个Socket套接字对应了一个客户端IP: 端口 到 服务器IP: 端口的通讯毗连。
  • ServerSocketChannel: 应用服务器步伐的监听通道。只有通过这个通道,应用步伐才气向操纵体系注册支持“多路复用IO”的端口监听。同时支持UDP协媾和TCP协议。
FileChannel 是磁盘IO的通道,后三个是网络IO的通道。而且FileChannel不能切换为非壅闭模式,因此FileChannel不得当Selector。
缓冲区

数据缓存区: 在JAVA NIO 框架中,为了包管每个通道的数据读写速率JAVA NIO 框架为每一种须要支持数据读写的通道集成了Buffer的支持。用于读取或写入数据到通道。
这句话怎么明确呢? 比方ServerSocketChannel通道它只支持对OP_ACCEPT变乱的监听,以是它是不能直接举行网络数据内容的读写的。以是ServerSocketChannel是没有集成Buffer的。
Buffer有两种工作模式: 写模式和读模式。在读模式下,应用步伐只能从Buffer中读取数据,不能举行写操纵。但是在写模式下,应用步伐是可以举行读操纵的,这就表现大概会出现脏读的情况。以是一旦您决定要从Buffer中读取数据,肯定要将Buffer的状态改为读模式。
发送给一个通道的全部数据都必须起首放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道举行读写数据,而是要先颠末缓冲区。
缓冲区实质上是一个数组,但它不但仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪体系的读/写进程。
缓冲区包罗以下范例:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
ByteBuffer 准确利用姿势


  • 向 buffer 写入数据,比方调用 channel.read(buffer)
  • 调用 flip() 切换至读模式
  • 从 buffer 读取数据,比方调用 buffer.get()
  • 调用 clear() 或 compact() 切换至写模式
  • 重复 1~4 步调
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,一个数组不敷,把多出来的内容写入新的数组,与前面的区别是消息存储不连续剖析复杂,长处是克制了拷贝引起的性能消耗

缓冲区状态变量


  • capacity: 最大容量;
  • position: 当前已经读写的字节数;
  • limit: 还可以读写的字节数。
状态变量的改变过程举例:
① 新建一个巨细为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。

② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 移动设置为 5,limit 保持稳固。

③ 在将缓冲区的数据写到输出通道之前,须要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。
写到输出通道,意味着要从buffer中读出,才气写入channel
  1. public Buffer flip() {
  2.      limit = position;
  3.      position = 0;
  4.      mark = -1;
  5.      return this;
  6. }
复制代码

④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。

⑤ 末了须要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。

⑥ compact 方法,是把未读完的部门向前压缩,然后切换至写模式

文件 NIO 实例

以下展示了利用 NIO 快速复制文件的实例:
  1. public static void fastCopy(String src, String dist) throws IOException {
  2.     // 获得源文件的输入字节流
  3.     FileInputStream fin = new FileInputStream(src);
  4.     // 获取输入字节流的文件通道
  5.     FileChannel fcin = fin.getChannel();
  6.     // 获取目标文件的输出字节流
  7.     FileOutputStream fout = new FileOutputStream(dist);
  8.     // 获取输出字节流的通道
  9.     FileChannel fcout = fout.getChannel();
  10.     // 为缓冲区分配 1024 个字节
  11.     ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
  12.     while (true) {
  13.         // 从输入通道中读取数据到缓冲区中
  14.         int r = fcin.read(buffer);//对于buffer来说,这是写入的过程
  15.         // read() 返回 -1 表示 EOF
  16.         if (r == -1) {
  17.             break;
  18.         }
  19.         // 切换读写
  20.         buffer.flip();
  21.         // 把缓冲区的内容写入输出文件中
  22.         fcout.write(buffer);//对于buffer来说,这是读取的过程
  23.         
  24.         // 清空缓冲区
  25.         buffer.clear();
  26.     }
  27. }
复制代码
选择器

Selector (选择器,多路复用器)是JavaNIO 中可以大概检测一到多个NIO通道,是否为诸如读写变乱做好准备的组件。如许,一个单独的线程可以管理多个channel,从而管理多个网络毗连。
NIO 实现了 IO 多路复用中的 多Reactor多进程/线程 模子,一个线程 Thread 利用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的变乱,从而让一个线程就可以处理惩罚多个变乱。通过设置监听的通道 Channel 为非壅闭,那么当 Channel 上的 IO 变乱还未到达时,就不会进入壅闭状态不停等候,而是继续轮询别的 Channel,找到 IO 变乱已经到达的 Channel 实验。
由于创建和切换线程的开销很大,因此利用一个线程来处理惩罚多个变乱而不是一个线程处理惩罚一个变乱具有更好的性能

  • 变乱订阅和Channel管理:应用步伐将向Selector对象注册须要它关注的Channel,以及详细的某一个Channel会对哪些IO变乱感爱好。Selector中也会维护一个“已经注册的Channel”的容器。以下代码来自WindowsSelectorImpl实现类中,对已经注册的Channel的管理容器:
  1. // Initial capacity of the poll array
  2. private final int INIT_CAP = 8;
  3. // Maximum number of sockets for select().
  4. // Should be INIT_CAP times a power of 2
  5. private final static int MAX_SELECTABLE_FDS = 1024;
  6. // The list of SelectableChannels serviced by this Selector. Every mod
  7. // MAX_SELECTABLE_FDS entry is bogus, to align this array with the poll
  8. // array,  where the corresponding entry is occupied by the wakeupSocket
  9. private SelectionKeyImpl[] channelArray = new SelectionKeyImpl[INIT_CAP];
复制代码

  • 轮询署理:应用层不再通过壅闭模式大概非壅闭模式直接扣问操纵体系“变乱有没有发生”,而是由Selector代其扣问。
  • 实现差异操纵体系的支持:多路复用IO技能 是须要操纵体系举行支持的,其特点就是操纵体系可以同时扫描同一个端口上差异网络毗连的变乱。以是作为上层的JVM,必须要为 差异操纵体系的多路复用IO实现 编写差异的代码。同样测试情况是Windows,它对应的实现类是sun.nio.ch.WindowsSelectorImpl:

selector 的作用就是共同一个线程来管理多个 channel,获取这些 channel 上发生的变乱,这些 channel 工作在非壅闭模式下,不会让线程吊死在一个 channel 上。得当毗连数特别多,但流量低的场景(low traffic)

创建选择器
  1. Selector selector = Selector.open();
复制代码
绑定 Channel 变乱

也称之为注册变乱,绑定的变乱 selector 才会关心
  1. ServerSocketChannel ssChannel = ServerSocketChannel.open();
  2. ssChannel.configureBlocking(false);
  3. ssChannel.register(selector, SelectionKey.OP_ACCEPT);
复制代码
Channel必须设置为非壅闭模式,否则利用选择器就没有任何意义了,由于如果通道在某个变乱上被壅闭,那么服务器就不能相应别的变乱,必须等候这个变乱处理惩罚完毕才气行止理别的变乱,显然这和选择器的作用背道而驰。
在将通道注册到选择器上时,还须要指定要注册的详细变乱,重要有以下几类:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE
它们在 SelectionKey 的界说如下:
  1. public static final int OP_READ = 1 << 0;
  2. public static final int OP_WRITE = 1 << 2;
  3. public static final int OP_CONNECT = 1 << 3;
  4. public static final int OP_ACCEPT = 1 << 4;
复制代码
split 方法
  1. int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
复制代码
处理惩罚 write 变乱

一次无法写完的例子


  • 非壅闭模式下,无法包管把 buffer 中全部数据都写入 channel,因此须要追踪 write 方法的返回值(代表实际写入的字节数)
  • 用 selector 监听全部 channel 的可写变乱,每个 channel 都须要一个 key 来跟踪 buffer,但如许又会导致占用内存过多,就有两阶段计谋

    • 当消息处理惩罚器第一次写入消息时,才将 channel 注册到 selector 上
    • selector 查抄 channel 上的可写变乱,如果全部的数据写完了,就取消 channel 的注册
    • 如果不取消,会每次可写均会触发 write 变乱

  1. int count = selector.select();
复制代码
客户端
  1. int count = selector.select(long timeout);
复制代码
文件编程 FileChannel

FileChannel 只能工作在壅闭模式下,没有非壅闭模式
获取FileChannel 时,不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 大概 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法

  • 通过 FileInputStream 获取的 channel 只能读
  • 通过 FileOutputStream 获取的 channel 只能写
  • 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定
两个 Channel 传输数据
  1. int count = selector.selectNow();
复制代码
凌驾 2g 巨细的文件传输
  1. // 获取所有事件
  2. Set<SelectionKey> keys = selector.selectedKeys();
  3. // 遍历所有事件,逐一处理
  4. Iterator<SelectionKey> keyIterator = keys.iterator();
  5. while (keyIterator.hasNext()) {
  6.     SelectionKey key = keyIterator.next();
  7.     // 判断事件类型
  8.     if (key.isAcceptable()) {
  9.         ServerSocketChannel c = (ServerSocketChannel) key.channel();
  10.         // 必须处理
  11.         SocketChannel sc = c.accept();
  12.         sc.configureBlocking(false);
  13.         sc.register(selector, SelectionKey.OP_READ);
  14.         // ...
  15.     }
  16.     // 处理完毕,必须将事件移除
  17.     keyIterator.remove();
  18. }
复制代码
实际传输一个超大文件
  1. // 获取所有事件
  2. Set<SelectionKey> keys = selector.selectedKeys();
  3. // 遍历所有事件,逐一处理
  4. Iterator<SelectionKey> keyIterator = keys.iterator();
  5. while (keyIterator.hasNext()) {
  6.     SelectionKey key = keyIterator.next();
  7.     // 判断事件类型
  8.     if (key.isAcceptable()) {
  9.         ServerSocketChannel c = (ServerSocketChannel) key.channel();
  10.         // 必须处理
  11.         SocketChannel sc = c.accept();
  12.         sc.configureBlocking(false);
  13.         sc.register(selector, SelectionKey.OP_READ);
  14.         // ...
  15.     } else if (key.isReadable()) {
  16.         SocketChannel sc = (SocketChannel) key.channel();
  17.         //实际使用中,不会一次给buffer缓冲区分配太多空间,因此可能存在粘包的问题
  18.         ByteBuffer buffer = ByteBuffer.allocate(128);
  19.         int read = sc.read(buffer);
  20.         if(read == -1) {
  21.             key.cancel();
  22.             sc.close();
  23.         } else {
  24.             buffer.flip();
  25.         }
  26.     }
  27.     // 处理完毕,必须将事件移除
  28.     keyIterator.remove();
  29. }
复制代码
FileChannel.map()方法着实就是接纳了操纵体系中的内存映射方式,将内核缓冲区的内存和用户缓冲区的内存做了一个地点映射。它办理数据从磁盘读取到内核缓冲区,然后内核缓冲区的数据复制移动到用户空间缓冲区。步伐照旧须要从用户态切换到内核态,然后再举行操纵体系调用,而且数据移动和复制了两次。
transferTo方法则是利用了sendfile的方式,来分析一下此中原理:

  • transferTo()方法直接将当前通道内容传输到另一个通道,没有涉及到Buffer的任何操纵,NIO中的Buffer是JVM堆大概堆外内存,但岂论怎样他们都是操纵体系内核空间的内存。也就是说这种方式不会有内核缓冲区和用户缓冲区之间的拷贝题目。
  • transferTo()的实现方式就是通过体系调用sendfile()(固然这是Linux中的体系调用),根据我们上面所写说这个过程是服从远高于从内核缓冲区到用户缓冲区的读写的。
  • 同理transferFrom()也是这种实现方式。
详细细节可以看这篇文章 网络编程 - NIO的零拷贝实现
网络编程

JAVA NIO 框架扼要计划分析

多路复用IO技能是操纵体系的内核实现。在差异的操纵体系,以致同一系列操纵体系的版本中所实现的多路复用IO技能都是不一样的。那么作为跨平台的JAVA JVM来说怎样顺应多种多样的多路复用IO技能实现呢? 面向对象的威力就显现出来了: 无论利用哪种实现方式,他们都会有“选择器”、“通道”、“缓存”这几个操纵要素,那么可以为差异的多路复用IO技能创建一个同一的抽象组,而且为差异的操纵体系举行详细的实现。JAVA NIO中对各种多路复用IO的支持,重要的根本是java.nio.channels.spi.SelectorProvider抽象类,此中的几个重要抽象方法包罗:

  • public abstract DatagramChannel openDatagramChannel(): 创建和这个操纵体系匹配的UDP 通道实现。
  • public abstract AbstractSelector openSelector(): 创建和这个操纵体系匹配的NIO选择器,就像上文所述,差异的操纵体系,差异的版本所默认支持的NIO模子是不一样的。
  • public abstract ServerSocketChannel openServerSocketChannel(): 创建和这个NIO模子匹配的服务器端通道。
  • public abstract SocketChannel openSocketChannel(): 创建和这个NIO模子匹配的TCP Socket套接字通道(用来反映客户端的TCP毗连)
由于JAVA NIO框架的整个计划是很大的,以是我们只能还原一部门我们关心的题目。这里我们以JAVA NIO框架中对于差异多路复用IO技能的选择器 举行实例化创建的方式作为例子,以点窥豹观全局:

很显着,差异的SelectorProvider实现对应了差异的 选择器。由详细的SelectorProvider实现举行创建。别的阐明一下,实际上netty底层也是通过这个计划得到详细利用的NIO模子。以下代码是Netty 4.0中NioServerSocketChannel举行实例化时的核心代码片断:
  1. // 获取所有事件
  2. Set<SelectionKey> keys = selector.selectedKeys();
  3. // 遍历所有事件,逐一处理
  4. Iterator<SelectionKey> keyIterator = keys.iterator();
  5. while (keyIterator.hasNext()) {
  6.     SelectionKey key = keyIterator.next();
  7.     // 判断事件类型
  8.     if (key.isAcceptable()) {
  9.         ServerSocketChannel c = (ServerSocketChannel) key.channel();
  10.         // 必须处理
  11.         SocketChannel sc = c.accept();
  12.         sc.configureBlocking(false);
  13.         ByteBuffer buffer = ByteBuffer.allocate(16); // attachment
  14.         // 将一个 byteBuffer 作为附件关联到 selectionKey 上
  15.         SelectionKey scKey = sc.register(selector, 0, buffer);
  16.         scKey.register(selector, SelectionKey.OP_READ);
  17.     } else if (key.isReadable()) { // 如果是 read
  18.         try {
  19.             SocketChannel sc = (SocketChannel) key.channel();
  20.             // 获取 selectionKey 上关联的附件
  21.             ByteBuffer buffer = (ByteBuffer) key.attachment();
  22.             int read = sc.read(buffer);
  23.             if(read == -1) {
  24.                 key.cancel();
  25.             } else {
  26.                 split(buffer);
  27.                 // 需要扩容
  28.                 if (buffer.position() == buffer.limit()) {
  29.                     ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
  30.                     buffer.flip();
  31.                     newBuffer.put(buffer); // 0123456789abcdef3333\n
  32.                     key.attach(newBuffer);
  33.                 }
  34.             } catch (IOException e) {
  35.                 e.printStackTrace();
  36.                 key.cancel();  // 因为客户端断开了,因此需要将 key 取消(从 selector 的 keys 集合中真正删除 key)
  37.             }
  38.     }
  39.     // 处理完毕,必须将事件移除
  40.     keyIterator.remove();
  41. }
复制代码
JAVA实例 - 利用多线程优化

前面的代码只有一个选择器,没有充实利用多核 cpu。而如今都是多核 cpu,计划时要充实思量别让 cpu 的力气被白白浪费
分两组选择器

  • 单线程配一个选择器,专门处理惩罚 accept 变乱
  • 创建 cpu 核心数的线程,每个线程配一个选择器,轮替处理惩罚 read 变乱
  1. private static void split(ByteBuffer source) {
  2.     source.flip();
  3.     for (int i = 0; i < source.limit(); i++) {
  4.         // 找到一条完整消息
  5.         if (source.get(i) == '\n') {
  6.             int length = i + 1 - source.position();
  7.             // 把这条完整消息存入新的 ByteBuffer
  8.             ByteBuffer target = ByteBuffer.allocate(length);
  9.             // 从 source 读,向 target 写
  10.             for (int j = 0; j < length; j++) {
  11.                 target.put(source.get());
  12.             }
  13.             debugAll(target);
  14.         }
  15.     }
  16.     source.compact(); // 0123456789abcdef  position 16 limit 16
  17. }
复制代码
UDP


  • UDP 是无毗连的,client 发送数据不会管 server 是否开启
  • server 这边的 receive 方法会将吸取到的数据存入 byte buffer,但如果数据报文凌驾 buffer 巨细,多出来的数据会被冷静扬弃
起首启动服务器端
  1. public class WriteServer {
  2.     public static void main(String[] args) throws IOException {
  3.         ServerSocketChannel ssc = ServerSocketChannel.open();
  4.         ssc.configureBlocking(false);
  5.         ssc.bind(new InetSocketAddress(8080));
  6.         Selector selector = Selector.open();
  7.         ssc.register(selector, SelectionKey.OP_ACCEPT);
  8.         while(true) {
  9.             selector.select();
  10.             Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
  11.             while (iter.hasNext()) {
  12.                 SelectionKey key = iter.next();
  13.                 iter.remove();
  14.                 if (key.isAcceptable()) {
  15.                     SocketChannel sc = ssc.accept();
  16.                     sc.configureBlocking(false);
  17.                     SelectionKey sckey = sc.register(selector, SelectionKey.OP_READ);
  18.                     // 1. 向客户端发送内容
  19.                     StringBuilder sb = new StringBuilder();
  20.                     for (int i = 0; i < 3000000; i++) {
  21.                         sb.append("a");
  22.                     }
  23.                     ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
  24.                     int write = sc.write(buffer);
  25.                     // 3. write 表示实际写了多少字节
  26.                     System.out.println("实际写入字节:" + write);
  27.                     // 4. 如果有剩余未读字节,才需要关注写事件
  28.                     if (buffer.hasRemaining()) {
  29.                         // read 1  write 4
  30.                         // 在原有关注事件的基础上,多关注 写事件
  31.                         //key.interestOps() 表示原有关注的时间,+  SelectionKey.OP_WRITE 写事件
  32.                         sckey.interestOps(sckey.interestOps() + SelectionKey.OP_WRITE);
  33.                         // 把 buffer 作为附件加入 sckey
  34.                         sckey.attach(buffer);
  35.                     }
  36.                 } else if (key.isWritable()) {
  37.                     ByteBuffer buffer = (ByteBuffer) key.attachment();
  38.                     SocketChannel sc = (SocketChannel) key.channel();
  39.                     int write = sc.write(buffer);
  40.                     System.out.println("实际写入字节:" + write);
  41.                     if (!buffer.hasRemaining()) { // 写完了
  42.                         // 为什么要取消关注 写事件
  43.                         // 只要向 channel 发送数据时,socket 缓冲可写,这个事件会频繁触发,因此应当只在 socket 缓冲区写不下时再关注可写事件,数据写完之后应该取消关注
  44.                         key.interestOps(key.interestOps() - SelectionKey.OP_WRITE);
  45.                         key.attach(null);
  46.                     }
  47.                 }
  48.             }
  49.         }
  50.     }
  51. }
复制代码
运行客户端
  1. public class WriteClient {
  2.     public static void main(String[] args) throws IOException {
  3.         Selector selector = Selector.open();
  4.         SocketChannel sc = SocketChannel.open();
  5.         sc.configureBlocking(false);
  6.         sc.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
  7.         sc.connect(new InetSocketAddress("localhost", 8080));
  8.         int count = 0;
  9.         while (true) {
  10.             selector.select();
  11.             Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
  12.             while (iter.hasNext()) {
  13.                 SelectionKey key = iter.next();
  14.                 iter.remove();
  15.                 if (key.isConnectable()) {
  16.                     System.out.println(sc.finishConnect());
  17.                 } else if (key.isReadable()) {
  18.                     ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
  19.                     count += sc.read(buffer);
  20.                     buffer.clear();
  21.                     System.out.println(count);
  22.                 }
  23.             }
  24.         }
  25.     }
  26. }
复制代码
多路复用IO的优缺点


  • 不消再利用多线程来举行IO处理惩罚了(包罗操纵体系内核IO管理模块和应用步伐进程而言)。固然实际业务的处理惩罚中,应用步伐进程照旧可以引入线程池技能的
  • 同一个端口可以处理惩罚多种协议,比方,利用ServerSocketChannel测测的服务器端口监听,既可以处理惩罚TCP协议又可以处理惩罚UDP协议。
  • 操纵体系级别的优化: 多路复用IO技能可以是操纵体系级别在一个端口上可以大概同时担当多个客户端的IO变乱。同时具有之前我们讲到的壅闭式同步IO和非壅闭式同步IO的全部特点。Selector的一部门作用更相称于“轮询署理器”。
  • 都是同步IO: 现在先容的 壅闭式IO、非壅闭式IO以致包罗多路复用IO,这些都是基于操纵体系级别对“同步IO”的实现。我们不停在说“同步IO”,不停都没有详细说,什么叫做“同步IO”。实际上一句话就可以说清晰: 只有上层(包罗上层的某种署理机制)体系扣问我是否有某个变乱发生了,否则我不会主动告诉上层体系变乱发生了
存在的误区

最初在熟悉上有如许的误区,以为只有在 netty,nio 如许的多路复用 IO 模子时,读写才不会相互壅闭,才可以实现高效的双向通讯,但实际上,Java Socket 是全双工的:在恣意时间,线路上存在A 到 B 和 B 到 A 的双向信号传输。纵然是壅闭 IO,读和写是可以同时举行的,只要分别接纳读线程和写线程即可,读不会壅闭写、写也不会壅闭读
服务端:
  1. String FROM = "helloword/data.txt";
  2. String TO = "helloword/to.txt";
  3. long start = System.nanoTime();
  4. try (FileChannel from = new FileInputStream(FROM).getChannel();
  5.      FileChannel to = new FileOutputStream(TO).getChannel();
  6.     ) {
  7.     from.transferTo(0, from.size(), to);
  8. } catch (IOException e) {
  9.     e.printStackTrace();
  10. }
  11. long end = System.nanoTime();
  12. System.out.println("transferTo 用时:" + (end - start) / 1000_000.0);//transferTo 用时:8.2011
复制代码
客户端:
  1. public class TestFileChannelTransferTo {
  2.     public static void main(String[] args) {
  3.         try (
  4.                 FileChannel from = new FileInputStream("data.txt").getChannel();
  5.                 FileChannel to = new FileOutputStream("to.txt").getChannel();
  6.         ) {
  7.             // 效率高,底层会利用操作系统的零拷贝进行优化
  8.             long size = from.size();
  9.             // left 变量代表还剩余多少字节
  10.             for (long left = size; left > 0; ) {
  11.                 System.out.println("position:" + (size - left) + " left:" + left);
  12.                 left -= from.transferTo((size - left), left, to);
  13.             }
  14.         } catch (IOException e) {
  15.             e.printStackTrace();
  16.         }
  17.     }
  18. }
复制代码
JavaNIO的缺陷

利用 Java 原生 NIO 来编写服务器应用,代码一样寻常类似:
  1. position:0 left:7769948160
  2. position:2147483647 left:5622464513
  3. position:4294967294 left:3474980866
  4. position:6442450941 left:1327497219
复制代码
selector.select() 应该 不停壅闭,直到有停当变乱到达,但很遗憾,由于 Java NIO 实现上存在 bug,select() 大概在 没有 任何停当变乱的情况下返回,从而导致 while(true) 被不停实验,末了导致某个 CPU 核心的利用率飙升到 100%,这就是污名昭著的 Java NIO 的 epoll bug。
实际上,这是 Linux 体系下 poll/epoll 实现导致的 bug,但 Java NIO 并未美满处理惩罚它,以是也可以说是 Java NIO 的 bug。
该题目最早在 Java 6 发现,随后很多版本声称办理了该题目,但实际上只是低沉了该 bug 的出现频率,最少从网上搜刮看,Java 8 照旧存在该题目。

免责声明:如果侵犯了您的权益,请联系站长及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金.

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表