☀️ JAVA NIO? 看这一篇就够了!! ☀️
Posted 经理,天台风好大
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了☀️ JAVA NIO? 看这一篇就够了!! ☀️相关的知识,希望对你有一定的参考价值。
文章目录
一、NIO 简介
1.1 NIO 概述
Java NIO(New IO)是从 Java 1.4 版本开始引入的一组新的 IO API(其核心构成有 Channels,Buffers,Selectors 三部分),目的主要是基于这组 API 改善 IO操作性能。
1.2 NIO&IO 分析
1.2.1 IO 操作流程
对于一个 network IO (这里我们以 read 举例),它会涉及到两个系统对象,一个是调用这个 IO 的 process (or thread),另一个就是系统内核(kernel)。当一个 read 操作发生时,该操作会经历两个阶段:
- 将数据拷贝到操作系统内核 Buffer。
- 将操作系统内核数据拷贝到用户进程。
1.2.2 面向流与面向缓冲区
Java NIO 和 IO 之间第一个最大的区别是:IO 是面向流的,NIO 是面向缓冲区的。例如:
-
面向流的操作(输入&输出流操作是单向的)
-
面向缓冲区的操作:(操作是双向且可移动指针)
说明:
面向缓冲区的操作时,是缓冲区中的读写模式进行操作,写模式用于向缓冲区写数据,读模式用于从缓冲区读。
1.2.3 阻塞与非阻塞
阻塞和非阻塞的概念描述的是用户线程调用内核 IO 操作的方式。
-
阻塞: 是指调用操作需要等待结果的完成,同时会影响后操作的执行。例如阻塞式 IO 操作,用户线程会在内核等待数据以及拷贝数据期间都处于阻塞状态。
整个 IO 请求的过程中,用户线程是被阻塞的,这导致用户在发起 IO 请求时,不能做任何事情,对 CPU 的资源利用率不够。话外音:小李去火车站买票,排队两天买到一张票。
-
非阻塞: 是指 IO 操作被调用后立即返回给用户一个状态值,无需等到 IO 操作彻底完成。例如:
虽然用户线程每次发起 IO 请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的 CPU 的资源。话外音:小李去火车站买票,火车站没票,然后每隔 3 小时去火车站问有没有人退票,两天天后买到一张票
为了避免同步非阻塞 IO 模型中轮询等待的问题,基于内核提供的多路分离函数 select(),可以实现 IO 的多路复用,例如:
使用 select 以后最大的优势是用户可以在一个线程内同时处理多个 socket的 IO 请求。用户可以注册多个 socket,然后不断地调用 select 读取被激活的socket,即可达到在同一个线程内同时处理多个 IO 请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。话外音:小李去火车站买票,委托黄牛,然后每隔 1 小时电话黄牛询问,黄牛两天内买到票,然后老李去火车站交钱领票。
这里的 select 函数是阻塞的,因此多路 IO 复用模型也被称为异步阻塞 IO模型。注意,这里的所说的阻塞是指 select 函数执行时线程被阻塞,而不是指socket。
1.2.4. 同步与异步
同步和异步的概念描述的是用户线程与内核的交互方式。
-
同步是指用户线程发起 IO 请求后需要等待或者轮询内核 IO 操作完成后才能继续执行。
说明: 这种用户线程轮询的改进方式是 IO 多路复用的实现。 -
异步是指用户线程发起 IO 请求后仍继续执行,当内核 IO 操作完成后会通知用户线程,或者调用用户线程注册的回调函数,例如:
说明:异步 IO 的实现需要操作系统的支持,目前系统对异步 IO 的支持并非特别完善,更多的是采用 IO 多路复用模型模拟异步 IO 的方式。
话外音:小李去火车站买票,给售票员留下电话,有票后,售票员电话通知小李并快递送票上门
二、Buffer 基本应用
2.1 Buffer 概述
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。
Java NIO 里关键的 Buffer 实现:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
这些 Buffer 覆盖了你能通过 IO 发送的基本数据类型:byte,short,int,long,float,double 和 char。
说明: 实际的项目中的物理 I/O 操作相比与内存操作要慢数千倍,所以一般为了提高应用程序的性能通常会对数据进行缓存。我们的 JAVA 应用程序和物理磁盘间通常会有多级缓存,例如:
其中:
- Disk Drive Buffer(磁盘缓存): 位于磁盘驱动器中的 RAM,将磁盘数据移
动到磁盘缓冲区是一件相当耗时的操作。 - OS Buffer(系统缓存): 操作系统自己缓存,可以在应用程序间共享数据
- Application Buffer(应用缓存): 应用程序的私有缓存(JVM进程)。
2.2 Buffer 基本应用
使用 Buffer 读写数据一般遵循以下四个步骤:
- 写入数据到 Buffer
- 调用
flip()
方法(切换模式) - 从 Buffer 中读取数据
- 调用
clear()
方法或者compact()
方法
当向 buffer 写入数据时,buffer 会记录下写了多少数据。一旦要读取数据,需要通过 flip()方法将 Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到 buffer 的所有数据。
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用 clear()
或 compact()
方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
@Test
public void testBuffer01(){
//构建一个缓冲区对象(在JVM内存中分配一块区域)
ByteBuffer buf=ByteBuffer.allocate(1024);
System.out.println("===数据写入前===");
doPrint(buf.position(),buf.limit(),buf.capacity());
//向缓冲区写入数据
byte []data="hello".getBytes();
buf.put(data);//放入缓冲区
System.out.println("===数据写入后===");
doPrint(buf.position(),buf.limit(),buf.capacity());
//切换模式(底层同时会移动指针,position位置会发生变换)
buf.flip();
System.out.println("===读数据之前===");
doPrint(buf.position(),buf.limit(),buf.capacity());
byte c1=buf.get();
System.out.println((char)c1);
System.out.println("===读数据之后===");
doPrint(buf.position(),buf.limit(),buf.capacity());
}
private void doPrint(int pos,int limit,int cap){
System.out.println("position:"+pos);
System.out.println("limit:"+limit);
System.out.println("capacity:"+cap);
}
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。
为了理解 Buffer 的工作原理,需要熟悉它的三个属性:
- capacity 容量
- position 位置
- limit 限制
position 和 limit 的含义取决于 Buffer 处在读模式还是写模式。不管 Buffer处在什么模式,capacity 的含义总是一样的。
Capacity
作为一个内存块,Buffer 有一个固定的大小值,也叫“capacity”.你只能往里写 capacity 个 byte、long,char 等类型数据。一旦 Buffer 满了,需要将其清空(通过读数据或者清除数据)才能继续往里写数据。
position
当你写数据到 Buffer 中时,position 表示当前的位置。初始的 position 值 为 0。当一个 byte、long 等数据写到 Buffer 后, position 会向前移动到下一个可插入数据的 Buffer 单元。position 最大可为 capacity – 1.
当读取数据时,也是从某个特定位置读。当将 Buffer 从写模式切换到读模式,position 会被重置为 0. 当从 Buffer 的 position 处读取数据时,position向前移动到下一个可读的位置。
limit
在写模式下,Buffer 的 limit 表示你最多能往 Buffer 里写多少数据。 写模式下,limit 等于 Buffer 的 capacity。
当切换 Buffer 到读模式时, limit 表示你最多能读到多少数据。因此,当切换 Buffer 到读模式时,limit 会被设置成写模式下的 position 值。换句话说,你能读到之前写入的所有数据(limit 被设置成已写数据的数量,这个值在写模式下就是 position)
三、Channel 基本应用
3.1 Channel 概述
NIO 是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。如图所示:
NIO 中 Channel 的一些具体实现类有:
- FileChannel :从文件中读写数据。
- DatagramChannel :能通过 UDP 读写网络中的数据。
- SocketChannel :能通过 TCP 读写网络中的数据。
- ServerSocketChannel 可以监听新进来的 TCP 连接,像 Web 服务器那样。
正如你所看到的,这些通道涵盖了 UDP 和 TCP 网络 IO,以及文件 IO。
3.2 FileChannel 基本应用
借助 Channel 对象(FileChannel 类型),从文件读取数据。代码示例:
案例1:
@Test
public void testFileChannel()throws Exception{
//构建一个Buffer对象(缓冲区):JVM内存
ByteBuffer buf=ByteBuffer.allocate(1024);
//构建一个文件通道对象(可以读写数据)
FileChannel fChannel=FileChannel.open(Paths.get("data.txt"),StandardOpenOption.READ);//读模式
//将文件内容读到缓冲区(ByteBuffer)
fChannel.read(buf);
System.out.println("切换buf模式,开始从buf读数据");
System.out.println(buf.position());
//从Buffer中取数据
buf.flip();
System.out.println(buf.position());
System.out.println(new String(buf.array()));
buf.clear();//不是是清除,而将数据标记为脏数据(无用数据)
//释放资源
fChannel.close();
}
案例2:
@Test
public void testFileChannel()throws Exception{
//构建一个Buffer对象(缓冲区):JVM内存
ByteBuffer buf=ByteBuffer.allocate(2);
//构建一个文件通道对象(可以读写数据)
FileChannel fChannel=FileChannel.open(Paths.get("data.txt"),StandardOpenOption.READ);//读模式
//将文件内容读到缓冲区(ByteBuffer)
int len=-1;
do{
len=fChannel.read(buf);
System.out.println("切换buf模式,开始从buf读数据");
buf.flip();
//判定缓冲区中是否有剩余数据
while(buf.hasRemaining()){
System.out.print((char)buf.get());//每次都1个字节
}
System.out.println();
buf.flip();
buf.clear();//每次读数据应将原数据设置为无效。
}while(len!=-1);
//释放资源
fChannel.close();
}
3.3 SocketChanel 基本应用
Java NIO 中的 SocketChannel 是一个连接到 TCP 网络另一端的通道。可以通过以下 2 种方式创建 SocketChannel:
- 客户端打开一个 SocketChannel 并连接到互联网上的某台服务器。
- 服务端一个新连接到达 ServerSocketChannel 时,会创建一个SocketChannel。
基于 channel 实现通讯
代码示例:
package com.java.nio;
import org.junit.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
/**
* @author archibald
* @date 2021/9/6
* @apiNote
*/
public class TestSocketChannel01 {
/**
* 创建服务
*/
@Test
public void testServerSocket() throws Exception {
//1.创建ServerSocketChannel对象
ServerSocketChannel ssc = ServerSocketChannel.open();
//2. 让对象在指定端口上进行监听
ssc.socket().bind(new InetSocketAddress(8090));
//3.构建一个buffer对象,用于存储读取的数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
//4.等待客户端的连接
while (true) {
SocketChannel socketChannel = ssc.accept();
System.out.println("有客户端来了~~~");
socketChannel.read(buffer);
buffer.flip();
System.out.println("Server: " + new String(buffer.array()));
socketChannel.close();
}
}
/**
* 创建客户端
*/
@Test
public void testClientSocket() throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(8090));
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello server".getBytes());
buffer.flip();
socketChannel.write(buffer);
socketChannel.close();
}
}
四、Selector 基本应用
4.1 Selector 概述
Selector 是 Java NIO 中实现多路复用技术的关键,多路复用技术又是提高通讯性能的一个重要因素。项目中可以基于 selector 对象实现了一个线程管理多个 channel 对象,多个网络链接的目的。
例如:在一个单线程中使用一个Selector 处理 3 个 Channel,如图所示:
为什么使用 Selector?
仅用单个线程来处理多个 Channel 的好处是:只用一个线程处理所有的通道,可以有效避免线程之间上下文切换带来的开销,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。
4.2 Selector 基本应用
Selector 的创建
通过调用 Selector.open()方法创建一个 Selector,如下:
Selector selector = Selector.open();
向 Selector 注册通道
为了将 Channel 和 Selector 配合使用,必须将 channel 注册到 selector 上。
通过 SelectableChannel.register()方法来实现,如下:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
channel 与 Selector 一起使用时,Channel 必须处于非阻塞模式下。这意味着不能将 FileChannel 与 Selector 一起使用,因为 FileChannel 不能切换到非阻塞模式。而套接字通道都可以。
注意 register()
方法的第二个参数。这是一个“interest 集合”,意思是在通过 Selector 监听 Channel 时对什么事件感兴趣。可以监听四种不同类型的事件:
- Connect
- Accept
- Read
- Write
通道触发了一个事件意思是该事件已经就绪。所以,某个 channel 成功连接到另一个服务器称为“连接就绪”。一个 server socket channel 准备好接收新进入的连接称为“接收就绪”。一个有数据可读的通道可以说是“读就绪”。等待写数据的通道可以说是“写就绪”。
这四种事件用 SelectionKey 的四个常量来表示:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
代码示例如下:
服务端实现:
//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
ssChannel.configureBlocking(false);
//3. 绑定连接
ssChannel.bind(new InetSocketAddress(9898));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上, 并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
//6. 轮询式的获取选择器上已经“准备就绪”的事件
while(selector.select() > 0){
//7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while(it.hasNext()){
//8. 获取准备“就绪”的是事件
SelectionKey sk = it.next();
//9. 判断具体是什么事件准备就绪
if(sk.isAcceptable()){
//10. 若“接收就绪”,获取客户端连接
SocketChannel sChannel = ssChannel.accept();
//11. 切换非阻塞模式
sChannel.configureBlocking(false);
//12. 将该通道注册到选择器上
sChannel.register(selector, SelectionKey.OP_READ);
}else if(sk.isReadable()){
//13. 获取当前选择器上“读就绪”状态的通道
SocketChannel sChannel = (SocketChannel) sk.channel();
//14. 读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while((len = sChannel.read(buf)) > 0 ){
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
}
//15. 取消选择键 SelectionKey
it.remove();
}
}
客户端实现:
//1. 获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
//2. 切换非阻塞模式
sChannel.configureBlocking(false);
//3. 分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//4. 发送数据给服务端
Scanner scan = new Scanner(System.in);
while(scan.hasNext()){
String str = scan.next();
buf.put((new Date().toString() + "\\n" + str).getBytes());
buf.flip();
sChannel.write(buf);
buf.clear();
}
//5. 关闭通道
sChannel.close();
程序运行分析如下:
五、Tomcat 中的 NIO 应用
5.1 Tomcat 核心架构
Tomcat 是一个 apache 推出的一个 web 应用服务器,核心功能就是解析 Http 协议,处理网络 IO 操作,执行 Servlet 对象,其简易架构如下:
其中:
- Server:代表整个容器,它可能包含一个或多个 Service 和全局的对象资源;
- Service:包含一个或多个 Connector 和一个 Engine,这些 Connector 和 Engine 相关联;
- Connector:处理与客户端的通信,网络 I/O 操作;
- Engine:表示请求处理流水线(pipeline),它接收所有连接器的请求,并将响应交给适当的连接器返回给客户端;
- Host:网络名称(域名)与 Tomcat 服务器的关联,默认主机名 localhost,一个 Engine 可包含多个 Host;
- Context:表示一个 Web 应用程序,一个 Host 包含多个上下文。
- …
5.2 Tomcat 中的 NIO 应用配置
Tomcat 中的 NIO 应用要从 Connector 说起,Connector 是请求接收环节与请求处理环节的连接器。具体点说,就是 Connector 将接收到的请求传递给 Tomcat 引擎(Engine)进行处理,引擎处理完成以后会交给 Connector 将其响应到客户端。但是 Connector 本身并不会读写网络中的数据,读写网络中的数据还是要基于网络 IO 进行实现。但使用哪种 IO 模型需要由 Connector 对象进
行指定。
例如:可在 tomcat 的 server.xml 进行配置,其默认配置如下:
<Connector connectionTimeout="20000"
port="8080"
protocolJava NIO看这一篇就够了