1、输入流
InputStream、InputStreamReader、BufferedReader
2、输出流
OutputStream、OutputStreamWriter、BufferedWriter
3、传统IO操作:
基于传统的IO方式,底层实际上通过调用read()和write()来实现。
通过read()把数据从硬盘读取到内核缓冲区,再复制到用户缓冲区;然后再通过write()写入到socket缓冲区,最后写入网卡设备。
整个过程发生了4次用户态和内核态的上下文切换和4次拷贝,具体流程如下:
1、用户进程通过read()方法向操作系统发起调用,此时【上下文从用户态转向内核态】
2、DMA控制器把数据从硬盘中拷贝到读缓冲区
3、CPU把读缓冲区数据拷贝到应用缓冲区,【上下文从内核态转为用户态】,read()返回
4、用户进程通过write()方法发起调用,【上下文从用户态转为内核态】
5、CPU将应用缓冲区中数据拷贝到socket缓冲区
6、DMA控制器把数据从socket缓冲区拷贝到网卡,【上下文从内核态切换回用户态】,write()返回
4、零拷贝
在Java程序中,零拷贝技术分为两种:mmap(内存映射)和sendFile,首先要了解零拷贝的概念:所谓的零拷贝不是不拷贝,而是不经过CPU拷贝,它还是需要拷贝的(比如将数据从硬盘拷贝到内核态),这个零拷贝是从操作系统(CPU)的角度看的。
零拷贝技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域,这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。
那么对于零拷贝而言,并非真的是完全没有数据拷贝的过程,只不过是减少用户态和内核态的切换次数以及CPU拷贝的次数。
4.1、mmap
mmap+write简单来说就是使用mmap替换了read+write中的read操作,减少一次CPU拷贝。
mmap主要实现方式是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,从而减少了从读缓冲区到用户缓冲区的一次CPU拷贝。
整个过程发生了4次用户态和内核态的上下文切换和3次拷贝,具体流程如下:
1、用户进程通过mmap()方法向操作系统发起调用,【上下文从用户态转向内核态】
2、DMA控制器把数据从硬盘中拷贝到读缓冲区
3、【上下文从内核态转为用户态】,mmap调用返回
4、用户进程通过write()方法发起调用,【上下文从用户态转为内核态】
5、CPU将读缓冲区中数据拷贝到socket缓冲区
6、DMA控制器把数据从socket缓冲区拷贝到网卡,【上下文从内核态切换回用户态】,write()返回
mmap的方式节省了一次CPU拷贝,同时由于用户进程中的内存是虚拟的,只是映射到内核的读缓冲区,所以可以节省一半的内存空间,比较适合大文件的传输。
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class MmapTest {
public static void main(String[] args) {
try {
// 获取文件
FileChannel readChannel = FileChannel.open(Paths.get("path1"),
StandardOpenOption.READ);
//MapMode:映射的模式,可选项包括:READ_ONLY,READ_WRITE,PRIVATE;
//Position:从哪个位置开始映射,字节数的位置;
//Size:从position开始向后多少个字节;
MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY,
0,
1024 * 1024 * 40);
FileChannel writeChannel = FileChannel.open(Paths.get("path2"),
StandardOpenOption.WRITE,
StandardOpenOption.CREATE);
//数据传输
writeChannel.write(data);
readChannel.close();
writeChannel.close();
}catch (Exception e){
System.out.println(e.getMessage());
}
}
}
4.2、sendFile
sendfile系统调用在内核版本2.1中被引入,通过使用sendfile数据可以直接在内核空间进行传输,因此避免了用户空间和内核空间的拷贝,目的是简化通过网络在两个通道之间进行的数据传输过程。相比mmap来说,sendfile同样减少了一次CPU拷贝,而且还减少了2次上下文切换。
整个过程发生了2次用户态和内核态的上下文切换和3次拷贝,具体流程如下:
1、用户进程通过sendfile()方法向操作系统发起调用,【上下文从用户态转向内核态】
2、DMA控制器把数据从硬盘中拷贝到读缓冲区
3、CPU将读缓冲区中数据拷贝到socket缓冲区
4、DMA控制器把数据从socket缓冲区拷贝到网卡,【上下文从内核态切换回用户态】,sendfile调用返回
sendfile方法IO数据对用户空间完全不可见,所以只能适用于完全不需要用户空间处理的情况,比如静态文件服务器。
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class SendfileTest {
//实例1
public static void main(String[] args) {
try {
FileChannel readChannel = FileChannel.open(Paths.get("path1"), StandardOpenOption.READ);
long len = readChannel.size();
long position = readChannel.position();
FileChannel writeChannel = FileChannel.open(Paths.get("path2"),
StandardOpenOption.WRITE,
StandardOpenOption.CREATE);
//数据传输
//开始发送数据:在Java中使用零拷贝技术调用transferTo方法,这个方法底层使用了零拷贝技术
// 在Linux系统下 使用transferTo 方法,没有文件大小限制,可以将文件调用一次transferTo方法即可传输完成
//但是在Windows系统下调用一次transferTo 方法,最多只能发送 8m 的数据,所以需要将文件进行分段传输
// transferTo 参数介绍:
//第一个参数:从文件的哪里开始读取
//第二个参数:读取多少字节
//第三个参数:将读取的字节,放入需要写入的Channel中
readChannel.transferTo(position, len, writeChannel);
readChannel.close();
writeChannel.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
//实例2
public void writeHtml(File file) {
if (file.exists() && file.isFile()) {
try {
System.err.println("文件存在");
//文件存在进行输出,此处使用零拷贝技术
//获取到该 channel 关联的 缓存Buffer
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
//得到一个文件通道
FileChannel fileChannel = new FileInputStream(file).getChannel();
writeBuffer.put(HttpProtocolUtil.sendHead(fileChannel.size(),"200"));
writeBuffer.flip();
channel.write(writeBuffer);
//开始发送数据:在Java中使用零拷贝技术调用transferTo方法,这个方法底层使用了零拷贝技术
// 在Linux系统下 使用transferTo 方法,没有文件大小限制,可以将文件调用一次transferTo方法即可传输完成
//但是在Windows系统下调用一次transferTo 方法,最多只能发送 8m 的数据,所以需要将文件进行分段传输
// transferTo 参数介绍:
// 第一个参数:从文件的哪里开始读取
// 第二个参数:读取多少字节
// 第三个参数:将读取的字节需要放入的SocketChannel
long count = fileChannel.transferTo(0, fileChannel.size(), channel);
System.err.println("传输的总的字节大小:"+count);
//关闭通道
close();
} catch (Exception e) {
e.printStackTrace();
System.err.println("写出静态资源失败");
}
} else {
doWrite(HttpProtocolUtil.send404("404 资源未找到"));
//关闭通道
close();
}
}
}