本文主要记录 Java 中  NIO 相关的基础知识点,以及基本的使用方式。

一、回顾传统的 I/O

刚接触 Java 中的 I/O 时,使用的传统的 BIO 的 API。由于 BIO 设计的类实在太多,至今我仍然不能信手拈来的写出完整的 BIO 的代码。不过它基本的特点和分类,我还是记得一二的。

  1. 从方向上看,分为输入流和输出流;
  2. 从类别上看,分为字节流和字符流;
  3. 从缓冲去上看,分为带缓冲的流和不带缓冲的流。

一个便于使用的的流对象的构建,一般都是由相对底层的流逐渐构建出相对高级的流。通常我都是用 BIO 来做一些本地文件的 I/O 操作 。在网络编程方面,BIO 因为其阻塞的原因,大家使用的都比较少,一般都使用 NIO,尤其是在服务端的网络开发。强大的 Netty 正是基于 Java NIO 的基础而开发出来的高性能框架,在学习 Netty 之前,很有必要去掌握 NIO 的基本使用。

二、NIO 底层 API

NIO  相关的核心概念有 3 个,Channel、Buffer 和 Selector。

2.1 Channel

  Java 中传统的 BIO 分为输入流和输出流,在同一个 Socket 连接或者文件的 IO 中,需要同时使用这两种流才能进行数据的交互。而 NIO 则使用了 Channel 的概念,可以对 Channel 进行双向操作。我们可以将数据写入到 Channel,也可以从 Channel 中读取数据。

Channel 的主要实现有以下 4 类:

  1. FileChannel
  2. DatagramChannel
  3. SocketChannel
  4. ServerSocketChannel

从名称上看,就能知道这些类分别对应了文件、UDP、TCP(Client、Server)。

2.2 Buffer

Buffer 也就是缓冲区。它负责将 Channel 中的数据取出来(读数据),或者将用户程序的数据放入 Channel(写数据)。如果将 Channel 比作是一架飞机及其航线,那么 Buffer 就是航站楼与飞机之

间的摆渡车。下图就是 Buffer 和 Channel 之间的交互:

Buffer 是用户程序与 Channel 进行数据交互的工具。ByteBuffer 是 NIO 中最底层的实现,在此基础上,还有 CharBuffer、DoubleBuffer 等。

ByteBuffer 实质上是维护了一个字节数组,它包含了一个几个特殊的属性:

  1. capacity:缓冲区的长度;
  2. position:下一个要操作元素的索引;
  3. limit:当前可操作元素的最大索引;
  4. mark:标记当前 position 的前 1 位。

  具体怎么用,可以查询 JDK API,只要直到其他只属性即可。

2.3 Selector

  在学习了 Channel 和 Buffer 之后,已经可以使用这两个类了进行阻塞式的 I/O 操作了。但是 Selector 才是 Java NIO 的核心优势点。只有 ScoketChannel 才能设置为非阻塞模式,所以 Selector 只能在网络 I/O 中才能使用。

  Selector 的核心方法就是 select()方法,该方法会一直阻塞,直到注册在该选择器上的通道有用户所感兴趣的事件准备就绪了才会返回。这里面又涉及到 Selector 与 Channel 之间的映射,这个关系用 SelectionKey 来表示。在调用选择器的 select()方法前,用户可以使用 Channel 的 register()方法,将其注册到选择器上,同时表明用户对该通道的哪些操作感兴趣。注意,register()方法返回的就是 SelectionKey 对象。

三、客户端示例

3.1 客户端示例:

  public class Client {
private static final int REMOTE_PORT = 8888;
private static final String REMOTE_HOST = "127.0.0.1";
private static final int BUFF_SIZE = 1024; public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
SocketChannel socketChannel =
SocketChannel.open(new InetSocketAddress(REMOTE_HOST, REMOTE_PORT));
     //将套接字通道设置为非阻塞模式
socketChannel.configureBlocking(false);
     //将通道注册到 Selector 中,第二个参数为该通道感兴趣的事件,此处为读事件
     //注册方法会返回一个 SelectionKey 对象,它代表通道与选择器之间的映射关系
socketChannel.register(selector,
SelectionKey.OP_READ, ByteBuffer.allocateDirect(BUFF_SIZE));
if (!socketChannel.isConnected()) {
socketChannel.finishConnect();
}
for (; ; ) {
       //选择器的 select()方法会阻塞到有通道所感兴趣的事件已经就绪
if (selector.select() > 0) {
          //调用选择器的 selectedKeys() 方法会返回,本次所有就绪通道对应的 SelectionKey 集合
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
//通过迭代去删除掉本次将要处理的 key
//如果不删除,下次 select 还会返回该 key
it.remove();
if (key.isReadable()) {
ByteBuffer bf = (ByteBuffer) key.attachment();
bf.clear();
SocketChannel sc = (SocketChannel) key.channel();
int i = sc.read(bf);
if (i < 0) {
key.cancel();
sc.close();
return;
}
bf.flip();
byte[] ret = new byte[bf.remaining()];
bf.get(ret);
for (byte b : ret) {
System.out.print(b);
}
System.out.println();
}
}
}
}
}
}

  关于该示例的一些说明:

  1. 示例中将通道注册到选择器时,没有注册写事件(15 行)。原因是当操作系统中发送数据的缓冲区未满时,写操作一般都是可用的。如果注册了写事件,由于写事件一直是就绪的,那么 select()方法会立刻返回,这就会导致程序的 CPU 使用率一路飙升;
  2. 示例中未包含向通道写入数据的演示。 写入数据可以直接将数据写入通道,也可以使用通道注册时返回的 key 来获取通道,然后写入数据到通道;
  3. 示例代码中的 31 行,当读取到的字节数为 -1 时,会让人产生疑惑,既然这个通道被 select 出来了,那么为什么没有数据可读呢?有一种情况就是对端关闭了套接字连接,此时客户端的 select()方法每次都会立刻返回,导致空轮询。并且每次都能 select 出这个对端已经关闭了的通道。如果我们不关闭该通道,稍后可能就会抛出 IOException远程主机强迫关闭了一个现有的连接JAVA NIO客户端主动关闭连接,导致服务器空轮询 - SegmentFault 思否

  对于上面的第 3 点,之前遇到过一个问题,对端如果发现连接在指定的间隔内没有数据通讯,就会关闭掉连接,这个时候我们也需要关闭对应的通道。下图是抓包的结果:

  

3.2 服务端示例:

 public class Server {

     private static final int PORT = 8888;

     public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.socket().bind(new InetSocketAddress(PORT));
//对可连接事件感兴趣
ssc.register(selector, SelectionKey.OP_ACCEPT);
for (; ; ) {
if (selector.select() > 0) {
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
if (key.isAcceptable()) {
//处理客户端的连接
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
//通过该 key 处理对应的读事件
}
}
}
}
}
}

  基于 Selector 的 NIO 模式,可以使用一个线程来处理大量的连接,优势十分明显。

四、总结

  以上只是 Java 中 NIO 的粗略介绍,仍需进一步熟悉各个 API 的使用方法。在熟悉 NIO 之后,下一步准备学习一下 Netty 框架的使用。

准备使用 Netty 的原因:

  1. BIO 和 NIO 的 API 都很繁杂,使用起来十分的不优雅。尤其是在 BIO 和 NIO 之间切换的时候,几乎是推倒重建;
  2. NIO 还有空轮询的 BUG;
  3. 实现稳定可靠的网络 I/O 程序是一个极具挑战的任务,这里面涉及了很多的知识:计算机网络、操作系统、程序设计等。

五、参考资料

Java NIO 入门的更多相关文章

  1. Java NIO入门(二):缓冲区内部细节

    Java NIO 入门(二)缓冲区内部细节 概述 本文将介绍 NIO 中两个重要的缓冲区组件:状态变量和访问方法 (accessor). 状态变量是前一文中提到的"内部统计机制"的 ...

  2. 史上最强Java NIO入门:担心从入门到放弃的,请读这篇!

    本文原题“<NIO 入门>,作者为“Gregory M. Travis”,他是<JDK 1.4 Tutorial>等书籍的作者. 1.引言 Java NIO是Java 1.4版 ...

  3. Java NIO入门

    NIO入门 前段时间在公司里处理一些大的数据,并对其进行分词.提取关键字等.虽说任务基本完成了(效果也不是特别好),对于Java还没入门的我来说前前后后花了2周的时间,我自己也是醉了.当然也有涉及到机 ...

  4. java NIO入门【原】

    server package com.server; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import jav ...

  5. Java NIO入门小例(短连接:客户端和服务器一问一答)

    例子中有些写法参考自Netty4源码,建议在实际运用中采用Netty,而非原生的Java NIO(小心epoll空转). 1. 服务器端 public class NioServer { static ...

  6. Java nio 笔记:系统IO、缓冲区、流IO、socket通道

    一.Java IO 和 系统 IO 不匹配 在大多数情况下,Java 应用程序并非真的受着 I/O 的束缚.操作系统并非不能快速传送数据,让 Java 有事可做:相反,是 JVM 自身在 I/O 方面 ...

  7. Java Socket NIO入门

    Java Socket.SocketServer的读写.连接事件监听,都是阻塞式的.Java提供了另外一种非阻塞式读写.连接事件监听方式——NIO.本文简单的介绍一个NIO Socket入门例子,原理 ...

  8. JAVA NIO异步通信框架MINA选型和使用的几个细节(概述入门,UDP, 心跳)

    Apache MINA 2 是一个开发高性能和高可伸缩性网络应用程序的网络应用框架.它提供了一个抽象的事件驱动的异步 API,可以使用 TCP/IP.UDP/IP.串口和虚拟机内部的管道等传输方式.A ...

  9. Mina入门:Java NIO基础概念

    JDK1.4引入了Java NIO API(Java New IO),Java NIO得到了广泛应用.NIO允许程序进行非阻塞IO操作.java.nio.* 包括以下NIO基本结构: Buffer - ...

随机推荐

  1. (JavaScript)实现上传图片实时预览和(文件)大小判断

    唉,为什么我一个做大数据和后端的要为前端耗尽心力啊??!! 昨天在做一个网页时遇到了一个问题,有一处需要插入图片,我原本的想法是获取到上传文件的URL,然后动态插入img标签,设置src为图片的URL ...

  2. There are multiple modules with names that only differ in casing. 黄色warning

    There are multiple modules with names that only differ in casing.有多个模块同名仅大小写不同This can lead to unexp ...

  3. js 如何判断数组元素是否存在重复项

    1.如何判断数组元素是否存在重复项 1)定义测试数组 //定义测试的数组(1个没有重复元素,1个有重复元素) var arr1 = new Array("111","33 ...

  4. 构建web应用之——文件上传

    我们通过使用multipart请求数据接收和处理二进制信息(如文件).DispatcherServlet并没有实现任何解析multipart请求数据的功能,它将该任务委托给了Spring中的multi ...

  5. MFC界面分割以及挂载

     MFC中文档与视图(二) Last Edit 2013/11/19 这篇主要是介绍一下怎么去分割视图. 视图的分割分为:动态分割,静态分割.所谓的静态分割是指软件一启动视图就分割完成,而动态分割是在 ...

  6. java字符串根据正则表达式让单词首字母大写

    public class Da { public static void main(String[] args) { String s = "hello_*java_*world" ...

  7. java第二次笔记

  8. Lumen框架使用Redis与框架Cache压测比较

    使用命令 ab -c 20000 -n 100000 'http://127.0.0.1:9050/v1/api.store.xxx'进行压测,并同时进行了交叉测试,结果如下: 高并发情况下数据目前没 ...

  9. MFC如何在树形图边上添加动态小地图

    MFC如何在树形图边上添加动态小地图 https://www.jianshu.com/p/7b1d828bf5db (简书无法识别缩进的...早知道先在博客园发了) (转载请注明出处) 作者:梦镜谷雨 ...

  10. cdh日常维护常见问题及解决方案

    为数据节点添加新硬盘 - 挂载硬盘到指定文件夹.如`/dfs_diskb`: - 打开cloudera manager -> hdfs -> 配置 -> DataNode -> ...