前言:在编程中经常遇到需要读文件完成读写操作,然后将文件以流的方式完成数据传输;普通的java对象完成数据序列化之后,也需要对其进行传输;那么应该选用何种方式完成对流的传输;
1 流操作的同步和阻塞概念:
- 同步:调用者在对一个方法完成调用时,会死等方法的结果返回,只有得到结果程序继续向下执行;
- 异步:调用者在对一个方法完成调用时,不等待方法的结果返回,继续向下执行程序;最后由被调用者通知调用者,或者调用者通过回调的方式来获取结果;
在java 中体现同步和异步,同步时主线程中的程序依次向下执行并取得结果;异步时通过多线程方式,发起方法调用后,不等待结果,最后通过callable 和future 获取多线程执行的结果;
如果说以程序调用方法是否死等结果来辨识同步和异步,那么阻塞和非阻塞则标识程序在等待调用结果是,当前线程是否会被挂起;
- 阻塞:被调用者在结果返回之前,调用者程序线程一直被挂起(让出CPU资源);
- 非阻塞:调用者在发起调用时,如果没有拿到被调用者的返回结果,回直接返回,而不会挂起线程;
同步和异步关注的通信的结果,阻塞和非阻塞关注的是线程在等待调用结果时线程是否被挂起;
2 通信模型:
2.1 用户空间,内核空间:
java 本身并没有那么大的系统权限可以直接操作文件,它必须借助api 的调用,然后在通过不同系统对于命令的实现,通过操作系统层面来完成文件的处理并将结果放入到内核空间,再由cpu将内核空间的缓冲数据复制到用户空间;
简单来说:用户空间就是用户可以看得见直接操作的地方,系统空间是操作系统执行命令进行数据的操作,是用户看不见的地方
2.2 BIO,NIO,AIO 通信模型;
2.2.1 BIO:全称是Blocking IO,是JDK1.4之前的传统IO模型,本身是同步阻塞模式;
当我们在向远程服务进行调用后,用户空间向内核空间要本次调用的结果,如果内核空间没有结果,则进行阻塞并让出cpu资源,当被调用者返回数据后,网卡将数据复制到系统空间,此时代表数据准备好,cpu 再将数据从系统空间拷贝到用户空间,并将阻塞的线程加入到工作队列,cpu轮询工作队列,用户进程得到调用结果,继续业务处理;
2.2.2 :Non-Blocking IO 是同步非阻塞的IO模型;
用户进程在向内核空间索要数据时,如果内核空间没有数据,此时不进行用户线程的挂起,而是以用户空间不断轮询向向内核空间索要数据,从而不让出cpu 资源,一旦内核空间的数据准备好后,阻塞当前线程,并将数据从内核空间复制到用户空间,用户进程在从用户空间拿到数据,应用进程不停的轮询内核空间,会造成CPU浪费;
多路IO复用模型:
用户进程首先阻塞于select方法,当内核返回可读状态后,根据事件类型去做调用,将数据复制到用户空间缓冲区,处理区间状态阻塞。
2.2.3 异步IO模型:
AIO是java中IO模型的一种,作为NIO的改进和增强随JDK1.7版本更新被集成在JDK的nio包中,因此AIO也被称作是NIO2.0。AIO提供了从建立连接到读、写的全异步操作。AIO可用于异步的文件读写和网络通信。
3 零拷贝:
零拷贝并不是真正的不发生拷贝,只是cpu不参与拷贝;
3.1 传统的文件拷贝:
File f = new File("helloword/data.txt");
RandomAccessFile file = new RandomAccessFile(file, "r");
byte[] buf = new byte[(int)f.length()];
// 包括2次拷贝(DMA(硬件到内核缓冲区),内核缓冲到用户缓冲)
file.read(buf);
Socket socket = ...;
//包括两次拷贝(用户缓冲区到socket缓冲区,DMA(socket缓冲区到网卡))
socket.getOutputStream().write(buf);
- 用户进行发起read 请求读取文件,从用户态切换到内核态;
- 系统内核从磁盘找到改文件,并通过DMA 将文件拷贝到系统内核空间的缓冲区中;
- cpu 将内核缓冲区的数据拷贝到用户空间中;
- 从内核态切换到用户态,用户进程从用户空间获取文件,并操作;
- 用户进程发起write 请求写文件,从用户态切换到内核态;
- cpu 将文件数据从用户空间拷贝到内核空间的socket 缓冲区中;
- 通过DMA拷贝将socket 缓冲区的数据拷贝到网卡进行数据发送;
传统的文件发送,涉及到两次内核切换,4次拷贝(2次DMA拷贝,2次cpu拷贝);在大并发的场景下,上述过程就会被累计放大,进程影响到系统性能,要想提高文件传输的性能就需要减少操作系统用户态和内核态的上下文切换和数据拷贝次数;
3.2 Nio优化使用直接内存拷贝:
3.2.1 技术1:mmap+write():
- 在原始文件拷贝的基础上,将内核态缓冲区的数据通过映射的方式直接映射到用户空间中,使得用户进程可以通过直接访问和修改用户态空间中的改文件;
- 在用户进程发起write 请求后,cpu 不在从用户空间拷贝数据,直接从内核空间将数据拷贝到Socket 缓存区;
- 通过DMA拷贝将socket缓冲区的数据拷贝到网卡中,进行数据发送;
通过在用户空间开辟jvm以外内存空间的方式,将内核空间映射到用户空间的内存中,使得java 可以直接对内核空间的数据进行访问和修改;
开辟jvm以外内存空间:这块内存不受 jvm 垃圾回收的影响,因此内存地址固定,有助于 IO 读写;这次优化后,mmap()+write()就变成了3次拷贝(2次DMA拷贝+1次CPU拷贝)+4次状态切换。
3.2.2 sendfile 优化:
sendfile系统调用可以直接把内核缓冲区的数据直接拷贝到socket缓冲区中,不再拷贝到用户态,这样就只由2次上下文切换和3次数据拷贝(2次DMA拷贝+1次CPU拷贝);
LInux内核2.4对sendfile优化:
从LInux内核2.4版本开始起,对于支持网卡支持SG-DMA的技术的情况下,上述过程又进行了优化:
优化之后:
- 通过DMA将数据从磁盘上拷贝到内核的缓冲区中;
- cpu将缓冲区描述符和数据长度传到socket缓冲区;
- DMA将缓冲区的数据拷贝到网卡中,进行发送;
注意:
1)2次状态切换是少不了的,因为要直接操作硬件,用户进程没有这么大权限,所以至少要发生一次系统调用;
2)其实还是有一次cpu拷贝的,只是拷贝的数据量很小,CPU需要把数据的长度length和偏移量offset拷贝到socket 缓冲区。这个过程相比2.1于拷贝完整的数据,可以忽略;
java实现:
1)mmap Java NIO有一个MappedByteBuffer的类,可以用来实现内存映射。它的底层是调用了Linux内核的mmap的API。
public class MmapTest {
public static void main(String[] args) {
try {
FileChannel readChannel = FileChannel.open(Paths.get("./helowWorld.txt"), StandardOpenOption.READ);
MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 * 1024 * 40);
FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//数据传输
writeChannel.write(data);
readChannel.close();
writeChannel.close();
}catch (Exception e){
System.out.println(e.getMessage());
}
}
}
/*
说明
1. MappedByteBuffer 可让文件直接在内存(堆外内存)修改
*/
public class MappedByteBufferTest {
public static void main(String[] args) throws Exception {
RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
//获取对应的通道
FileChannel channel = randomAccessFile.getChannel();
/**
* 参数1: FileChannel.MapMode.READ_WRITE 使用的读写模式
* 参数2: 0 : 可以直接修改的起始位置
* 参数3: 5: 是映射到内存的大小(不是索引位置) ,即将 1.txt 的多少个字节映射到内存
* 可以直接修改的范围就是 0-5
* 实际类型 DirectByteBuffer
*/
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
mappedByteBuffer.put(0, (byte) 'H');
mappedByteBuffer.put(3, (byte) '9');
mappedByteBuffer.put(5, (byte) 'Y');//IndexOutOfBoundsException
randomAccessFile.close();
System.out.println("修改成功~~");
}
}
- sendfile:FileChannel的transferTo()/transferFrom(),底层就是sendfile() 系统调用函数。
public class NewIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 7001));
String filename = "protoc-3.6.1-win32.zip";
//得到一个文件channel
FileChannel fileChannel = new FileInputStream(filename).getChannel();
//准备发送
long startTime = System.currentTimeMillis();
//在linux下一个transferTo 方法就可以完成传输
//在windows 下 一次调用 transferTo 只能发送8m , 就需要分段传输文件, 而且要主要
//transferTo 底层使用到零拷贝
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("发送的总的字节数 =" + transferCount + " 耗时:" + (System.currentTimeMillis() - startTime));
//关闭
fileChannel.close();
}
}
建议:如果涉及到文件传输,transferTo是首选,但是如果涉及到对内存数据的修改选用MappedByteBuffer。
3.3 AIO 模型实现:AIO 用来解决数据复制阶段的阻塞问题:
异步模型需要底层操作系统(Kernel)提供支持:
- Windows 系统通过 IOCP 实现了真正的异步 IO
- Linux 系统异步 IO 在 2.6 版本引入,但其底层实现还是用多路复用模拟了异步 IO,性能没有优势
- AIO 文件读取:
package org.lgx.bluegrass.bluegrasscoree.util.aio;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
/**
* @Description TODO
* @Date 2023/2/17 17:13
* @Author lgx
* @Version 1.0
*/
public class AioTest {
public static void main(String[] args) throws IOException {
try {
AsynchronousFileChannel s = AsynchronousFileChannel.open(
Paths.get("C:\\Users\\Administrator\\Desktop\\开发\\csdn\\工具篇\\aio\\test.txt"), StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
System.out.println("begin...");
s.read(buffer, 0, null, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("read completed..." + result);
buffer.flip();
System.out.println(Thread.currentThread().getName() + ",内容是:" + print(buffer));
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("ead failed...");
}
});
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("do other things...");
System.in.read();
}
static String print(ByteBuffer b) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < b.limit(); i++) {
stringBuilder.append((char) b.get(i));
}
return stringBuilder.toString();
}
}
2) AIO serversocket:
package org.lgx.bluegrass.bluegrasscoree.util.selectorsocket.server;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.Charset;
/**
* @Description TODO
* @Date 2023/2/17 17:19
* @Author lgx
* @Version 1.0
*/
public class AioServer {
public static void main(String[] args) throws IOException {
AsynchronousServerSocketChannel ssc = AsynchronousServerSocketChannel.open();
ssc.bind(new InetSocketAddress(8080));
ssc.accept(null, new AcceptHandler(ssc));
System.in.read();
}
private static void closeChannel(AsynchronousSocketChannel sc) {
try {
System.out.printf("[%s] %s close\n", Thread.currentThread().getName(), sc.getRemoteAddress());
sc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private static class ReadHandler implements CompletionHandler<Integer, ByteBuffer> {
private final AsynchronousSocketChannel sc;
public ReadHandler(AsynchronousSocketChannel sc) {
this.sc = sc;
}
@Override
public void completed(Integer result, ByteBuffer attachment) {
try {
if (result == -1) {
closeChannel(sc);
return;
}
System.out.printf("[%s] %s read\n", Thread.currentThread().getName(), sc.getRemoteAddress());
attachment.flip();
System.out.println(Charset.defaultCharset().decode(attachment));
attachment.clear();
// 处理完第一个 read 时,需要再次调用 read 方法来处理下一个 read 事件
sc.read(attachment, attachment, this);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
closeChannel(sc);
exc.printStackTrace();
}
}
private static class WriteHandler implements CompletionHandler<Integer, ByteBuffer> {
private final AsynchronousSocketChannel sc;
private WriteHandler(AsynchronousSocketChannel sc) {
this.sc = sc;
}
@Override
public void completed(Integer result, ByteBuffer attachment) {
// 如果作为附件的 buffer 还有内容,需要再次 write 写出剩余内容
if (attachment.hasRemaining()) {
sc.write(attachment);
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
closeChannel(sc);
}
}
private static class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Object> {
private final AsynchronousServerSocketChannel ssc;
public AcceptHandler(AsynchronousServerSocketChannel ssc) {
this.ssc = ssc;
}
@Override
public void completed(AsynchronousSocketChannel sc, Object attachment) {
try {
System.out.printf("[%s] %s connected\n", Thread.currentThread().getName(), sc.getRemoteAddress());
} catch (IOException e) {
e.printStackTrace();
}
ByteBuffer buffer = ByteBuffer.allocate(16);
// 读事件由 ReadHandler 处理
sc.read(buffer, buffer, new ReadHandler(sc));
// 写事件由 WriteHandler 处理
sc.write(Charset.defaultCharset().encode("server hello!"), ByteBuffer.allocate(16), new WriteHandler(sc));
// 处理完第一个 accpet 时,需要再次调用 accept 方法来处理下一个 accept 事件
ssc.accept(null, this);
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
}
}
参考:
1 netty(六)NIO、BIO与AIO;
2 java NIO 零拷贝技术;