深入分析通過Socket進行數據文件傳遞中的傳統IO的弊端以及NIO的零拷貝實現原理,及用戶空間和內核空間的切換方式
傳統的IO流程
在這個過程中:
- 數據從磁盤拷貝進內核空間緩沖區
- 從內核空間緩沖區拷貝到用戶空間緩沖區
- 從用戶空間緩沖區拷貝回內核空間緩沖區
- 在從內核空間緩沖區拷貝到socket的緩沖區
- 由Socket緩存區傳遞給數據發送引擎發送
第三步的必要性:
IO操作涉及到本地方法,java擔心,當使用native本地方法對堆內數組進行操作時發生GC, 因為堆內內存是受JVM影響的,一旦發生了垃圾回收機制就使得全部數據都是錯亂的,而堆外內存是不受JVM控制的.
就這樣, 前前后后一共發生了4次數據的拷貝,用戶空間模式和內核空間模式來回切換了4次, 其中用戶空間參與的第二次和第三次拷貝並沒有對數據進行任何改動,它僅僅是起到了中轉的作用; 這恰恰是傳統的IO的局限性
NIO的零拷貝
在NIO的數據傳遞模型中可以看到,用戶明顯少了用戶空間緩沖區緩存數據的步驟, 減少了兩次不必要的數據的拷貝,以及不必要的上下文切換, 具體如下:
- 數據從磁盤寫入內核空間緩沖區
- 再從內核空間緩沖區寫入到Socket緩沖區
- 由Socket緩存區傳遞給數據發送引擎發送
然而這個模型中仍然有問題存在,在內核空間緩沖區中仍然存在數據的拷貝
- 數據從內核空間緩沖區拷貝進了Socket緩沖區
這種現狀也是有辦法解決的
在2.X版本的linux中,NIO的零拷貝模型如下:
這個模型中充分利用了Scatter/Gather 分散和匯聚的特性
這張圖是最完美的零拷貝模型,
- 首先文件從磁盤中加載進內核空間緩沖區
- CPU將內核空間緩沖區存儲的數據的adress以及數據的大小存放進Socket
- 協議引擎根據socket提供的數據的描述,直接去內核緩沖區取出數據
第2步 一個完整的可用的buffer被分散在兩個buffer中, 可以理解成是一個分散的過程 Scatter
第3步 操作系統去收集buffer,可以理解成一個Gather的過程
從而實現了真正的零拷貝
回到Java
除了上面的第一張圖片以外,其他圖片中數據全部在內核緩沖區,這部分空間對於人來說其實是一個黑盒,於是java提供了封裝類幫我們和這塊黑盒打交道
mappedByteBuffer
這是他的繼承體系,和HeadByteBuffer位於同一級,我們稱它為內存映射文件 他是通道的調用map()方法得來的, 這個mappedByteBuffer相對於普通的buffer而言,他並沒有板板整整的維護自己的數組,相反直接關聯着堆外內存,針對它的任何修改,操作系統都會自動的同步到文件中
如下修改內存buffer,卻更新了文件
RandomAccessFile randomAccessFile = new RandomAccessFile("123.txt", "rw"); //class sun.nio.ch.FileChannelImpl
FileChannel channel = randomAccessFile.getChannel();
System.out.println(channel.getClass());
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
// todo 接下來我們直接修改內存中的內容就行了,不需要修改文件
mappedByteBuffer.put(0, (byte) 'a');
mappedByteBuffer.put(3, (byte) 'b');
randomAccessFile.close();
channel.close();
關於FileChannel.MapMode
文件通道的映射模型 是個枚舉:
- PRIVATE
- READ_WRITE
- READ_ONLY
當我們想構建read_write類型的只能使用 RandomAccessFile類型的文件stream, 通過它的rw參數,設置為可讀寫的類型
關於ByteBuffer
的ByteBuffer.allocateDirect()
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
{
...
}
最常用的ByteBuffer的allocateDirect()
底層使用同樣是MappedBytebuffer的實現類,DirectByteBuffer,這個對象相對於HeapByteBuffer
來說,他並沒有初始化父類ByteBuffer
中的數組,但是它使用了超類BUffer
中的Long類型的adress
關鍵字
adress
關鍵字的作用是 存放了一個堆外的地址,這個地址標記着一個堆外數組的位置,使得java可以使用unsafe
類下的本地方法,操作adress
標記的堆外內存,這樣就省去了在第一張圖片中的還要把堆內數組拷貝到堆外再進行讀寫的弊端,實現了零拷貝
scattering 和 gathering在NIO編程中的體現
scattering是一個分散的過程,即把一整塊數據分散在不同的buffer中,而gathering與之相反,是一個聚集的過程,只有搜集全所有的全部的buffer得到的數據才是有意義的
例子: 自定義網絡協議 將請求頭分裝成多個緩存buffer中,實現了天然的解析
ByteBuffer[] byteBuffers = new ByteBuffer[3];
byteBuffers[0] = ByteBuffer.allocate(2);
byteBuffers[1] = ByteBuffer.allocate(3);
byteBuffers[2] = ByteBuffer.allocate(4);
SocketChannel client = serverSocketChannel.accept();
long read = client.read(byteBuffers);