提交 5a85309c 编写于 作者: 沉默王二's avatar 沉默王二 💬

java NIO

上级 d326a867
......@@ -218,8 +218,11 @@
## Java NIO
- [为什么我们要使用 Java NIO?](docs/nio/why.md)
- [Java NIO 快速入门(buffer缓冲区、Channel管道、Selector选择器)](docs/nio/rumen.md)
- [一文彻底理解Java IO模型(阻塞IO非阻塞IO/IO多路复用)](docs/nio/moxing.md)
- [使用Java NIO完成网络通信](docs/nio/network-connect.md)
- [如何给女朋友解释什么是 BIO、NIO 和 AIO?](docs/nio/BIONIOAIO.md)
- [一文彻底理解 Java NIO 核心组件](docs/nio/nio.md)
## Java并发编程
......
......@@ -330,8 +330,11 @@ export const sidebarConfig = sidebar({
collapsable: true,
prefix: "nio/",
children: [
"why",
"rumen",
"moxing",
"network-connect",
"BIONIOAIO",
"nio",
],
},
{
......
......@@ -233,8 +233,11 @@ head:
### Java NIO
- [为什么我们要使用 Java NIO?](nio/why.md)
- [Java NIO 快速入门(buffer缓冲区、Channel管道、Selector选择器)](nio/rumen.md)
- [一文彻底理解Java IO模型(阻塞IO非阻塞IO/IO多路复用)](nio/moxing.md)
- [使用Java NIO完成网络通信](nio/network-connect.md)
- [如何给女朋友解释什么是 BIO、NIO 和 AIO?](nio/BIONIOAIO.md)
- [一文彻底理解 Java NIO 核心组件](nio/nio.md)
### Java并发编程
......
---
title: Java NIO 学习笔记(一)
shortTitle: Java NIO 学习笔记(一)
author: 概述,Channel/Buffer
category:
- 博客园
---
**目录:**
[Java NIO 学习笔记(一)----概述,Channel/Buffer](https://www.cnblogs.com/czwbig/p/10035631.html)
[Java NIO 学习笔记(二)----聚集和分散,通道到通道](https://www.cnblogs.com/czwbig/p/10040349.html)
[Java NIO 学习笔记(三)----Selector](https://www.cnblogs.com/czwbig/p/10043421.html)
[Java NIO 学习笔记(四)----文件通道和网络通道](https://www.cnblogs.com/czwbig/p/10046987.html)
[Java NIO 学习笔记(五)----路径、文件和管道 Path/Files/Pipe](https://www.cnblogs.com/czwbig/p/10056126.html)
[Java NIO 学习笔记(六)----异步文件通道 AsynchronousFileChannel](https://www.cnblogs.com/czwbig/p/10056131.html)
[Java NIO 学习笔记(七)----NIO/IO 的对比和总结](https://www.cnblogs.com/czwbig/p/10056804.html)
Java NIO (来自 Java 1.4)可以替代标准 IO 和 Java Networking API ,NIO 提供了与标准 IO 不同的使用方式。学习 NIO 之前建议先掌握标准 IO 和 Java 网络编程,推荐教程:
* [系统学习 Java IO----目录,概览](https://www.cnblogs.com/czwbig/p/10007201.html)
* [初步接触 Java Net 网络编程](https://www.cnblogs.com/czwbig/p/10018118.html)
**本文目的:** 掌握了标准 IO 之后继续学习 NIO 知识。主要参考 JavaDoc 和 Jakob Jenkov 的英文教程 [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html)
# Java NIO 概览
NIO 由以下核心组件组成:
1. 通道和缓冲区
在标准 IO API 中,使用字节流和字符流。 在 NIO 中使用通道和缓冲区。 数据总是从通道读入缓冲区,或从缓冲区写入通道。
2. 非阻塞IO
NIO 可以执行非阻塞 IO 。 例如,当通道将数据读入缓冲区时,线程可以执行其他操作。 并且一旦数据被读入缓冲区,线程就可以继续处理它。 将数据写入通道也是如此。
3. 选择器
NIO 包含“选择器”的概念。 选择器是一个可以监视多个事件通道的对象(例如:连接打开,数据到达等)。 因此,单个线程可以监视多个通道的数据。
NIO 有比这些更多的类和组件,但在我看来,Channel,Buffer 和 Selector 构成了 API 的核心。 其余的组件,如 Pipe 和 FileLock ,只是与三个核心组件一起使用的实用程序类。
### Channels/Buffers 通道和缓冲区
通常,NIO 中的所有 IO 都以 Channel 开头,频道有点像流。 数据可以从 Channel 读入 Buffer,也可以从 Buffer 写入 Channel :
![通道将数据读入缓冲区,缓冲区将数据写入通道](//upload-images.jianshu.io/upload_images/14923529-b3432ef114d32991.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
有几种 Channel 和 Buffer ,以下是 NIO 中主要 Channel 实现类的列表,这些通道包括 UDP + TCP 网络 IO 和文件 IO:
* FileChannel :文件通道
* DatagramChannel :数据报通道
* SocketChannel :套接字通道
* ServerSocketChannel :服务器套接字通道
这些类也有一些有趣的接口,但为了简单起见,这里暂时不提,后续会进行学习的。
以下是 NIO 中的核心 Buffer 实现,其实就是 7 种基本类型:
* ByteBuffer
* CharBuffer
* ShortBuffer
* IntBuffer
* LongBuffer
* FloatBuffer
* DoubleBuffer
NIO 还有一个 MappedByteBuffer,它与内存映射文件一起使用,同样这个后续再讲。
### Selectors 选择器
选择器允许单个线程处理多个通道。 如果程序打开了许多连接(通道),但每个连接只有较低的流量,使用选择器就很方便。 例如,在聊天服务器中, 以下是使用 Selector 处理 3 个 Channel 的线程图示:
![1个线程使用选择器处理3个通道](//upload-images.jianshu.io/upload_images/14923529-6c435a8b1a6f1593.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
要使用选择器,需要使用它注册通道。 然后你调用它的 select() 方法。 此方法将阻塞,直到有一个已注册通道的事件准备就绪。 一旦该方法返回,该线程就可以处理这些事件。 事件可以是传入连接,接收数据等。
# Channel (通道)
NIO 通道类似于流,但有一些区别:
* 通道可以读取和写入。 流通常是单向的(读或写)。
* 通道可以异步读取和写入。
* 通道始终读取或写入缓冲区,即它只面向缓冲区。
如上所述,NIO 中总是将数据从通道读取到缓冲区,或将数据从缓冲区写入通道。 这是一个例子:
```java
// 文件内容是 123456789
RandomAccessFile accessFile = new RandomAccessFile("D:\\test\\1.txt", "rw");
FileChannel fileChannel = accessFile.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(48);
int data = fileChannel.read(buffer); // 将 Channel 的数据读入缓冲区,返回读入到缓冲区的字节数
```
# Buffer(缓冲区)
使用 Buffer 与 Channel 交互,数据从通道读入缓冲区,或从缓冲区写入通道。
缓冲区本质上是一个可以写入数据的内存块,之后可以读取数据。 Buffer 对象包装了此内存块,提供了一组方法,可以更轻松地使用内存块。
### Buffer 的基本用法
使用 Buffer 读取和写入数据通常遵循以下四个步骤:
1. 将数据写入缓冲区
2. 调用 buffer.flip() 反转读写模式
3. 从缓冲区读取数据
4. 调用 buffer.clear() 或 buffer.compact() 清除缓冲区内容
将数据写入Buffer 时,Buffer 会跟踪写入的数据量。 当需要读取数据时,就使用 flip() 方法将缓冲区从写入模式切换到读取模式。 在读取模式下,缓冲区允许读取写入缓冲区的所有数据。
读完所有数据之后,就需要清除缓冲区,以便再次写入。 可以通过两种方式执行此操作:通过调用 clear() 或调用 compact() 。区别在于 clear() 是方法清除整个缓冲区,而 compact() 方法仅清除已读取的数据,未读数据都会移动到缓冲区的开头,新数据将在未读数据之后写入缓冲区。
这是一个简单的缓冲区用法示例:
```java
public class ChannelExample {
public static void main(String[] args) throws IOException {
// 文件内容是 123456789
RandomAccessFile accessFile = new RandomAccessFile("D:\\test\\1.txt", "rw");
FileChannel fileChannel = accessFile.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(48); //创建容量为48字节的缓冲区
int data = fileChannel.read(buffer); // 将 Channel 的数据读入缓冲区,返回读入到缓冲区的字节数
while (data != -1) {
System.out.println("Read " + data); // Read 9
buffer.flip(); // 将 buffer 从写入模式切换为读取模式
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get()); // 每次读取1byte,循环输出 123456789
}
buffer.clear(); // 清除当前缓冲区
data = fileChannel.read(buffer); // 将 Channel 的数据读入缓冲区
}
accessFile.close();
}
}
```
##### Buffer 的 capacity,position 和 limit
缓冲区有 3 个需要熟悉的属性,以便了解缓冲区的工作原理。 这些是:
1. capacity : 容量缓冲区的容量,是它所包含的元素的数量。不能为负并且不能更改。
2. position :缓冲区的位置 是下一个要读取或写入的元素的索引。不能为负,并且不能大于 limit
3. limit : 缓冲区的限制,缓冲区的限制不能为负,并且不能大于 capacity
另外还有标记 mark ,
标记、位置、限制和容量值遵守以下不变式:
0 <= mark<= position <= limit<= capacity
position 和 limit 的含义取决于 Buffer 是处于读取还是写入模式。 无论缓冲模式如何,capacity 总是一样的表示容量。
以下是写入和读取模式下的容量,位置和限制的说明:
![](//upload-images.jianshu.io/upload_images/14923529-02664119749dc674.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
##### capacity
作为存储器块,缓冲区具有一定的固定大小,也称为“容量”。 只能将 capacity 多的 byte,long,char 等写入缓冲区。 缓冲区已满后,需要清空它(读取数据或清除它),然后才能将更多数据写入。
##### position
将数据写入缓冲区时,可以在某个位置执行操作。 position 初始值为 0 ,当一个 byte,long,char 等已写入缓冲区时,position 被移动,指向缓冲区中的下一个单元以插入数据。 position 最大值为 capacity -1
从缓冲区读取数据时,也可以从给定位置开始读取数据。 当缓冲区从写入模式切换到读取模式时,position 将重置为 0 。当从缓冲区读取数据时,将从 position 位置开始读取数据,读取后会将 position 移动到下一个要读取的位置。
##### limit
在写入模式下,Buffer 的 limit 是可以写入缓冲区的数据量的限制,此时 limit=capacity。
将缓冲区切换为读取模式时,limit 表示最多能读到多少数据。 因此,当将 Buffer 切换到读取模式时,limit被设置为之前写入模式的写入位置(position ),换句话说,你能读到之前写入的所有数据(例如之前写写入了 6 个字节,此时 position=6 ,然后切换到读取模式,limit 代表最多能读取的字节数,因此 limit 也等于 6)。
##### 分配缓冲区
要获取 Buffer 对象,必须先分配它。 每个 Buffer 类都有一个 allocate() 方法来执行此操作。 下面是一个显示ByteBuffer分配的示例,容量为48字节:
```java
ByteBuffer buffer = ByteBuffer.allocate(48); //创建容量为48字节的缓冲区
```
##### 将数据写入缓冲区
可以通过两种方式将数据写入 Buffer:
1. 将数据从通道写入缓冲区
2. 通过缓冲区的 put() 方法,自己将数据写入缓冲区。
这是一个示例,显示了 Channel 如何将数据写入 Buffer:
```java
int data = fileChannel.read(buffer); // 将 Channel 的数据读入缓冲区,返回读入到缓冲区的字节数
buffer.put(127); // 此处的 127 是 byte 类型
```
put() 方法有许多其他版本,允许以多种不同方式将数据写入 Buffer 。 例如,在特定位置写入,或将一个字节数组写入缓冲区。
##### flip() 切换缓冲区的读写模式
flip() 方法将 Buffer 从写入模式切换到读取模式。 调用 flip() 会将 position 设置回 0,并将 limit 的值设置为切换之前的 position 值。换句话说,limit 表示之前写进了多少个 byte、char 等 —— 现在能读取多少个 byte、char 等。
##### 从缓冲区读取数据
有两种方法可以从 Buffer 中读取数据:
1. 将数据从缓冲区读入通道。
2. 使用 get() 方法之一,自己从缓冲区读取数据。
以下是将缓冲区中的数据读入通道的示例:
```java
int bytesWritten = fileChannel.write(buffer);
byte aByte = buffer.get();
```
和 put() 方法一样,get() 方法也有许多其他版本,允许以多种不同方式从 Buffer 中读取数据。有关更多详细信息,请参阅JavaDoc以获取具体的缓冲区实现。
以下列出 ByteBuffer 类的部分方法:
方法|描述|
---|---|
byte\[\] array()|返回实现此缓冲区的 byte 数组,此缓冲区的内容修改将导致返回的数组内容修改,反之亦然。|
CharBuffer asCharBuffer()|创建此字节缓冲区作为新的独立的char 缓冲区。新缓冲区的内容将从此缓冲区的当前位置开始|
XxxBuffer asXxxBuffer()|同上,创建对应的 Xxx 缓冲区,Xxx 可为 Short/Int/Long/Float/Double|
byte get()|相对 get 方法。读取此缓冲区当前位置的字节,然后该 position 递增。|
ByteBuffer get(byte\[\] dst, int offset, int length)|相对批量 get 方法,后2个参数可省略|
byte get(int index)|绝对 get 方法。读取指定索引处的字节。|
char getChar()|用于读取 char 值的相对 get 方法。|
char getChar(int index)|用于读取 char 值的绝对 get 方法。|
xxx getXxx(int index)|用于读取 xxx 值的绝对 get 方法。index 可以选,指定位置。|
众多 put() 方法|参考以上 get() 方法|
static ByteBuffer wrap(byte\[\] array)|将 byte 数组包装到缓冲区中。|
##### rewind() 倒带
Buffer对象的 rewind() 方法将 position 设置回 0,因此可以重读缓冲区中的所有数据, limit 则保持不变。
##### clear() 和 compact()
如果调用 clear() ,则将 position 设置回 0 ,并将 limit 被设置成 capacity 的值。换句话说,Buffer 被清空了。 但是 Buffer 中的实际存放的数据并未清除。
如果在调用 clear() 时缓冲区中有任何未读数据,数据将被“遗忘”,这意味着不再有任何标记告诉读取了哪些数据,还没有读取哪些数据。
如果缓冲区中仍有未读数据,并且想稍后读取它,但需要先写入一些数据,这时候应该调用 compact() ,它会将所有未读数据复制到 Buffer 的开头,然后它将 position 设置在最后一个未读元素之后。 limit 属性仍设置为 capacity ,就像 clear() 一样。 现在缓冲区已准备好写入,并且不会覆盖未读数据。
##### mark() 和 reset()
以通过调用 Buffer 对象的 mark() 方法在 Buffer 中标记给定位置。 然后,可以通过调用 Buffer.reset() 方法将位置重置回标记位置,就像在标准 IO 中一样。
```java
buffer.mark();
// 调用 buffer.get() 等方法读取数据...
buffer.reset(); // 设置 position 回到 mark 位置。
```
##### equals() 和 compareTo()
可以使用 equals() 和 compareTo() 比较两个缓冲区。
equals() 成立的条件:
1. 它们的类型相同(byte,char,int等)
2. 它们在缓冲区中具有相同数量的剩余字节,字符等。
3. 所有剩余的字节,字符等都相等。
如上,equals 仅比较缓冲区的一部分,而不是它内部的每个元素。 实际上,它只是比较缓冲区中的其余元素。
compareTo() 方法比较两个缓冲区的剩余元素(字节,字符等), 在下列情况下,一个 Buffer 被视为“小于”另一个 Buffer:
1. 第一个不相等的元素小于另一个 Buffer 中对应的元素 。
2. 所有元素都相等,但第一个 Buffer 在第二个 Buffer 之前耗尽了元素(第一个 Buffer 元素较少)。
>参考链接:[https://www.cnblogs.com/czwbig/p/10035631.html](https://www.cnblogs.com/czwbig/p/10035631.html),整理:沉默王二
---
title: 知识星球 | 深度连接铁杆粉丝,运营高品质社群,知识变现的工具
shortTitle: 知识星球 | 深度连接铁杆粉丝,运营高品质社群,知识变现的工具
description: 知识星球是创作者连接铁杆粉丝,实现知识变现的工具。任何从事创作或艺术的人,例如艺术家、工匠、教师、学术研究、科普等,只要能获得一千位铁杆粉丝,就足够生计无忧,自由创作。社群管理、内容沉淀、链接粉丝等就在知识星球。
tags:
- 优质文章
category:
- 其他网站
head:
- - meta
- name: keywords
content: 社群服务,社群工具,公众号粉丝管理,粉丝社区,内容付费,知识变现,流量变现,粉丝经济,KOL,大 V,知识管理,内容沉淀,企业社区,内部论坛,小团队共享,私密圈,原名小密圈,小秘圈,小蜜圈
---
>参考链接:[https://articles.zsxq.com/id_a20wm4o4aawc.html](https://articles.zsxq.com/id_a20wm4o4aawc.html),整理:沉默王二
......@@ -9,7 +9,7 @@ description: Java程序员进阶之路,小白的零基础Java教程,BIO、NI
head:
- - meta
- name: keywords
content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,Java IO,java BIO,java NIO,java AIO,bio,nio,aio
content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,IO,BIO,NIO,AIO
---
......
---
title: 一文彻底理解Java IO模型(阻塞IO非阻塞IO/IO多路复用)
shortTitle: 一文彻底理解Java IO模型
category:
- Java核心
tag:
- Java NIO
description: Java程序员进阶之路,小白的零基础Java教程,一文彻底理解 Java IO 模型(非阻塞 IO/IO多路复用/异步IO)
head:
- - meta
- name: keywords
content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,nio,多路复用,阻塞IO
---
**文件的IO就告一段落了**,我们来学习网络中的IO~~~为了更好地理解NIO,**我们先来学习一下IO的模型**~
根据UNIX网络编程对I/O模型的分类,**在UNIX可以归纳成5种I/O模型**
* **阻塞I/O**
* **非阻塞I/O**
* **I/O多路复用**
* 信号驱动I/O
* 异步I/O
## 学习I/O模型需要的基础
### 文件描述符
Linux 的内核将所有外部设备**都看做一个文件来操作**,对一个文件的读写操作会**调用内核提供的系统命令(api)**,返回一个`file descriptor`(fd,文件描述符)。而对一个socket的读写也会有响应的描述符,称为`socket fd`(socket文件描述符),描述符就是一个数字,**指向内核中的一个结构体**(文件路径,数据区等一些属性)。
* 所以说:在Linux下对文件的操作是**利用文件描述符(file descriptor)来实现的**
### 用户空间和内核空间
为了保证用户进程不能直接操作内核(kernel),**保证内核的安全**,操心系统将虚拟空间划分为两部分
* **一部分为内核空间**
* **一部分为用户空间**
### I/O运行过程
我们来看看IO在系统中的运行是怎么样的(我们**以read为例**)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/moxing-54ee4738-b689-4026-863f-13e456b374de.jpg)
可以发现的是:当应用程序调用read方法时,是需要**等待**的--->从内核空间中找数据,再将内核空间的数据拷贝到用户空间的。
* **这个等待是必要的过程**
下面只讲解用得最多的3个I/0模型:
* **阻塞I/O**
* **非阻塞I/O**
* **I/O多路复用**
## 阻塞I/O模型
在进程(用户)空间中调用`recvfrom`,其系统调用直到数据包到达且**被复制到应用进程的缓冲区中或者发生错误时才返回**,在此期间**一直等待**
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/moxing-8a1cb207-6c56-4bd8-8489-c21d5a76e1ca.jpg)
## 非阻塞I/O模型
`recvfrom`从应用层到内核的时候,如果没有数据就**直接返回**一个EWOULDBLOCK错误,一般都对非阻塞I/O模型**进行轮询检查这个状态**,看内核是不是有数据到来。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/moxing-6590a3de-0e7c-4ce2-aa1c-815625095e62.jpg)
## I/O复用模型
前面也已经说了:在Linux下对文件的操作是**利用文件描述符(file descriptor)来实现的**
在Linux下它是这样子实现I/O复用模型的:
* 调用`select/poll/epoll/pselect`其中一个函数,**传入多个文件描述符**,如果有一个文件描述符**就绪,则返回**,否则阻塞直到超时。
比如`poll()`函数是这样子的:`int poll(struct pollfd *fds,nfds_t nfds, int timeout);`
其中 `pollfd` 结构定义如下:
```c
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生了的事件 */
};
```
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/moxing-aec90e84-33c5-4f5b-997e-8db54d6bce88.jpg)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/moxing-62def8ad-3ca3-467b-81f6-5d0a31dd7fdc.jpg)
* (1)当用户进程调用了select,那么整个进程会被block;
* (2)而同时,kernel会“监视”所有select负责的socket;
* (3)当任何一个socket中的数据准备好了,select就会返回;
* (4)这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程(空间)。
* 所以,I/O 多路复用的特点是**通过一种机制一个进程能同时等待多个文件描述符**,而这些文件描述符**其中的任意一个进入读就绪状态**,select()函数**就可以返回**
select/epoll的优势并不是对于单个连接能处理得更快,而是**在于能处理更多的连接**
## I/O模型总结
正经的描述都在上面给出了,不知道大家理解了没有。下面我举几个例子总结一下这三种模型:
**阻塞I/O:**
* Java3y跟女朋友去买喜茶,排了很久的队终于可以点饮料了。我要绿研,谢谢。可是喜茶不是点了单就能立即拿,于是我**在喜茶门口等了一小时才拿到**绿研。
* 在门口干等一小时
**非阻塞I/O:**
* Java3y跟女朋友去买一点点,排了很久的队终于可以点饮料了。我要波霸奶茶,谢谢。可是一点点不是点了单就能立即拿,**同时**服务员告诉我:你大概要等半小时哦。你们先去逛逛吧~于是Java3y跟女朋友去玩了几把斗地主,感觉时间差不多了。于是**又去一点点问**:请问到我了吗?我的单号是xxx。服务员告诉Java3y:还没到呢,现在的单号是XXX,你还要等一会,可以去附近耍耍。问了好几次后,终于拿到我的波霸奶茶了。
* 去逛了下街、斗了下地主,时不时问问到我了没有
**I/O复用模型:**
* Java3y跟女朋友去麦当劳吃汉堡包,现在就厉害了可以使用微信小程序点餐了。于是跟女朋友找了个地方坐下就用小程序点餐了。点餐了之后玩玩斗地主、聊聊天什么的。**时不时听到广播在复述XXX请取餐**,反正我的单号还没到,就继续玩呗。~~**等听到广播的时候再取餐就是了**。时间过得挺快的,此时传来:Java3y请过来取餐。于是我就能拿到我的麦辣鸡翅汉堡了。
* 听广播取餐,**广播不是为我一个人服务**。广播喊到我了,我过去取就Ok了。
>参考链接:[https://www.zhihu.com/question/29005375/answer/667616386](https://www.zhihu.com/question/29005375/answer/667616386),整理:沉默王二
---------
最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html)
关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png)
\ No newline at end of file
---
title: 使用Java NIO完成网络通信
shortTitle: 使用Java NIO完成网络通信
category:
- Java核心
tag:
- Java NIO
description: Java程序员进阶之路,小白的零基础Java教程,使用Java NIO完成网络通信
head:
- - meta
- name: keywords
content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,nio,网络通信
---
## NIO基础继续讲解
回到我们最开始的图:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/network-connect-bb1bd676-8aeb-4428-9498-230a05ee717d.jpg)
NIO被叫为 `no-blocking io`,其实是在**网络这个层次中理解的**,对于**FileChannel来说一样是阻塞**
我们前面也仅仅讲解了FileChannel,对于我们网络通信是还有几个Channel的~
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/network-connect-ba836b5c-82d2-42b0-b1ae-83f8ee0b0101.jpg)
所以说:我们**通常**使用NIO是在网络中使用的,网上大部分讨论NIO都是在**网络通信的基础之上**的!说NIO是非阻塞的NIO也是**网络中体现**的!
从上面的图我们可以发现还有一个`Selector`选择器这么一个东东。从一开始我们就说过了,nio的**核心要素**有:
* Buffer缓冲区
* Channel通道
* Selector选择器
我们在网络中使用NIO往往是I/O模型的**多路复用模型**
* Selector选择器就可以比喻成麦当劳的**广播**
* **一个线程能够管理多个Channel的状态**
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/network-connect-63f45193-8eb7-4cc5-a5a7-70713fac0d73.jpg)
## NIO阻塞形态
为了更好地理解,我们先来写一下NIO**在网络中是阻塞的状态代码**,随后看看非阻塞是怎么写的就更容易理解了。
* **是阻塞的就没有Selector选择器了**,就直接使用Channel和Buffer就完事了。
客户端:
```java
public class BlockClient {
public static void main(String[] args) throws IOException {
// 1. 获取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));
// 2. 发送一张图片给服务端吧
FileChannel fileChannel = FileChannel.open(Paths.get("X:\\Users\\ozc\\Desktop\\新建文件夹\\1.png"), StandardOpenOption.READ);
// 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是与数据打交道的呢
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4.读取本地文件(图片),发送到服务器
while (fileChannel.read(buffer) != -1) {
// 在读之前都要切换成读模式
buffer.flip();
socketChannel.write(buffer);
// 读完切换成写模式,能让管道继续读取文件的数据
buffer.clear();
}
// 5. 关闭流
fileChannel.close();
socketChannel.close();
}
}
```
服务端:
```java
public class BlockServer {
public static void main(String[] args) throws IOException {
// 1.获取通道
ServerSocketChannel server = ServerSocketChannel.open();
// 2.得到文件通道,将客户端传递过来的图片写到本地项目下(写模式、没有则创建)
FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
// 3. 绑定链接
server.bind(new InetSocketAddress(6666));
// 4. 获取客户端的连接(阻塞的)
SocketChannel client = server.accept();
// 5. 要使用NIO,有了Channel,就必然要有Buffer,Buffer是与数据打交道的呢
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 6.将客户端传递过来的图片保存在本地中
while (client.read(buffer) != -1) {
// 在读之前都要切换成读模式
buffer.flip();
outChannel.write(buffer);
// 读完切换成写模式,能让管道继续读取文件的数据
buffer.clear();
}
// 7.关闭通道
outChannel.close();
client.close();
server.close();
}
}
```
结果就可以将客户端传递过来的图片保存在本地了:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/network-connect-7545f4e4-dda9-4e62-8463-58f821cb51ed.jpg)
此时服务端保存完图片想要告诉客户端已经收到图片啦:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/network-connect-c25d8b70-cd8f-4f4b-90eb-2e471deeb958.jpg)
客户端接收服务端带过来的数据:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/network-connect-a7841446-4eed-4b7b-a815-0d8666b4dd44.jpg)
如果仅仅是上面的代码**是不行**的!这个程序会**阻塞**起来!
* 因为服务端**不知道客户端还有没有数据要发过来**(与刚开始不一样,客户端发完数据就将流关闭了,服务端可以知道客户端没数据发过来了),导致服务端一直在读取客户端发过来的数据。
* 进而导致了阻塞!
于是客户端在写完数据给服务端时,**显式告诉服务端已经发完数据**了!
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/network-connect-97c88ca9-1b0c-4cd0-b410-60d059605ee2.jpg)
## NIO非阻塞形态
如果使用非阻塞模式的话,那么我们就可以不显式告诉服务器已经发完数据了。我们下面来看看怎么写:
**客户端**
```java
public class NoBlockClient {
public static void main(String[] args) throws IOException {
// 1. 获取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));
// 1.1切换成非阻塞模式
socketChannel.configureBlocking(false);
// 2. 发送一张图片给服务端吧
FileChannel fileChannel = FileChannel.open(Paths.get("X:\\Users\\ozc\\Desktop\\新建文件夹\\1.png"), StandardOpenOption.READ);
// 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是与数据打交道的呢
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4.读取本地文件(图片),发送到服务器
while (fileChannel.read(buffer) != -1) {
// 在读之前都要切换成读模式
buffer.flip();
socketChannel.write(buffer);
// 读完切换成写模式,能让管道继续读取文件的数据
buffer.clear();
}
// 5. 关闭流
fileChannel.close();
socketChannel.close();
}
}
```
**服务端**
```java
public class NoBlockServer {
public static void main(String[] args) throws IOException {
// 1.获取通道
ServerSocketChannel server = ServerSocketChannel.open();
// 2.切换成非阻塞模式
server.configureBlocking(false);
// 3. 绑定连接
server.bind(new InetSocketAddress(6666));
// 4. 获取选择器
Selector selector = Selector.open();
// 4.1将通道注册到选择器上,指定接收“监听通道”事件
server.register(selector, SelectionKey.OP_ACCEPT);
// 5. 轮训地获取选择器上已“就绪”的事件--->只要select()>0,说明已就绪
while (selector.select() > 0) {
// 6. 获取当前选择器所有注册的“选择键”(已就绪的监听事件)
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
// 7. 获取已“就绪”的事件,(不同的事件做不同的事)
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 接收事件就绪
if (selectionKey.isAcceptable()) {
// 8. 获取客户端的链接
SocketChannel client = server.accept();
// 8.1 切换成非阻塞状态
client.configureBlocking(false);
// 8.2 注册到选择器上-->拿到客户端的连接为了读取通道的数据(监听读就绪事件)
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) { // 读事件就绪
// 9. 获取当前选择器读就绪状态的通道
SocketChannel client = (SocketChannel) selectionKey.channel();
// 9.1读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 9.2得到文件通道,将客户端传递过来的图片写到本地项目下(写模式、没有则创建)
FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
while (client.read(buffer) > 0) {
// 在读之前都要切换成读模式
buffer.flip();
outChannel.write(buffer);
// 读完切换成写模式,能让管道继续读取文件的数据
buffer.clear();
}
}
// 10. 取消选择键(已经处理过的事件,就应该取消掉了)
iterator.remove();
}
}
}
}
```
还是刚才的需求:**服务端保存了图片以后,告诉客户端已经收到图片了**
在服务端上只要在后面写些数据给客户端就好了:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/network-connect-5ea2f6d3-be10-4703-aa99-bf36e30fab77.jpg)
在客户端上要想获取得到服务端的数据,也需要注册在register上(监听读事件)!
```java
public class NoBlockClient2 {
public static void main(String[] args) throws IOException {
// 1. 获取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));
// 1.1切换成非阻塞模式
socketChannel.configureBlocking(false);
// 1.2获取选择器
Selector selector = Selector.open();
// 1.3将通道注册到选择器中,获取服务端返回的数据
socketChannel.register(selector, SelectionKey.OP_READ);
// 2. 发送一张图片给服务端吧
FileChannel fileChannel = FileChannel.open(Paths.get("X:\\Users\\ozc\\Desktop\\新建文件夹\\1.png"), StandardOpenOption.READ);
// 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是与数据打交道的呢
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4.读取本地文件(图片),发送到服务器
while (fileChannel.read(buffer) != -1) {
// 在读之前都要切换成读模式
buffer.flip();
socketChannel.write(buffer);
// 读完切换成写模式,能让管道继续读取文件的数据
buffer.clear();
}
// 5. 轮训地获取选择器上已“就绪”的事件--->只要select()>0,说明已就绪
while (selector.select() > 0) {
// 6. 获取当前选择器所有注册的“选择键”(已就绪的监听事件)
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
// 7. 获取已“就绪”的事件,(不同的事件做不同的事)
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 8. 读事件就绪
if (selectionKey.isReadable()) {
// 8.1得到对应的通道
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
// 9. 知道服务端要返回响应的数据给客户端,客户端在这里接收
int readBytes = channel.read(responseBuffer);
if (readBytes > 0) {
// 切换读模式
responseBuffer.flip();
System.out.println(new String(responseBuffer.array(), 0, readBytes));
}
}
// 10. 取消选择键(已经处理过的事件,就应该取消掉了)
iterator.remove();
}
}
}
}
```
测试结果:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/network-connect-b69665b3-77f9-4f3d-9075-ffac1489637f.jpg)
下面就**简单总结一下**使用NIO时的要点:
* 将Socket通道注册到Selector中,监听感兴趣的事件
* 当感兴趣的时间就绪时,则会进去我们处理的方法进行处理
* 每处理完一次就绪事件,删除该选择键(因为我们已经处理完了)
## 4.4管道和DataGramChannel
这里我就不再讲述了,最难的TCP都讲了,UDP就很简单了。
UDP:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/network-connect-7fc34dd6-bab4-4c4a-af8a-6ab898c4b6e9.jpg)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/network-connect-d1e531f8-9638-4fd5-9f3a-70db2c25d92e.jpg)
管道:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/network-connect-86f3b103-0c1c-47fb-8364-e7ec3d91b8b1.jpg)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/network-connect-29d105c0-6525-4efc-912c-d85abd878e82.jpg)
>参考链接:[https://www.zhihu.com/question/29005375/answer/667616386](https://www.zhihu.com/question/29005375/answer/667616386),整理:沉默王二
---------
最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html)
关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png)
\ No newline at end of file
---
title: 一文彻底理解 Java NIO 核心组件
shortTitle: 一文彻底理解NIO核心组件
category:
- Java核心
tag:
- Java NIO
description: Java程序员进阶之路,小白的零基础Java教程,一文彻底理解 Java NIO 核心组件
head:
- - meta
- name: keywords
content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,Java IO,java NIO,nio
---
**同步、异步、阻塞、非阻塞**
首先,这几个概念非常容易搞混淆,但NIO中又有涉及,所以总结一下。
* 同步:API调用返回时调用者就知道操作的结果如何了(实际读取/写入了多少字节)。
* 异步:相对于同步,API调用返回时调用者不知道操作的结果,后面才会回调通知结果。
* 阻塞:当无数据可读,或者不能写入所有数据时,挂起当前线程等待。
* 非阻塞:读取时,可以读多少数据就读多少然后返回,写入时,可以写入多少数据就写入多少然后返回。
对于I/O操作,根据Oracle官网的文档,同步异步的划分标准是“调用者是否需要等待I/O操作完成”,这个“等待I/O操作完成”的意思不是指一定要读取到数据或者说写入所有数据,而是指真正进行I/O操作时,比如数据在TCP/IP协议栈缓冲区和JVM缓冲区之间传输的这段时间,调用者是否要等待。
所以,我们常用的 read() 和 write() 方法都是同步I/O,同步I/O又分为阻塞和非阻塞两种模式,如果是非阻塞模式,检测到无数据可读时,直接就返回了,并没有真正执行I/O操作。
总结就是,Java中实际上只有 同步阻塞I/O、同步非阻塞I/O 与 异步I/O 三种机制,我们下文所说的是前两种,JDK 1.7才开始引入异步 I/O,那称之为NIO.2。
## 传统IO
我们知道,一个新技术的出现总是伴随着改进和提升,Java NIO的出现亦如此。
传统 I/O 是阻塞式I/O,主要问题是系统资源的浪费。比如我们为了读取一个TCP连接的数据,调用 InputStream 的 read() 方法,这会使当前线程被挂起,直到有数据到达才被唤醒,那该线程在数据到达这段时间内,占用着内存资源(存储线程栈)却无所作为,也就是俗话说的占着茅坑不拉屎,为了读取其他连接的数据,我们不得不启动另外的线程。在并发连接数量不多的时候,这可能没什么问题,然而当连接数量达到一定规模,内存资源会被大量线程消耗殆尽。另一方面,线程切换需要更改处理器的状态,比如程序计数器、寄存器的值,因此非常频繁的在大量线程之间切换,同样是一种资源浪费。
随着技术的发展,现代操作系统提供了新的I/O机制,可以避免这种资源浪费。基于此,诞生了Java NIO,NIO的代表性特征就是非阻塞I/O。紧接着我们发现,简单的使用非阻塞I/O并不能解决问题,因为在非阻塞模式下,read()方法在没有读取到数据时就会立即返回,不知道数据何时到达的我们,只能不停的调用read()方法进行重试,这显然太浪费CPU资源了,从下文可以知道,Selector组件正是为解决此问题而生。
## Java NIO 核心组件
### 1.Channel
#### 概念
Java NIO中的所有I/O操作都基于Channel对象,就像流操作都要基于Stream对象一样,因此很有必要先了解Channel是什么。以下内容摘自JDK 1.8的文档
> A channel represents an open connection to an entity such as a
>
> hardware device, a file, a network socket, or a program component that
>
> is capable of performing one or more distinct I/O operations, for
>
> example reading or writing.
从上述内容可知,一个Channel(通道)代表和某一实体的连接,这个实体可以是文件、网络套接字等。也就是说,通道是Java NIO提供的一座桥梁,用于我们的程序和操作系统底层I/O服务进行交互。
通道是一种很基本很抽象的描述,和不同的I/O服务交互,执行不同的I/O操作,实现不一样,因此具体的有FileChannel、SocketChannel等。
通道使用起来跟Stream比较像,可以读取数据到Buffer中,也可以把Buffer中的数据写入通道。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/segmentfault-yiwrncdlxjavaniohxzjsegmentfaultsp-393f5b9a-8268-4177-ad19-f207b5064466.png)
当然,也有区别,主要体现在如下两点:
* 一个通道,既可以读又可以写,而一个Stream是单向的(所以分 InputStream 和 OutputStream)
* 通道有非阻塞I/O模式
#### 实现
Java NIO中最常用的通道实现是如下几个,可以看出跟传统的 I/O 操作类是一一对应的。
* FileChannel:读写文件
* DatagramChannel: UDP协议网络通信
* SocketChannel:TCP协议网络通信
* ServerSocketChannel:监听TCP连接
### 2.Buffer
NIO中所使用的缓冲区不是一个简单的byte数组,而是封装过的Buffer类,通过它提供的API,我们可以灵活的操纵数据,下面细细道来。
与Java基本类型相对应,NIO提供了多种 Buffer 类型,如ByteBuffer、CharBuffer、IntBuffer等,区别就是读写缓冲区时的单位长度不一样(以对应类型的变量为单位进行读写)。
Buffer中有3个很重要的变量,它们是理解Buffer工作机制的关键,分别是
* capacity (总容量)
* position (指针当前位置)
* limit (读/写边界位置)
Buffer的工作方式跟C语言里的字符数组非常的像,类比一下,capacity就是数组的总长度,position就是我们读/写字符的下标变量,limit就是结束符的位置。Buffer初始时3个变量的情况如下图
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/segmentfault-yiwrncdlxjavaniohxzjsegmentfaultsp-5f1a7222-15cf-41e0-a6c6-729aec5e0a97.png)
在对Buffer进行读/写的过程中,position会往后移动,而 limit 就是 position 移动的边界。由此不难想象,在对Buffer进行写入操作时,limit应当设置为capacity的大小,而对Buffer进行读取操作时,limit应当设置为数据的实际结束位置。(注意:将Buffer数据 写入 通道是Buffer 读取 操作,从通道 读取 数据到Buffer是Buffer 写入 操作)
在对Buffer进行读/写操作前,我们可以调用Buffer类提供的一些辅助方法来正确设置 position 和 limit 的值,主要有如下几个
* flip(): 设置 limit 为 position 的值,然后 position 置为0。对Buffer进行读取操作前调用。
* rewind(): 仅仅将 position
置0。一般是在重新读取Buffer数据前调用,比如要读取同一个Buffer的数据写入多个通道时会用到。
* clear(): 回到初始状态,即 limit 等于 capacity,position 置0。重新对Buffer进行写入操作前调用。
* compact(): 将未读取完的数据(position 与 limit 之间的数据)移动到缓冲区开头,并将 position
设置为这段数据末尾的下一个位置。其实就等价于重新向缓冲区中写入了这么一段数据。
然后,看一个实例,使用 FileChannel 读写文本文件,通过这个例子验证通道可读可写的特性以及Buffer的基本用法(注意 FileChannel 不能设置为非阻塞模式)。
```java
FileChannel channel = new RandomAccessFile("test.txt", "rw").getChannel();
channel.position(channel.size()); // 移动文件指针到末尾(追加写入)
ByteBuffer byteBuffer = ByteBuffer.allocate(20);
// 数据写入Buffer
byteBuffer.put("你好,世界!\n".getBytes(StandardCharsets.UTF_8));
// Buffer -> Channel
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
channel.write(byteBuffer);
}
channel.position(0); // 移动文件指针到开头(从头读取)
CharBuffer charBuffer = CharBuffer.allocate(10);
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
// 读出所有数据
byteBuffer.clear();
while (channel.read(byteBuffer) != -1 || byteBuffer.position() > 0) {
byteBuffer.flip();
// 使用UTF-8解码器解码
charBuffer.clear();
decoder.decode(byteBuffer, charBuffer, false);
System.out.print(charBuffer.flip().toString());
byteBuffer.compact(); // 数据可能有剩余
}
channel.close();
```
这个例子中使用了两个Buffer,其中 byteBuffer 作为通道读写的数据缓冲区,charBuffer 用于存储解码后的字符。clear() 和 flip() 的用法正如上文所述,需要注意的是最后那个 compact() 方法,即使 charBuffer 的大小完全足以容纳 byteBuffer 解码后的数据,这个 compact() 也必不可少,这是因为常用中文字符的UTF-8编码占3个字节,因此有很大概率出现在中间截断的情况,请看下图:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/segmentfault-yiwrncdlxjavaniohxzjsegmentfaultsp-3e69926a-d4aa-4e1a-ac3d-b699b5a9abe9.png)
当 Decoder 读取到缓冲区末尾的 0xe4 时,无法将其映射到一个 Unicode,decode()方法第三个参数 false 的作用就是让 Decoder 把无法映射的字节及其后面的数据都视作附加数据,因此 decode() 方法会在此处停止,并且 position 会回退到 0xe4 的位置。如此一来, 缓冲区中就遗留了“中”字编码的第一个字节,必须将其 compact 到前面,以正确的和后序数据拼接起来。
BTW,例子中的 CharsetDecoder 也是 Java NIO 的一个新特性,所以大家应该发现了一点哈,NIO的操作是面向缓冲区的(传统I/O是面向流的)。
至此,我们了解了 Channel 与 Buffer 的基本用法。接下来要说的是让一个线程管理多个Channel的重要组件。
### 3.Selector
#### Selector 是什么
Selector(选择器)是一个特殊的组件,用于采集各个通道的状态(或者说事件)。我们先将通道注册到选择器,并设置好关心的事件,然后就可以通过调用select()方法,静静地等待事件发生。
通道有如下4个事件可供我们监听:
* Accept:有可以接受的连接
* Connect:连接成功
* Read:有数据可读
* Write:可以写入数据了
#### 为什么要用Selector
前文说了,如果用阻塞I/O,需要多线程(浪费内存),如果用非阻塞I/O,需要不断重试(耗费CPU)。Selector的出现解决了这尴尬的问题,非阻塞模式下,通过Selector,我们的线程只为已就绪的通道工作,不用盲目的重试了。比如,当所有通道都没有数据到达时,也就没有Read事件发生,我们的线程会在select()方法处被挂起,从而让出了CPU资源。
#### 使用方法
如下所示,创建一个Selector,并注册一个Channel。
注意:要将 Channel 注册到 Selector,首先需要将 Channel 设置为非阻塞模式,否则会抛异常。
```java
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
```
register()方法的第二个参数名叫“interest set”,也就是你所关心的事件集合。如果你关心多个事件,用一个“按位或运算符”分隔,比如
```java
SelectionKey.OP_READ | SelectionKey.OP_WRITE复制代码
```
这种写法一点都不陌生,支持位运算的编程语言里都这么玩,用一个整型变量可以标识多种状态,它是怎么做到的呢,其实很简单,举个例子,首先预定义一些常量,它们的值(二进制)如下
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/segmentfault-yiwrncdlxjavaniohxzjsegmentfaultsp-7a1acc85-7b5b-45d3-996d-79f39b61523d.png)
可以发现,它们值为1的位都是错开的,因此对它们进行按位或运算之后得出的值就没有二义性,可以反推出是由哪些变量运算而来。怎么判断呢,没错,就是“按位与”运算。比如,现在有一个状态集合变量值为 0011,我们只需要判断 “0011 & OP\_READ” 的值是 1 还是 0 就能确定集合是否包含 OP\_READ 状态。
然后,注意 register() 方法返回了一个SelectionKey的对象,这个对象包含了本次注册的信息,我们也可以通过它修改注册信息。从下面完整的例子中可以看到,select()之后,我们也是通过获取一个 SelectionKey 的集合来获取到那些状态就绪了的通道。
## 一个完整实例
概念和理论的东西阐述完了(其实写到这里,我发现没写出多少东西,好尴尬(⊙ˍ⊙)),看一个完整的例子吧。
这个例子使用Java NIO实现了一个单线程的服务端,功能很简单,监听客户端连接,当连接建立后,读取客户端的消息,并向客户端响应一条消息。
需要注意的是,我用字符 ‘0′(一个值为0的字节) 来标识消息结束。
### 单线程Server
```java
public class NioServer {
public static void main(String[] args) throws IOException {
// 创建一个selector
Selector selector = Selector.open();
// 初始化TCP连接监听通道
ServerSocketChannel listenChannel = ServerSocketChannel.open();
listenChannel.bind(new InetSocketAddress(9999));
listenChannel.configureBlocking(false);
// 注册到selector(监听其ACCEPT事件)
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
// 创建一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(100);
while (true) {
selector.select(); //阻塞,直到有监听的事件发生
Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
// 通过迭代器依次访问select出来的Channel事件
while (keyIter.hasNext()) {
SelectionKey key = keyIter.next();
if (key.isAcceptable()) { // 有连接可以接受
SocketChannel channel = ((ServerSocketChannel) key.channel()).accept();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
System.out.println("与【" + channel.getRemoteAddress() + "】建立了连接!");
} else if (key.isReadable()) { // 有数据可以读取
buffer.clear();
// 读取到流末尾说明TCP连接已断开,
// 因此需要关闭通道或者取消监听READ事件
// 否则会无限循环
if (((SocketChannel) key.channel()).read(buffer) == -1) {
key.channel().close();
continue;
}
// 按字节遍历数据
buffer.flip();
while (buffer.hasRemaining()) {
byte b = buffer.get();
if (b == 0) { // 客户端消息末尾的\0
System.out.println();
// 响应客户端
buffer.clear();
buffer.put("Hello, Client!\0".getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
((SocketChannel) key.channel()).write(buffer);
}
} else {
System.out.print((char) b);
}
}
}
// 已经处理的事件一定要手动移除
keyIter.remove();
}
}
}
}
```
### Client
这个客户端纯粹测试用,为了看起来不那么费劲,就用传统的写法了,代码很简短。
要严谨一点测试的话,应该并发运行大量Client,统计服务端的响应时间,而且连接建立后不要立刻发送数据,这样才能发挥出服务端非阻塞I/O的优势。
```java
public class Client {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("localhost", 9999);
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
// 先向服务端发送数据
os.write("Hello, Server!\0".getBytes());
// 读取服务端发来的数据
int b;
while ((b = is.read()) != 0) {
System.out.print((char) b);
}
System.out.println();
socket.close();
}
}
```
### NIO vs IO
学习了NIO之后我们都会有这样一个疑问:到底什么时候该用NIO,什么时候该用传统的I/O呢?
其实了解他们的特性后,答案还是比较明确的,NIO擅长1个线程管理多条连接,节约系统资源,但是如果每条连接要传输的数据量很大的话,因为是同步I/O,会导致整体的响应速度很慢;而传统I/O为每一条连接创建一个线程,能充分利用处理器并行处理的能力,但是如果连接数量太多,内存资源会很紧张。
总结就是:连接数多数据量小用NIO,连接数少用I/O(写起来也简单- -)。
## Next
经过NIO核心组件的学习,了解了非阻塞服务端实现的基本方法。然而,细心的你们肯定也发现了,上面那个完整的例子,实际上就隐藏了很多问题。比如,例子中只是简单的将读取到的每个字节输出,实际环境中肯定是要读取到完整的消息后才能进行下一步处理,由于NIO的非阻塞特性,一次可能只读取到消息的一部分,这已经很糟糕了,如果同一条连接会连续发来多条消息,那不仅要对消息进行拼接,还需要切割,同理,例子中给客户端响应的时候,用了个while()循环,保证数据全部write完成再做其它工作,实际应用中为了性能,肯定不会这么写。另外,为了充分利用现代处理器多核心并行处理的能力,应该用一个线程组来管理这些连接的事件。
要解决这些问题,需要一个严谨而繁琐的设计,不过幸运的是,我们有开源的框架可用,那就是优雅而强大的Netty,Netty基于Java NIO,提供异步调用接口,开发高性能服务器的一个很好的选择,之前在项目中使用过,但没有深入学习,打算下一步好好学学它,到时候再写一篇笔记。
Java NIO设计的目标是为程序员提供API以享受现代操作系统最新的I/O机制,所以覆盖面较广,除了文中所涉及的组件与特性,还有很多其它的,比如 Pipe(管道)、Path(路径)、Files(文件) 等,有的是用于提升I/O性能的新组件,有的是简化I/O操作的工具,具体用法可以参看最后 References 里的链接。
>参考链接:[https://segmentfault.com/a/1190000017040893](https://segmentfault.com/a/1190000017040893),整理:沉默王二
---------
最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html)
关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png)
\ No newline at end of file
---
title: Java NIO 快速入门(buffer缓冲区、Channel管道、Selector选择器)
shortTitle: Java NIO快速入门
category:
- Java核心
tag:
- Java NIO
description: Java程序员进阶之路,小白的零基础Java教程,Java NIO 快速入门(buffer缓冲区、Channel管道、Selector选择器)
head:
- - meta
- name: keywords
content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,nio,buffer,channel,selector
---
首先我们来看看**IO和NIO的区别**
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-90c84f53-f82d-43dd-87c5-4477e540fa57.jpg)
* 可简单认为:**IO是面向流的处理,NIO是面向块(缓冲区)的处理**
* 面向流的I/O 系统**一次一个字节地处理数据**
* 一个面向块(缓冲区)的I/O系统**以块的形式处理数据**
NIO主要有**三个核心部分组成**
* **buffer缓冲区**
* **Channel管道**
* **Selector选择器**
## buffer缓冲区和Channel管道
在NIO中并不是以流的方式来处理数据的,而是以buffer缓冲区和Channel管道**配合使用**来处理数据。
简单理解一下:
* Channel管道比作成铁路,buffer缓冲区比作成火车(运载着货物)
而我们的NIO就是**通过Channel管道运输着存储数据的Buffer缓冲区的来实现数据的处理**
* 要时刻记住:Channel不与数据打交道,它只负责运输数据。与数据打交道的是Buffer缓冲区
* **Channel-->运输**
* **Buffer-->数据**
相对于传统IO而言,**流是单向的**。对于NIO而言,有了Channel管道这个概念,我们的**读写都是双向**的(铁路上的火车能从广州去北京、自然就能从北京返还到广州)!
### buffer缓冲区核心要点
我们来看看Buffer缓冲区有什么值得我们注意的地方。
Buffer是缓冲区的抽象类:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-436aa175-3586-4457-b93c-70b21ff122dc.jpg)
其中ByteBuffer是**用得最多的实现类**(在管道中读写字节数据)。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-4bf73cdc-b5e2-4866-ac68-cc57602be5e8.jpg)
拿到一个缓冲区我们往往会做什么?很简单,就是**读取缓冲区的数据/写数据到缓冲区中**。所以,缓冲区的核心方法就是:
* put()
* get()
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-7229ef4c-a27d-4f90-97d0-8abbfda810a0.jpg)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-df9f0bdf-3afe-42dc-9e7e-459484d7cb8e.jpg)
Buffer类维护了4个核心变量属性来提供**关于其所包含的数组的信息**。它们是:
* 容量Capacity
* **缓冲区能够容纳的数据元素的最大数量**。容量在缓冲区创建时被设定,并且永远不能被改变。(不能被改变的原因也很简单,底层是数组嘛)
* 上界Limit
* **缓冲区里的数据的总数**,代表了当前缓冲区中一共有多少数据。
* 位置Position
* **下一个要被读或写的元素的位置**。Position会自动由相应的 `get( )``put( )`函数更新。
* 标记Mark
* 一个备忘位置。**用于记录上一次读写的位置**
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-85991ca3-99bd-4e56-a84e-e58af4d8aac9.jpg)
### buffer代码演示
首先展示一下**是如何创建缓冲区的,核心变量的值是怎么变化的**
```java
public static void main(String[] args) {
// 创建一个缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 看一下初始时4个核心变量的值
System.out.println("初始时-->limit--->"+byteBuffer.limit());
System.out.println("初始时-->position--->"+byteBuffer.position());
System.out.println("初始时-->capacity--->"+byteBuffer.capacity());
System.out.println("初始时-->mark--->" + byteBuffer.mark());
System.out.println("--------------------------------------");
// 添加一些数据到缓冲区中
String s = "沉默王二";
byteBuffer.put(s.getBytes());
// 看一下初始时4个核心变量的值
System.out.println("put完之后-->limit--->"+byteBuffer.limit());
System.out.println("put完之后-->position--->"+byteBuffer.position());
System.out.println("put完之后-->capacity--->"+byteBuffer.capacity());
System.out.println("put完之后-->mark--->" + byteBuffer.mark());
}
```
运行结果:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-cbf8b617-f71c-47f7-bc20-72d46306349f.jpg)
现在**我想要从缓存区拿数据**,怎么拿呀??NIO给了我们一个`flip()`方法。这个方法可以**改动position和limit的位置**
还是上面的代码,我们`flip()`一下后,再看看4个核心属性的值会发生什么变化:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-571a843f-1334-4fcb-bbae-90dbbe31ac8c.jpg)
很明显的是:
* **limit变成了position的位置了**
* **而position变成了0**
看到这里的同学可能就会想到了:当调用完`filp()`时:**limit是限制读到哪里,而position是从哪里读**
一般我们称`filp()`**“切换成读模式”**
* 每当要从缓存区的时候读取数据时,就调用`filp()`**“切换成读模式”**
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-b7d1a7d4-f2a7-4635-b5a8-9d10733df5f3.jpg)
切换成读模式之后,我们就可以读取缓冲区的数据了:
```java
// 创建一个limit()大小的字节数组(因为就只有limit这么多个数据可读)
byte[] bytes = new byte[byteBuffer.limit()];
// 将读取的数据装进我们的字节数组中
byteBuffer.get(bytes);
// 输出数据
System.out.println(new String(bytes, 0, bytes.length));
```
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-40d60cff-e87b-4180-a350-7dc5a5207156.jpg)
随后输出一下核心变量的值看看:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-c0fc49ea-bc74-43e8-8f16-d26b93e731bf.jpg)
**读完我们还想写数据到缓冲区**,那就使用`clear()`函数,这个函数会“清空”缓冲区:
* 数据没有真正被清空,只是被**遗忘**掉了
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-6567241c-8ca6-492d-a4d1-45e6b275e75e.jpg)
### FileChannel通道核心要点
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-275c9588-216a-416f-934e-f3fbe54fda43.jpg)
Channel通道**只负责传输数据、不直接操作数据的**。操作数据都是通过Buffer缓冲区来进行操作!
```java
// 1. 通过本地IO的方式来获取通道
FileInputStream fileInputStream = new FileInputStream("F:\\3yBlog\\JavaEE常用框架\\Elasticsearch就是这么简单.md");
// 得到文件的输入通道
FileChannel inchannel = fileInputStream.getChannel();
// 2. jdk1.7后通过静态方法.open()获取通道
FileChannel.open(Paths.get("F:\\3yBlog\\JavaEE常用框架\\Elasticsearch就是这么简单2.md"), StandardOpenOption.WRITE);
```
使用**FileChannel配合缓冲区**实现文件复制的功能:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-741d14cb-4ea6-43cb-aacc-4fa3297cedba.jpg)
使用**内存映射文件**的方式实现**文件复制**的功能(直接操作缓冲区):
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-c8020177-39b4-405b-abc0-c908ab7cf73d.jpg)
通道之间通过`transfer()`实现数据的传输(直接操作缓冲区):
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-2ce868f7-691e-419e-a443-e25131b2785a.jpg)
### 直接与非直接缓冲区
* 非直接缓冲区是**需要**经过一个:copy的阶段的(从内核空间copy到用户空间)
* 直接缓冲区**不需要**经过copy阶段,也可以理解成--->**内存映射文件**,(上面的图片也有过例子)。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-c51af71c-759c-40de-9d92-92fffa2d075d.jpg)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-c6181e11-8960-46f3-a4b5-8233d013499c.jpg)
使用直接缓冲区有两种方式:
* 缓冲区创建的时候分配的是直接缓冲区
* 在FileChannel上调用`map()`方法,将文件直接映射到内存中创建
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-16943811-2190-4fc9-82f9-df34c06c22d2.jpg)
### scatter和gather、字符集
这个知识点我感觉用得挺少的,不过很多教程都有说这个知识点,我也拿过来说说吧:
* 分散读取(scatter):将一个通道中的数据分散读取到多个缓冲区中
* 聚集写入(gather):将多个缓冲区中的数据集中写入到一个通道中
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-d2b8a337-3c1b-4bce-ae8d-ed107a3676a2.jpg)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-e0916f2c-2ce9-4be6-b071-754301a09642.jpg)
分散读取
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-886e1838-3404-4bfb-84c9-36cffa19aa19.jpg)
聚集写入
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-aba6d233-f294-4d1f-b389-dd174e76d1b0.jpg)
字符集(只要编码格式和解码格式一致,就没问题了)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/rumen-dba55dfc-48df-4111-884d-d67227b7723a.jpg)
>参考链接:[https://www.zhihu.com/question/29005375/answer/667616386](https://www.zhihu.com/question/29005375/answer/667616386),整理:沉默王二
---------
最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html)
关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png)
---
title: 为什么我们要使用 Java NIO?
shortTitle: 为什么我们要使用Java NIO?
category:
- Java核心
tag:
- Java NIO
description: Java程序员进阶之路,小白的零基础Java教程,为什么我们要使用 Java NIO?
head:
- - meta
- name: keywords
content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java入门,教程,nio
---
我花了几天去了解**NIO的核心知识点**,期间看了《Java 编程思想》和《疯狂Java 讲义》的nio模块。**但是**,会发现看完了之后还是很**迷**,不知道NIO这是干嘛用的,而网上的资料与书上的知识点没有很好地对应。
* 网上的资料很多都以IO的五种模型为基础来讲解NIO,而IO这五种模型其中又涉及到了很多概念:`同步/异步/阻塞/非阻塞/多路复用`**而不同的人又有不同的理解方式**
* 还有涉及到了unix的`select/epoll/poll/pselect``fd`这些关键字,没有相关基础的人看起来简直是天书
* 这就导致了在初学时认为nio远不可及
我在找资料的过程中也收藏了好多讲解NIO的资料,这篇文章就是**以初学的角度来理解NIO**。也算是我这两天看NIO的一个总结吧。
* 希望大家可以看了之后知道什么是NIO,NIO的核心知识点是什么,会使用NIO~
那么接下来就开始吧,如果文章有错误的地方请大家多多包涵,不吝在评论区指正哦~
> 声明:本文使用JDK1.8
JDK 1.4中的`java.nio.*包`中引入新的Java I/O库,其目的是**提高速度**。实际上,“旧”的I/O包已经使用NIO**重新实现过,即使我们不显式的使用NIO编程,也能从中受益**
* nio翻译成 no-blocking io 或者 new io 都无所谓啦,都说得通~
在《Java编程思想》读到**“即使我们不显式的使用NIO编程,也能从中受益”**的时候,我是挺在意的,所以:我们**测试**一下使用NIO复制文件和传统IO复制文件的性能:
```java
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class SimpleFileTransferTest {
private long transferFile(File source, File des) throws IOException {
long startTime = System.currentTimeMillis();
if (!des.exists())
des.createNewFile();
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(source));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(des));
//将数据源读到的内容写入目的地--使用数组
byte[] bytes = new byte[1024 * 1024];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
long endTime = System.currentTimeMillis();
return endTime - startTime;
}
private long transferFileWithNIO(File source, File des) throws IOException {
long startTime = System.currentTimeMillis();
if (!des.exists())
des.createNewFile();
RandomAccessFile read = new RandomAccessFile(source, "rw");
RandomAccessFile write = new RandomAccessFile(des, "rw");
FileChannel readChannel = read.getChannel();
FileChannel writeChannel = write.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 1024);//1M缓冲区
while (readChannel.read(byteBuffer) > 0) {
byteBuffer.flip();
writeChannel.write(byteBuffer);
byteBuffer.clear();
}
writeChannel.close();
readChannel.close();
long endTime = System.currentTimeMillis();
return endTime - startTime;
}
public static void main(String[] args) throws IOException {
SimpleFileTransferTest simpleFileTransferTest = new SimpleFileTransferTest();
File sourse = new File("F:\\电影\\[电影天堂www.dygod.cn]猜火车-cd1.rmvb");
File des = new File("X:\\Users\\ozc\\Desktop\\io.avi");
File nio = new File("X:\\Users\\ozc\\Desktop\\nio.avi");
long time = simpleFileTransferTest.transferFile(sourse, des);
System.out.println(time + ":普通字节流时间");
long timeNio = simpleFileTransferTest.transferFileWithNIO(sourse, nio);
System.out.println(timeNio + ":NIO时间");
}
}
```
我分别测试了文件大小为13M,40M,200M的:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/why-d5118350-471f-4998-abb2-4e82c7a50344.jpg)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/why-ffcb8770-5f0a-41e9-8534-f92a6f931a49.jpg)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nio/why-0425087f-7878-466b-b02a-a802444e7405.jpg)
为什么要使用NIO?
可以看到使用过NIO重新实现过的**传统IO根本不虚**,在大文件下效果还比NIO要好(当然了,个人几次的测试,或许不是很准)
* 而NIO要有一定的学习成本,也没有传统IO那么好理解。
那这意味着我们**可以不使用/学习NIO了吗**
答案是**否定**的,IO操作往往在**两个场景**下会用到:
* 文件IO
* 网络IO
NIO的**魅力:在网络中使用IO就可以体现出来了**
* 后面会说到网络中使用NIO,不急哈~
>参考链接:[https://www.zhihu.com/question/29005375/answer/667616386](https://www.zhihu.com/question/29005375/answer/667616386),整理:沉默王二
---------
最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html)
关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png)
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册