Java NIO 入门
本文主要记录 Java 中 NIO 相关的基础知识点,以及基本的使用方式。
一、回顾传统的 I/O
刚接触 Java 中的 I/O 时,使用的传统的 BIO 的 API。由于 BIO 设计的类实在太多,至今我仍然不能信手拈来的写出完整的 BIO 的代码。不过它基本的特点和分类,我还是记得一二的。
- 从方向上看,分为输入流和输出流;
- 从类别上看,分为字节流和字符流;
- 从缓冲去上看,分为带缓冲的流和不带缓冲的流。
一个便于使用的的流对象的构建,一般都是由相对底层的流逐渐构建出相对高级的流。通常我都是用 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 类:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
从名称上看,就能知道这些类分别对应了文件、UDP、TCP(Client、Server)。
2.2 Buffer
Buffer 也就是缓冲区。它负责将 Channel 中的数据取出来(读数据),或者将用户程序的数据放入 Channel(写数据)。如果将 Channel 比作是一架飞机及其航线,那么 Buffer 就是航站楼与飞机之
间的摆渡车。下图就是 Buffer 和 Channel 之间的交互:
Buffer 是用户程序与 Channel 进行数据交互的工具。ByteBuffer 是 NIO 中最底层的实现,在此基础上,还有 CharBuffer、DoubleBuffer 等。
ByteBuffer 实质上是维护了一个字节数组,它包含了一个几个特殊的属性:
- capacity:缓冲区的长度;
- position:下一个要操作元素的索引;
- limit:当前可操作元素的最大索引;
- 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();
}
}
}
}
}
}
关于该示例的一些说明:
- 示例中将通道注册到选择器时,没有注册写事件(15 行)。原因是当操作系统中发送数据的缓冲区未满时,写操作一般都是可用的。如果注册了写事件,由于写事件一直是就绪的,那么 select()方法会立刻返回,这就会导致程序的 CPU 使用率一路飙升;
- 示例中未包含向通道写入数据的演示。 写入数据可以直接将数据写入通道,也可以使用通道注册时返回的 key 来获取通道,然后写入数据到通道;
- 示例代码中的 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 的原因:
- BIO 和 NIO 的 API 都很繁杂,使用起来十分的不优雅。尤其是在 BIO 和 NIO 之间切换的时候,几乎是推倒重建;
- NIO 还有空轮询的 BUG;
- 实现稳定可靠的网络 I/O 程序是一个极具挑战的任务,这里面涉及了很多的知识:计算机网络、操作系统、程序设计等。
五、参考资料
- 攻破JAVA NIO技术壁垒 - CSDN博客 基础知识介绍的很全面
- Java NIO服务器:远程主机强迫关闭了一个现有的连接 - CSDN博客
- SocketChannel---各种注意点 - CSDN博客
Java NIO 入门的更多相关文章
- Java NIO入门(二):缓冲区内部细节
Java NIO 入门(二)缓冲区内部细节 概述 本文将介绍 NIO 中两个重要的缓冲区组件:状态变量和访问方法 (accessor). 状态变量是前一文中提到的"内部统计机制"的 ...
- 史上最强Java NIO入门:担心从入门到放弃的,请读这篇!
本文原题“<NIO 入门>,作者为“Gregory M. Travis”,他是<JDK 1.4 Tutorial>等书籍的作者. 1.引言 Java NIO是Java 1.4版 ...
- Java NIO入门
NIO入门 前段时间在公司里处理一些大的数据,并对其进行分词.提取关键字等.虽说任务基本完成了(效果也不是特别好),对于Java还没入门的我来说前前后后花了2周的时间,我自己也是醉了.当然也有涉及到机 ...
- java NIO入门【原】
server package com.server; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import jav ...
- Java NIO入门小例(短连接:客户端和服务器一问一答)
例子中有些写法参考自Netty4源码,建议在实际运用中采用Netty,而非原生的Java NIO(小心epoll空转). 1. 服务器端 public class NioServer { static ...
- Java nio 笔记:系统IO、缓冲区、流IO、socket通道
一.Java IO 和 系统 IO 不匹配 在大多数情况下,Java 应用程序并非真的受着 I/O 的束缚.操作系统并非不能快速传送数据,让 Java 有事可做:相反,是 JVM 自身在 I/O 方面 ...
- Java Socket NIO入门
Java Socket.SocketServer的读写.连接事件监听,都是阻塞式的.Java提供了另外一种非阻塞式读写.连接事件监听方式——NIO.本文简单的介绍一个NIO Socket入门例子,原理 ...
- JAVA NIO异步通信框架MINA选型和使用的几个细节(概述入门,UDP, 心跳)
Apache MINA 2 是一个开发高性能和高可伸缩性网络应用程序的网络应用框架.它提供了一个抽象的事件驱动的异步 API,可以使用 TCP/IP.UDP/IP.串口和虚拟机内部的管道等传输方式.A ...
- Mina入门:Java NIO基础概念
JDK1.4引入了Java NIO API(Java New IO),Java NIO得到了广泛应用.NIO允许程序进行非阻塞IO操作.java.nio.* 包括以下NIO基本结构: Buffer - ...
随机推荐
- PHP的openssl_encrypt方法的Java实现
<?php class OpenSSL3DES { /*密钥,22个字符*/ const KEY='09bd821d3e764f44899a9dc6'; /*向量,8个或10个字符*/ cons ...
- js 获取二级域名
js获取页面完整地址: window.location.href; var s =" https://ejym.baidu.com"; var h = s.s ...
- struts1与struts2的区别。
1) 两个框架都是MVC的WEB框架, 2) struts1是老牌框架,应用很广泛,有很好的群众基础,使用它开发风险很小,成本低,Struts2核心设计思想主要源自Webwork,实现更优雅,更容易扩 ...
- 【转】Binlog 基本操作
MySQL的二进制日志可以说是MySQL最重要的日志了,它记录了所有的DDL和DML(除了数据查询语句)语句,以事件形式记录,还包含语句所执行的消耗的时间,MySQL的二进制日志是事务安全型的. 一般 ...
- 八大排序算法——希尔(shell)排序(动图演示 思路分析 实例代码java 复杂度分析)
一.动图演示 二.思路分析 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序:随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止. 简单插 ...
- scatter参数
列出scatter常用的一些参数: plt.scatter(x,y,c= '颜色可选',marker= '点的样式', cmap= '颜色变化',alpha=“透明度”, linewidths=“线宽 ...
- Webpack学习-工作原理(下)
继上篇文章介绍了Webpack的基本概念,完整流程,以及打包过程中广播的一些事件的作用,这篇文章主要讲生成的chunk文件如何输出成具体的文件.分同步和异步两种情况来分析输出的文件使用的webpack ...
- 微信小程序wx.previewImage实用案例(交流QQ群:604788754)
本案例是可以滑动预览多张图片效果.(本案例在本地配置好之后,请扫描二维码到手机滑动预览.在开发者工具上预览,滑动不是很流畅) 图片必须选择远程图片,本地图片无法实现预览. 或是通过wx.chooseI ...
- lr12关联,响应乱码
1.前程贷登录.投标脚本 Action() { // web_url("登录页", // "URL=http://120.78.128.25:8765 ...
- UVa LA 3266 - Tian Ji -- The Horse Racing 贪心,不只处理一端,也处理另一端以理清局面 难度: 2
题目 https://icpcarchive.ecs.baylor.edu/index.php?option=com_onlinejudge&Itemid=8&page=show_pr ...