java的 transformTo transformFrom

java是平台无关的,但是JVM是平台相关的。

磁盘上文件拿出来发给用户

最原始的实现(c实现)

内核空间系统调用–上下文切换–直接内存访问DMA 数据**拷贝到内核空间缓冲区(页缓存)–上下文切换–又拷贝**到了用户空间的缓冲区

内核空间系统调用–上下文切换–用户空间 数据**拷贝到内核空间缓冲区(页缓存)–又拷贝**到了网卡的socket缓冲区–上下文切换–返回用户空间

用户空间没有对数据修改

  • 4次上下文切换
  • 2次系统调用
  • 4次数据拷贝

1624802954524-9524161a-3848-4e1b-88a2-8e68e9834ea3.png

零拷贝(c实现)

完全依赖操作系统的,不会有数据在用户内核之间拷贝

sendfile() 内核空间系统调用–上下文切换–直接内存访问DMA 数据**拷贝到内核空间缓冲区(页缓存)–数据拷贝**到目标socket的缓冲区–缓冲区到DMA网卡发送–上下文切换–返回用户空间

1624803392683-d2000245-9018-4793-b038-3ad022e6bbea.png

真正的零拷贝(c实现)

文件描述符可以描述数据的一些大小偏移,socket缓存里面只是存这些,

DMA 拷贝内核缓冲区(页缓存)之后–Linux 2.4 之后,文件描述符gather操作,数据不会kernel buffer到socketbuffer,****只有文件描述符的信息(kernel buffer 的内存地址在什么地方;kernel buffer 多长)会到socket buffer里面。原来的数据本身

真正的数据直接去网卡缓冲区

1624803704100-2f64a8cc-43a4-46ea-8829-f5672a8c7bd4.png

1624806298348-b4c0cb19-e103-4de7-808b-bf452ed17132.png

1624806344586-ac506341-e8ce-4941-b626-1391cb23c1a1.png

零拷贝案例

服务端读取完就丢弃掉

客户端打印需要的时间

传统方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

import java.io.DataInputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class OldIOServer {

public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8899);

while (true) {
// 连接 建立起来才会不阻塞
Socket socket = serverSocket.accept();
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());

try {
byte[] byteArray = new byte[4096];

while (true) {
// 实际读到的字节数
int readCount = dataInputStream.read(byteArray, 0, byteArray.length);

// 读完就返回-1
if (-1 == readCount) {
break;
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.Socket;

public class OldIOClient {

public static void main(String[] args) throws Exception {
Socket socket = new Socket("localhost", 8899);

String fileName = "~/Desktop/spark-2.2.0-bin-hadoop2.7.tgz";
InputStream inputStream = new FileInputStream(fileName);

DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());

byte[] buffer = new byte[4096];
long readCount;
long total = 0;

long startTime = System.currentTimeMillis();

while ((readCount = inputStream.read(buffer)) >= 0) {
total += readCount;
dataOutputStream.write(buffer);
}

// 200多MB
// 400多ms
System.out.println("发送总字节数: " + total + ", 耗时: " + (System.currentTimeMillis() - startTime));

dataOutputStream.close();
socket.close();
inputStream.close();
}
}

零拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class NewIOServer {

public static void main(String[] args) throws Exception{
InetSocketAddress address = new InetSocketAddress(8899);

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverSocketChannel.socket();
// 连接关闭之后会有超时状态,超时时候就可以重用了
// 端口号处于time wait的时候是不可以绑定连接的,true就可以绑定,即便是time out状态
// 处于time wait就可以重用
serverSocket.setReuseAddress(true);
serverSocket.bind(address);

// 字节数数组的大小是4096
ByteBuffer byteBuffer = ByteBuffer.allocate(4096);

while (true) {
// false 非阻塞,没有Selector,所以就要用阻塞的
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(true);

int readCount = 0;

while (-1 != readCount) {
try {
// position到最后没法read了
readCount = socketChannel.read(byteBuffer);
} catch (Exception ex) {
ex.printStackTrace();
}

//
// 磁带倒带,Mark丢弃,position变为0.
// rewind与flip的用法差异
// https://blog.csdn.net/yiifaa/article/details/77652914
byteBuffer.rewind();
}
}

}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.io.FileInputStream;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;

public class NewIOClient {

public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8899));
socketChannel.configureBlocking(true);

String fileName = "~/Desktop/spark-2.2.0-bin-hadoop2.7.tgz";

FileChannel fileChannel = new FileInputStream(fileName).getChannel();

long startTime = System.currentTimeMillis();

// 使用 FileChannel transferTo 就是写到 socketChannel
// 传递多少就是 0, fileChannel.size(),
// transferTo 比简单的循环效率更高,很多OS会直接从文件系统缓存直接写到目标Channel。不会从源Channel拷贝到目标
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);


// 149ms
System.out.println("发送总字节数:" + transferCount + ",耗时: " + (System.currentTimeMillis() - startTime));

fileChannel.close();
}
}

磁盘上文件修改一下发给用户

内存映射

文件内容映射,用户空间直接操作内核空间的文件

java里面的MappedByteBuffer就是这个在支持