Java NIO 入门

时间:2022-08-23 21:49:26

本文主要记录 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 之间的交互:

Java  NIO 入门

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

  Java  NIO 入门

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选型和使用的几个细节&lpar;概述入门,UDP&comma; 心跳&rpar;

    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. Vagrant 快速入门

    1. Vagrant功能: Vagrant uses Oracle’s VirtualBox to build configurable, lightweight, and portable virt ...

  2. caffe源码阅读&lpar;2&rpar;-Layer

    神经网络是由层组成的,深度神经网络就是层数多了.layer对应神经网络的层.数据以Blob的形式,在不同的layer之间流动.caffe定义的神经网络已protobuf形式定义.例如: layer { ...

  3. Nopcommerce 3&period;7 增加了Redis 作为缓存啦

    Nopcommerce 3.7  版增加了Redis 缓存,相比之前内存缓存, 感觉使用了Redis 作为缓存,明显加快了Web页面响应速度! 废话少说 拉代码: 1 git clone https: ...

  4. bzoj2588

    一开始一看树上的操作,就无脑写了树链剖分+主席树 然后果断T了,因为树链剖分+主席树必然带来两个log的复杂度 而且树链剖分复杂度还比较大…… 后来发现其实没必要,在这道题,我们可以直接利用主席树维护 ...

  5. Activity的创建和使用

    Activity: 1:创建一个类继承Activity或者它的子类 public class MainActivity extends Activity { @Override protected v ...

  6. VC2008如何生成及使用DLL&lpar;图文并茂,完整版&rpar;

    博客分类: Dot net VC2008 DLL Dot net   生成.使用DLL看起来简单,但做起来才发现还是有一些地方需要注意的. 1. 打开VS2008,新建一个VC工程,选择Win32类型 ...

  7. javascript&colon;void&lpar;0&rpar;&semi; 是什么意思

    javascript:void(0); 是什么意思.. ------------------------------------------------- 常常在百度云网页版下载东西的时候网页的左下角 ...

  8. 并发库应用之十二 &amp&semi; 常用集合问题汇总

    1. List遍历时修改报错 别的先什么都不说,直接上代码看看就知道了: public class ListTest { public static void main(String[] args) ...

  9. 最近邻算法(KNN)

    最近邻算法: 1.什么是最近邻是什么? kNN算法全程是k-最近邻算法(k-Nearest Neighbor) kNN算法的核心思想是如果一个样本在特征空间中的k个最相邻的样本中的大多数数以一个类型别 ...

  10. (转)RabbitMQ学习

    (二期)24.消息中间件RabbitMq [课程24]RabbitM...概念.xmind60.2KB [课程24]五种队列模式.xmind0.8MB [课程24]消息确...rm).xmind84. ...