本文主要记录 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. fibos开发踩坑集合

    fibos.js API资料: 与eosjs相比,fibos.js没有添加新功能,可以在eosjs项目页面https://developers.eos.io/eosio-nodeos/referenc ...

  2. vue中computed、metfods、watch的区别

    一.computed和methods 我们可以将同一函数定义为一个 method 或者一个计算属性.对于最终的结果,两种方式确实是相同的. 不同的是computed计算属性是基于它们的依赖进行缓存的. ...

  3. Java多线程之synchronized及其优化

    Synchronized和同步阻塞synchronized是jvm提供的同步和锁机制,与之对应的是jdk层面的J.U.C提供的基于AbstractQueuedSynchronizer的并发组件.syn ...

  4. 《程序设计入门——C语言》翁恺老师 第二周编程练习记录

    1 逆序的三位数(5分) 题目内容: 逆序的三位数: 程序每次读入一个正三位数,然后输出逆序的数字.注意,当输入的数字含有结尾的0时,输出不应带有前导的0.比如输入700,输出应该是7. 提示:用%1 ...

  5. [Codeforces375E]Red and Black Tree

    Problem 给定一棵有边权的树.树上每个点是黑或白的.黑白点能两两交换. 求符合任意一个白点到最近黑点的距离小于等于x时,黑白点交换次数最少为多少. Solution 明显是一题树形DP.我们先跑 ...

  6. failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected 排坑指南

    训练maskrcnn时,出现了 failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected 一开始以 ...

  7. 后端程序员必会常用Linux命令总结

    1. 调整终端窗口大小: ctrl + '-'  缩小, ctrl + shift + '='  放大. 2. command --help 查询命令详细 或者 man command 3.ls命令, ...

  8. 详解Python的作用域和命名空间

    最近在学习Python,不得不说,Python真的是一门很好用的语言.但是学习的过程中关于变量作用域(scope)的命名空间(namespace)的问题真的把我给搞懵了.在查阅了相关资料之后,觉得自己 ...

  9. 算法面试题(python)——如何找出数组中出现一次的数

    题目描述: 一个数组里,除了三个数是唯一出现的,其余的数都出现了偶数次,找出这三个数中任意一个.比如数组序列为[1,2,4,5,6,4,2],只有1.5.6这三个数字是唯一出现的,数字2.4均出现了偶 ...

  10. 读取properties配置文件,value值为中文时出现乱码

    已确保idea工具默认设置都是UTF-8格式:     然后在配置类上,指定编码: @PropertySource(value = "classpath:short_message.prop ...