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 - ...
随机推荐
- hive -- 自定义函数和Transform
hive -- 自定义函数和Transform UDF操作单行数据, UDAF:聚合函数,接受多行数据,并产生一个输出数据行 UDTF:操作单个数据 使用udf方法: 第一种: add jar xxx ...
- typescript初入门
1.通过npm安装 typescript 进入终端窗口安装typescript: npm install -g typescript 查看typescript版本号: tsc -v 2.编译代码:t ...
- Fortran程序调试中的“吐核”错误
在CentOS7上安装了PGI编译器,但是调试过程中遇到的“段错误(吐核)”一直让人很头疼. 通常采用在程序中增加屏幕输出代码的方式来追踪和定位出错的变量,比如下面这个样例程序就在第16行和第18行增 ...
- git--编写好代码文件后更新到git仓库流程
先说一下git仓库分类: Git分为三大部分存储区域1:工作区域(就是你打开编辑器的本地代码仓库)2:提交缓存区域(使用git add 命令暂时放置的区域)3:git远程仓库(使用git push命令 ...
- 浅谈C中操作字符串函数的用法(一)
按照内核string.h中函数的顺序进行大概的介绍,若干函数会给出一个简单的例子.有不足之处还希望各位看到的留言告知. 一.memcpy: 函数原型:extern void * memcpy(void ...
- s6k0:一种输入法分词关联模型演示
实现:用kotlin.但是考虑到习惯问题,需要借助akka实现erlang的actor,以及rx.java 需求:略 预计:最快两周 保守估计时间:2019年3月左右 优先级:低 加速方法:打饭钱 赞 ...
- 关于Servlet的一些归纳(2)
1.web项目结构 根路径: 文件夹 文件 WEB-INF: lib(存放一些jar文件) classes(存放class文件) web.xml 2.GenericServlet类 实现了Servle ...
- 【转载】JsonLayout log4j2 json格式输出日志
JsonLayout log4j2 json格式输出日志 如果日志输出时,想改变日志的输出形式为Json格式,可以在log4j2.xml中使用JsonLayout标签,使日志输出格式为Json格式. ...
- 简易祖玛--canvas
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...
- select、poll、epoll的区别
本文写于2017-02-26,从老账号迁移到本账号,原文地址:https://www.cnblogs.com/huangweiyang/p/6444746.html select.poll.epoll ...