IO
io定义
IO (Input/Output,输入/输出,其定义是站在CPU和内存的角度进行定义、研究),即数据的读入或写出操作,通常用户进程中的一个完整IO分为两阶段:用户进程空间至内核空间、内核空间至设备空间(磁盘、网络等)。IO主要分为网络IO和磁盘IO两种。理解系统IO是理解java 相关io模型的基础。
磁盘io
磁盘I/O的访问方式又包括以下几种:缓存I/O、直接I/O以及内存映射
缓存 i/o
上图,操作系统将内存分为两部分,分别为内核部分与用户部分,其容量大小比值为1:3,内核由操作系统直接控制使用,用户部分由操作系统分配给应用程序使用,我们应用程序通常使用user这一部分。kennel buffer是内核中分配给磁盘做缓存的内存块,user buffer是用户空间读入文件存储的内存块。
- 读入:当应用程序需要从磁盘中读取一个文件的时候,应用程序调用操作系统提供的接口,操作系统收到指令以后,切换到内核进程,检查kennel buffer里面是否有所需要的数据,如果有,则直接返回;如果没有,则调用磁盘驱动程序读取所需要文件,缓存至kennel buffer,然后再返回应用线程,用户线程决定对所取的数据进行处理。通常对于应用线程来讲,这一操作是同步阻塞的。
- 写出:当应用程序需要将数据写出到磁盘的时候,应用线程将指令传递给操作系统,操作系统将数据写入到缓存页中(图中kennel),然后返回给应用线程。操作系统会根据一定策略,延迟将数据刷新到磁盘中。
直接IO
一般来说,上面介绍的缓冲IO已经足够应付日常需求了。但是像数据库这种极度依赖IO的应用程序,为了追求极致的性能,往往更加愿意自己直接操作磁盘。
直接IO可以直接将数据从磁盘复制到用户空间,或者将数据从用户空间写到磁盘,减少了kernel中的缓冲区这一环节,这是直接IO可以提高性能的原理。
对于java语言来讲,没有可直接操作直接IO的API接口,在 Java 中使用 Direct IO 最终需要调用到 c 语言的 pwrite 接口,并设置 O_DIRECT flag,使用 O_DIRECT 存在不少限制:
-
操作系统限制:Linux 操作系统在 2.4.10 及以后的版本中支持 O_DIRECT flag,老版本会忽略该 Flag;Mac OS也有类似于 O_DIRECT 的机制
-
用于传递数据的缓冲区,其内存边界必须对齐为 blockSize 的整数倍
-
用于传递数据的缓冲区,其传递数据的大小必须是 blockSize 的整数倍。
-
数据传输的开始点,即文件和设备的偏移量,必须是 blockSize 的整数倍。
查看系统 blockSize 大小的方式:stat /boot/|grep “IO Block”
ubuntu@VM-30-130-ubuntu:~$ stat /boot/|grep “IO Block” Size: 4096 > Blocks: 8 IO Block: 4096 directory
通常为 4kb
引入依赖:
<dependency>
<groupId>moe.cnkirito.kdio</groupId>
<artifactId>kdio-core</artifactId>
<version>1.0.0</version>
</dependency>
内存映射
mmap方法会返回一个void *类型的指针ptr,它指向进程逻辑空间中的一个地址。后续如果想要读写文件,无需调用read/write方法, 而是直接操作这个ptr指针即可。
用户试图向ptr指针指向的内存空间读写数据时,由于MMU无法在物理内存中找到对应的地址,会触发一次缺页中断,OS会去硬盘中找到对应的数据并复制到内存中,然后用户就能正常完成读写操作了。这个过程是由操作系统自动完成的。
为什么说内存映射效率比缓冲IO要高?
我们回忆一下缓存IO的工作流程:
-
用户调用read方法
-
系统调用,触发中断,进程从用户态进入内核态
-
从硬盘中读取数据并复制到kernel缓冲区
-
将数据从kernel缓存区复制到用户提供的byte数组中
-
进程从内核态返回到用户态
完成
从上面的流程中我们可以看到,调用一次read方法,最多可能会引起两次 用户态与内核态之间的切换,以及两次数据复制
而内存映射呢?
-
用户试图访问ptr指向的数据
-
MMU解析失败,触发缺页中断,程序从用户态进入到内核态
-
从硬盘中读取数据并复制到进程空间中ptr指向的逻辑空间里
-
进程从内核态返回到用户态
完成
可以看出,试图访问内存映射文件,最多可能会引起 两次用户态与内核态之间的切换,以及一次数据复制
也就是说,内存映射与缓冲IO相比,可以节省数据复制带来的开销,因此效率较高。
java实现代码示例如下:
也可参考java mmap示例:
@SneakyThrows
@Test
public void testMmap(){
String text =new StringBuilder("大王叫我去巡山\n").append("巡到一处花果山\n").toString();
byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
//创建映射
RandomAccessFile randomAccessFile = new RandomAccessFile(new File("d:\\巡山.txt"), "rw");
MappedByteBuffer mappedByteBuffer = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE,0,bytes.length);
//数据写入文件
mappedByteBuffer.put(bytes);
mappedByteBuffer.clear();
//取消映射
Cleaner cleaner = ((DirectBuffer)mappedByteBuffer).cleaner();
if(cleaner!=null){
cleaner.clean();
}
}
网络IO
普通网络IO
对于读取网络的数据,当从网卡的数据到达socket buffer以后,应用会将其数据读入到用户内存里面,然后根据程序的指标,可以将数据存储到磁盘,也可计算以后,返回给 socket buffer,并发送至远程计算机。
如下图所示:
写至网络数据 应用系统可将内存里面的数据写至 socket buffer,然后操作系统将数据发送至网卡,送至网络上的远程计算机。
如下图所至:
sendfile函数
sendfile函数是linux内核自带的系统函数,专门用于处理从磁盘中加载数据,并传送至网络的场景。其数据传输模式如下图所示:
对比
相比普通的网络IO模式,其减少了硬盘缓存到用户内存,用户内存到网络缓存的数据复制,且减少了kennel到user和user到kennel的cpu上下文切换。
总结:相比普通缓存类的IO模式,其减少了二次数据复制和两次的CPU上下切换。
Java IO
java的io是基于操作系统进行实现的,可分BIO,NIO,AIO
BIO
即block-io,对于用户线程来讲,其属于同步阻塞的IO。即线程发出读取或者写指令后,需要等待动作完成,收到结果才可能进行一步指令。形如传统的文件操作代码:
@Slf4j
public class JavaIOTest {
File file = new File("d:\\a.txt");
@SneakyThrows
@Test
public void bioWriteTest(){
OutputStream out = new FileOutputStream(file);
String txt = "好好学习,天天向上";
out.write(txt.getBytes(StandardCharsets.UTF_8));
out.close();
}
@SneakyThrows
@Test
public void bioReadTest(){
InputStream inputStream = new FileInputStream(file);
byte[] buffer = new byte[1024];
inputStream.read(buffer);
inputStream.close();
log.info("文件内容为:{}",new String(buffer,StandardCharsets.UTF_8));
}
}
其通常使用缓存IO模型。
对于网络IO的情况,简单的阻塞示例演示(供应演示数据传输,不具备生产实用价值)。
@Slf4j
public class BioServer {
@SneakyThrows
public void start() {
ServerSocket serverSocket = new ServerSocket(3301);
while (true) {
Socket clientSocket = serverSocket.accept();
new Thread(() -> {
try {
byte[] datas = new byte[1024];
clientSocket.getInputStream().read(datas);
String receive = new String(datas, StandardCharsets.UTF_8);
log.info("服务端收到的数据为:{}", receive);
log.info("数据-->客户端");
clientSocket.getOutputStream().write("收到了,over\n".getBytes(StandardCharsets.UTF_8));
clientSocket.getOutputStream().flush();
} catch (IOException e) {
log.info("", e);
}
}).start();
}
}
public static void main(String[] args) {
new BioServer().start();
}
@SneakyThrows
@Test
public void testClient() {
Socket socket = new Socket();
socket.connect(new InetSocketAddress("localhost", 3301), 2000);
log.info("连接上服务器了,开始发送数据");
//发送数据给服务端
PrintWriter printWriter = new PrintWriter(socket.getOutputStream(), true);
printWriter.println("大王叫我来巡山");
printWriter.println("巡到一个花果山");
printWriter.println("是不是想当王,是不是想登基");
printWriter.flush();
//接收服务端数据
byte[] datas = new byte[256];
socket.getInputStream().read(datas);
log.info("客户端收到的数据={}",new String(datas,StandardCharsets.UTF_8));
socket.close();
}
}
网络BIO,典型特性是,是有一个线程负责接收连接请求,并将建立好的连接委托给另外的业务线程执行。
- 业务线程需要等待数据写入到socket buffer,然后再从socket buffer里面复制到应用内存,才可以进行处理。
- 当业务处理完成以后,业务线程需要将数据回写到socket buffer中,再由操作系统发送到网络中去。
此两个过程,业务线程都会由于IO的缓慢而发生阻塞,特别是读取阶段,线程需要等待数据从网络IO上面传到socket buffer,然后才开始复制到内存。发送数据要好一些,只需要将数据发送到socket buffer,业务线程任务就完成了,后续从socket buffer 到网络,由操作系统控制处理。如下图:
NIO
java的nio 原意为 new io 。从JDK1.4开始,Java提供了一系列改进的输入/输出处理的新特性,被统称为NIO(即New I/O)。新增了许多用于处理输入输出的类,这些类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写,新增了满足NIO的功能。 这里 new io 跟非阻塞io没有强相关。
java nio 有三个核心概念:
- Channel :通道,是指跟某块IO设备连接的通道。
- Buffers:内存块,处于用户内存。
- Selectors: 选择器。
channel
channel与流的几个重点区别
-
通道可以读也可以写,流一般来说是单向的(只能读或者写,所以之前我们用流进行IO操作的时候需要分别创建一个输入流和一个输出流)。
-
通道可以异步读写。
-
通道总是基于缓冲区Buffer来读写。
Java NIO中最重要的几个Channel的实现:
-
FileChannel: 用于文件的数据读写
-
DatagramChannel: 用于UDP的数据读写
-
SocketChannel: 用于TCP的数据读写,一般是客户端实现
-
ServerSocketChannel: 允许我们监听TCP链接请求,每个请求会创建会一个SocketChannel,一般是服务器实现
下图是一个简单的 channel示例图:
其中 FileChannel和 SocketChannel均为双向通道。
Buffer
Buffer为用户内存中的一块区域,channel可以将数据写入其中,也可以将其中的数据读出,发送到外部设备。
典型的实现有ByteBuffer,CharBuffer,ShortBuffer,IntBuffer,FloatBuffer,DoubleBuffer,LongBuffer等。
利用Buffer读写数据,通常遵循四个步骤:
- 把数据写入buffer;
- 调用flip;
- 从Buffer中读取数据;
- 调用buffer.clear()或者buffer.compact()。
Selector
selector是一个轮询器,他会实时轮询注册到他上面的所有通道的状态。
//创建selector
Selector selector = Selector.open();
//设置通道为非阻塞。
channel.configureBlocking(false);
//注册通道到轮询器上面。
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
Channel必须是非阻塞的。
所以FileChannel不适用Selector,因为FileChannel不能切换为非阻塞模式,更准确的来说是因为FileChannel没有继承SelectableChannel。Socket channel可以正常使用。
Selector的详细使用介绍,请参考 Java NIO之Selector(选择器)。
接下来,我们看一下 在java nio 中,对文件的基本操作,通过fileChannel,
注意:此过程对用户线程来讲,还是阻塞的,并没有本质改变。
代码示例:
FileChannel:
@Slf4j
public class FileChannelTest {
@SneakyThrows
@Test
public void testWriteFileChannel() {
String str = RandomStringUtils.randomPrint(20,40);
//写
FileChannel fileChannel = FileChannel.open(Paths.get("d:\\a.txt"), StandardOpenOption.WRITE, StandardOpenOption.READ);
ByteBuffer byteBuffer = ByteBuffer.allocate(256);
log.info("str={}",str);
byteBuffer.put(str.getBytes(Charset.defaultCharset()));
byteBuffer.flip();
while (byteBuffer.hasRemaining()){
fileChannel.write(byteBuffer);
}
byteBuffer.clear();
}
@SneakyThrows
@Test
public void testReadFileChannel(){
//读
ByteBuffer byteBuffer = ByteBuffer.allocate(256);
FileChannel fileRead = FileChannel.open(Paths.get("d:\\a.txt"), StandardOpenOption.READ);
fileRead.read(byteBuffer);
byteBuffer.flip();
String result = new String(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit(), StandardCharsets.UTF_8);
log.info("读入的数据为:{}", result);
}
@SneakyThrows
@Test
public void testCopyFileChannel(){
//复制
ByteBuffer byteBuffer = ByteBuffer.allocate(256);
FileChannel fileRead = FileChannel.open(Paths.get("d:\\a.txt"), StandardOpenOption.READ);
fileRead.read(byteBuffer);
byteBuffer.flip();
FileChannel writeChannel = FileChannel.open(Paths.get("d:\\b.txt"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
while (byteBuffer.hasRemaining()){
writeChannel.write(byteBuffer);
}
byteBuffer.clear();
// fileRead.transferTo(0,fileRead.size(),writeChannel);,复制数据,也可这样。
}
}
SocketChannel
我们接下来看一下,在网络io中的 nio Server
@Slf4j
public class NioServer {
static int selectCount = 1;
static int threadNameIndex = 0;
public static void main(String[] args) {
new NioServer().start();
}
@SneakyThrows
public void start() {
//生成一个轮询器
Selector selector = Selector.open();
//生成连接通道。
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定主机、端口
serverSocketChannel.bind(new InetSocketAddress("localhost", 3301));
//设置连接通道非阻塞。
serverSocketChannel.configureBlocking(false);
//注册事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
if (selector.select(10) < 1) {
continue;
}
log.info("第{}次轮询", selectCount);
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
int keyCount = 1;
while (iterator.hasNext()) {
log.info("第{}次轮询的第{}次轮key,Key数量={}", selectCount, keyCount++, selector.selectedKeys().size());
SelectionKey key = iterator.next();
//1.连接请求的数据来到
if (key.isAcceptable()) {
log.info("处理连接事件");
handleAccept(key);
}
//2.如果是数据通道有数据来了,
if (key.isReadable()) {
log.info("处理读入事件");
handleRead(key);
}
//3.通道可写
if (key.isWritable() && key.isValid()) {
log.info("处理写出事件");
handleWrite(key);
}
iterator.remove();
}
selectCount++;
}
}
@SneakyThrows
private void handleRead(SelectionKey key) {
SocketChannel dataChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//读入网络数据
dataChannel.read(byteBuffer);
new Thread(() -> {
//这里读数据,由于数据是从kennel得到 当前byte Buffer
// 所以会短暂阻塞当前线程
//相当于业务线程处理业务数据
byteBuffer.flip();
log.info("服务端收到的数据为:{}", new String(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()), StandardCharsets.UTF_8);
byteBuffer.clear();
//准备回送数据。
ByteBuffer sendBuffer = ByteBuffer.wrap("来自服务端的问候".getBytes(StandardCharsets.UTF_8));
//注册可写事件,
// 问:为毛不直接发送数据?
// 答:主要是发送数据这种事情不要来占用业务线程的时间,收数发据还是留给IO线程,当然直接发送也是可以的,写这个操作不会怎么阻塞线程。毕竟是内存之间复制数据嘛。
try {
dataChannel.register(key.selector(), SelectionKey.OP_WRITE, sendBuffer);
log.info("注册可写事件成功");
} catch (ClosedChannelException e) {
log.info("注册可写事件失败", e);
}
}, "业务线程-" + threadNameIndex++).start();
}
private void handleAccept(SelectionKey key) {
//连接通道收到连接请求了。
ServerSocketChannel acceptChannel = (ServerSocketChannel) key.channel();
try {
//建立连接
SocketChannel dataChannel = acceptChannel.accept();
dataChannel.configureBlocking(false);
//继续将数建立好的数据通道注册至选择器 Selector,等待数据的来临
dataChannel.register(key.selector(), SelectionKey.OP_READ);
} catch (IOException e) {
log.info("连接失败", e);
}
}
@SneakyThrows
private void handleWrite(SelectionKey key) {
SocketChannel dataChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
//发送数据至客户端,
// 当前bytebuffer --> kennel bytebuffer 段,
// 短暂阻塞当前线程
log.info("发送数据为={}", new String(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit(), StandardCharsets.UTF_8));
dataChannel.write(byteBuffer);
log.info("已经向客户端发送数据");
dataChannel.close();
byteBuffer.clear();
}
@SneakyThrows
@Test
public void testClient() {
Socket socket = new Socket();
socket.connect(new InetSocketAddress("localhost", 3301), 2000);
log.info("连接上服务器了,开始发送数据");
//发送数据给服务端
PrintWriter printWriter = new PrintWriter(socket.getOutputStream(), true);
printWriter.print("大王叫我来巡山,");
printWriter.print("巡到一个花果山,");
printWriter.print("是不是想当王,是不是想登基");
printWriter.flush();
//接收服务端数据
byte[] datas = new byte[256];
socket.getInputStream().read(datas);
log.info("客户端收到的数据={}", new String(datas, StandardCharsets.UTF_8));
socket.close();
}
}
上述代码,我们建立了一个简单的NioServer, 客户端即用testClient方法,客户端保持使用bio, 我们主要研究服务端nio 过程。在其处理过程中,我们打印日志,用以跟踪其处理流程。如:
14:55:45.412 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer - 第1次轮询
14:55:45.419 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer - 第1次轮询的第1次轮key,Key数量=1
14:55:45.419 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer - 处理连接事件
14:55:45.420 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer - 第2次轮询
14:55:45.420 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer - 第2次轮询的第1次轮key,Key数量=1
14:55:45.420 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer - 处理读入事件
14:55:45.422 [业务线程-0] INFO xyd.tan.yongfu.sb2.nio.NioServer - 服务端收到的数据为:大王叫我来巡山,巡到一个花果山,是不是想当王,是不是想登基
14:55:45.423 [业务线程-0] INFO xyd.tan.yongfu.sb2.nio.NioServer - 注册可写事件成功
14:55:45.437 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer - 第3次轮询
14:55:45.437 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer - 第3次轮询的第1次轮key,Key数量=1
14:55:45.437 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer - 处理写出事件
14:55:45.437 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer - 发送数据为=来自服务端的问候
14:55:45.438 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer - 已经向客户端发送数据
过程分析:
- 第一个循环:处理的是连接事件,并在其中注册了一个读事件。
- 第二个循环:处理读事件,将数据读入后,委托给另外一个业务线程进行业务处理(正式的,会使用专门的业务线程池。),并在业务线程中,处理完业务以后,注册写事件。
- 第三个循环:处理写事件,将数据发送至客户端。
网络NIO与网络BIO对比:
bio,模型为:连接线程获取到连接请求以后,创建连接,然后将连接委托给业务线程。此时,业务线程需要阻塞并等待网络数据的到来,然后处理完业务,再将数据复制到socket 缓存中。此过程,业务线程被阻塞在网络数据的传输中,浪费了线程利用率。特别在高并发的时候,很多连接无法分配到线程,导致业务无法处理。
nio,模型为:Selector所在的master线程一直循环检测 Selector中注册的的通道是件是否产生事件。如果产生了事件,就会取出相应的通道,并对事件进行处理。
具体的:
- 对于连接事件,master线程直接创建连接,并注册读事件。
- 对于通道读事件,由于数据到达kennel buffer后才会产生读事件,所以master线程可直接读取数据,这个过程,由于是kennel buffer与user buffer 之间数据复制,不存在等待网络数据传输,所以阻塞时间很短。数据获取以后,可直接将数据委托给业务线程,业务线程不会处理IO事件,故不会阻塞和浪费业务线程资源。
- 对于通道写事件,由于是从user buffer 复制数据至kennel buffer中,不涉及网络慢io,故业务线程可以处理, master线程也可处理,对性能影响不大。
总结:
对于两个不同的io模式, nio 的主要优点是释放业务线程的职能,让业务线程不用一直去阻塞等待网络数据的传输。将其精力主要放在处理后端业务能力上面,以期能够承载更多的计算任务。nio模型主要是通过Selector 内部的事件模型,避免业务线程的等待,从而在相同的时间内能够容纳更多的网络连接。nio的优点是能够容纳更多的计算业务和网络连接,对于单个客户端连接的服务,并不会提高其响应速度。
AIO
aio,即Asynchronous IO,异步io。典型的异步处理模式,同步发出请求,返回处理中,线程释放回线程池,接收其他任务。异步通知回来,启用新线程进行处理回调任务。故在调用异步IO的时候,需要传递一个 CompletionHandler<V,A>回调函数,用于kennel处理完io以后,使用新线程处理回调函数里面的任务。
以下是一个AIO File 的事例:
@Slf4j
public class AioFileChannelTest {
@SneakyThrows
@Test
public void testAioFile() {
//创建异步处理通道,简单演示,d:\\a.txt内容不要超过1024字节。
Path path = Paths.get("d:", "a.txt");
AsynchronousFileChannel afc = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
//创建一个内存缓存块。
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//异步读取文件内容,并传递一个回调函数
log.info("发送读取请求时当前线程名称:{}", Thread.currentThread().getName());
afc.read(byteBuffer, 0, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@SneakyThrows
@Override
public void completed(Integer result, ByteBuffer attachment) {
log.info("回调函数开始执行,已经读取完文件数据,线程名:{}", Thread.currentThread().getName());
attachment.flip();
log.info("文件内容为:{}" + new String(attachment.array(), attachment.position(), attachment.limit(), StandardCharsets.UTF_8));
//处理完数据,关闭通道
afc.close();
}
@SneakyThrows
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
afc.close();
}
});
}
}
请看日志,特别注意其执行的线程。
17:09:06.428 [main] INFO xyd.tan.yongfu.sb2.aio.AioFileChannelTest - 发送读取请求时当前线程名称:main
17:09:06.434 [Thread-13] INFO xyd.tan.yongfu.sb2.aio.AioFileChannelTest - 回调函数开始执行,已经读取完文件数据,线程名:Thread-13
17:09:06.434 [Thread-13] INFO xyd.tan.yongfu.sb2.aio.AioFileChannelTest - 文件内容为:大王叫我来巡山,巡到一个火焰山。你是真的秀!
再来一个,关于AIO server。
public class AioServer {
AsynchronousServerSocketChannel assc;
@SneakyThrows
public void start(){
//1. 创建一个线程池
ExecutorService es = Executors.newCachedThreadPool();
//2. 创建异步通道群组,并设置线程池。
AsynchronousChannelGroup tg = AsynchronousChannelGroup.withCachedThreadPool(es, 1);
//3. 创建服务端异步通道
assc = AsynchronousServerSocketChannel.open(tg);
//4. 绑定监听端口
assc.bind(new InetSocketAddress(80));
AioAttachment aioAttachment = new AioAttachment();
aioAttachment.setServer(assc);
//5. 监听连接,传入回调类处理连接请求
assc.accept(aioAttachment, new AcceptHandler());
}
}
定义 在各个线程之间传输的上下文附件。
@Data
public class AioAttachment {
private AsynchronousServerSocketChannel server;
private AsynchronousSocketChannel client;
private boolean isReadMode;
private ByteBuffer buffer;
private WriteCompletHander writeCompletHander;
private ReadCompletHander readCompletHander;
}
连接完成处理器。
@Slf4j
public class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, AioAttachment> {
@SneakyThrows
@Override
public void completed(AsynchronousSocketChannel channel, AioAttachment attachment) {
//继续监听
attachment.getServer().accept(attachment, this);
log.info("收到连接请求={}", channel.getRemoteAddress());
//
AioAttachment aioAttachment = new AioAttachment();
aioAttachment.setServer(attachment.getServer());
aioAttachment.setClient(channel);
aioAttachment.setReadMode(true);
aioAttachment.setBuffer(ByteBuffer.allocate(1024 << 4));
aioAttachment.setWriteCompletHander(new WriteCompletHander());
aioAttachment.setReadCompletHander(new ReadCompletHander());
//连接成功后,马上发起读取数据请求。
channel.read(aioAttachment.getBuffer(), aioAttachment, aioAttachment.getReadCompletHander());
}
@Override
public void failed(Throwable exc, AioAttachment attachment) {
log.info("处理连接请求失败={}", exc);
}
}
数据读取完成处理器
@Slf4j
public class ReadCompletHander implements CompletionHandler<Integer, AioAttachment> {
@SneakyThrows
@Override
public void completed(Integer result, AioAttachment attachment) {
// 读取来自客户端的数据
ByteBuffer buffer = attachment.getBuffer();
buffer.flip();
byte bytes[] = new byte[buffer.limit() - buffer.position()];
buffer.get(bytes);
String msg = new String(bytes, StandardCharsets.UTF_8).trim();
log.info("收到来自客户端的数据: " + msg);
// 响应客户端请求,准备回写的数据
buffer.clear();
buffer.put(("Response from server!:" + RandomStringUtils.randomAlphanumeric(10)).getBytes(StandardCharsets.UTF_8));
// 写数据到客户端也是异步
buffer.flip();
log.info("发送数据至客户端");
attachment.getClient().write(buffer, attachment, attachment.getWriteCompletHander());
}
@Override
public void failed(Throwable exc, AioAttachment attachment) {
log.info("读取数据失败", exc);
}
}
数据写入完成处理器
@Slf4j
public class WriteCompletHander implements CompletionHandler<Integer, AioAttachment> {
@SneakyThrows
@Override
public void completed(Integer result, AioAttachment attachment) {
// 继续监听读取事件。
log.info("数据发送完毕,继续监听");
attachment.getBuffer().clear();
attachment.getClient().read(attachment.getBuffer(), attachment, attachment.getReadCompletHander());
}
@Override
public void failed(Throwable exc, AioAttachment attachment) {
log.info("写数据失败", exc);
}
}
总结:
IO中的几种访问方式,如缓存IO,直接IO,内存映射IO,其反映的是数据流的交互方式,其主要影响IO响应速度,各自有其优缺点。平时在使用相应的IO框架的时候,可以了解其底层实现逻辑,以判定其是否合适当前场景。
java 中的 bio ,nio ,aio ,其核心差异并不是提高IO的响应速度,而是通过使用操作系统提供的能力,尽量减少业务线程去等待和处理IO流的相关事宜。将业务线程尽可能释放出来,以方便系统可以处理更多的业务。从而在宏观上面提升系统的吞吐量。
参考资料
Java 文件 IO 操作之 DirectIO
Java IO模型–边学边写边理解
Java NIO 之 Channel(通道)
Java NIO 之 Buffer(缓冲区)
Java NIO之Selector(选择器)