騰訊云服務器上傳網站b站視頻推廣網站2023
傳輸(Transport)
在網絡中傳遞的數據總是具有相同的類型:字節(jié)。 這些字節(jié)流動的細節(jié)取決于網絡傳輸,它是一個幫我們抽象 底層數據傳輸機制的概念,我們不需要關心字節(jié)流動的細節(jié),只需要確保字節(jié)被可靠的接收和發(fā)送。
當我們使用Java網絡編程時,可能會接觸到多種不同的網絡IO模型,如NIO,BIO(OIO: Old IO),AIO等,我們可能因為 使用這些不同的API而遇到問題。 Netty則為這些不同的IO模型實現了一個通用的API,我們使用這個通用的API比直接使用JDK提供的API要 簡單的多,且避免了由于使用不同API而帶來的問題,大大提高了代碼的可讀性。 在傳輸這一部分,我們將主要學習這個通用的API,以及它與JDK之間的對比。
傳輸API
傳輸API的核心是Channel(io.netty.Channel,而非java nio的Channel)接口,它被用于所有的IO操作。
Channel結構層次:
每個Channel都會被分配一個ChannelPipeline和ChannelConfig, ChannelConfig包含了該Channel的所有配置,并允許在運行期間更新它們。
ChannelPipeline在上面已經介紹過了,它存儲了所有用于處理出站和入站數據的ChannelHandler, 我們可以在運行時根據自己的需求添加或刪除ChannelPipeline中的ChannelHandler。
此外,Channel還有以下方法值得留意:
方法名 | 描述 |
---|---|
eventLoop | 返回當前Channel注冊到的EventLoop |
pipeline | 返回分配給Channel的ChannelPipeline |
isActive | 判斷當前Channel是活動的,如果是則返回true。 此處活動的意義依賴于底層的傳輸,如果底層傳輸是TCP Socket,那么客戶端與服務端保持連接便是活動的;如果底層傳輸是UDP Datagram,那么Datagram傳輸被打開就是活動的。 |
localAddress | 返回本地SocketAddress |
remoteAddress | 返回遠程的SocketAddress |
write | 將數據寫入遠程主機,數據將會通過ChannelPipeline傳輸 |
flush | 將之前寫入的數據刷新到底層傳輸 |
writeFlush | 等同于調用 write 寫入數據后再調用 flush 刷新數據 |
Netty內置的傳輸
Netty內置了一些開箱即用的傳輸,我們上面介紹了傳輸的核心API是Channel,那么這些已經封裝好的 傳輸也是基于Channel的。
Netty內置Channel接口層次:
名稱 | 包 | 描述 |
---|---|---|
NIO | io.netty.channel.socket.nio | NIO Channel基于java.nio.channels,其io模型為IO多路復用 |
Epoll | io.netty.channel.epoll | Epoll Channel基于操作系統(tǒng)的epoll函數,其io模型為IO多路復用,不過Epoll模型只支持在Linux上的多種特性,比NIO性能更好 |
KQueue | io.netty.channel.kqueue | KQueue 與 Epoll 相似,它主要被用于 FreeBSD 系統(tǒng)上,如Mac等 |
OIO(Old Io) | io.netty.channel.socket.oio | OIO Channel基于java.net包,其io模型是阻塞的,且此傳輸被Netty標記為deprecated,故不推薦使用,最好使用NIO / EPOLL / KQUEUE 等傳輸 |
Local | io.netty.channel.local | Local Channel 可以在VM虛擬機內部進行本地通信 |
Embedded | io.netty.channel.embedded | Embedded Channel允許在沒有真正的網絡傳輸中使用ChannelHandler,可以非常有用的測試ChannelHandler |
零拷貝
理解零拷貝 零拷貝是Netty的重要特性之一,而究竟什么是零拷貝呢? WIKI中對其有如下定義:
“Zero-copy” describes computer operations in which the CPU does not perform the task of copying data from one memory area to another.
從WIKI的定義中,我們看到“零拷貝”是指計算機操作的過程中,CPU不需要為數據在內存之間的拷貝消耗資源。而它通常是指計算機在網絡上發(fā)送文件時,不需要將文件內容拷貝到用戶空間(User Space)而直接在內核空間(Kernel Space)中傳輸到網絡的方式。
Non-Zero Copy方式:
應用程序啟動后,向內核發(fā)出read調用(用戶態(tài)切換到內核態(tài)),操作系統(tǒng)收到調用請求后, 會檢查文件是否已經緩存過了,如果緩存過了,就將數據從緩沖區(qū)(直接內存)拷貝到用戶應用進程(內核態(tài)切換到用戶態(tài)), 如果是第一次訪問這個文件,則系統(tǒng)先將數據先拷貝到緩沖區(qū)(直接內存),然后CPU將數據從緩沖區(qū)拷貝到應用進程內(內核態(tài)切換到用戶態(tài)), 應用進程收到內核的數據后發(fā)起write調用,將數據拷貝到目標文件相關的堆棧內存(用戶態(tài)切換到內核態(tài)), 最后再從緩存拷貝到目標文件。
根據上面普通拷貝的過程我們知道了其缺點主要有:
- 用戶態(tài)與內核態(tài)之間的上下文切換次數較多(用戶態(tài)發(fā)送系統(tǒng)調用與內核態(tài)將數據拷貝到用戶空間)。
- 拷貝次數較多,每次IO都需要DMA和CPU拷貝。
而零拷貝正是針對普通拷貝的缺點做了很大改進,使得其拷貝速度在處理大數據的時候很是出色。
Zero Copy方式:
從上圖中可以清楚的看到,Zero Copy的模式中,避免了數據在用戶空間和內存空間之間的拷貝,從而提高了系統(tǒng)的整體性能。Linux中的sendfile()
以及Java NIO中的FileChannel.transferTo()
方法都實現了零拷貝的功能,而在Netty中也通過在FileRegion
中包裝了NIO的FileChannel.transferTo()
方法實現了零拷貝。
零拷貝主要有兩種實現技術:
- 內存映射(mmp)
- 文件傳輸(sendfile)
可以參照我編寫的demo進行接下來的學習:
zerocopy
內存映射(Memory Mapped)
內存映射對應JAVA NIO的API為
FileChannel.map。
當用戶程序發(fā)起 mmp 系統(tǒng)調用后,操作系統(tǒng)會將文件的數據直接映射到內核緩沖區(qū)中, 且緩沖區(qū)會與用戶空間共享這一塊內存,這樣就無需將數據從內核拷貝到用戶空間了,用戶程序接著發(fā)起write 調用,操作系統(tǒng)直接將內核緩沖區(qū)的數據拷貝到目標文件的緩沖區(qū),最后再將數據從緩沖區(qū)拷貝到目標文件。
其過程如下:
內存映射由原來的四次拷貝減少到了三次,且拷貝過程都在內核空間,這在很大程度上提高了IO效率。
但是mmp也有缺點: 當我們使用mmp映射一個文件到內存并將數據write到指定的目標文件時, 如果另一個進程同時對這個映射的文件做出寫的操作,用戶程序可能會因為訪問非法地址而產生一個錯誤的信號從而終止。
試想一種情況:我們的服務器接收一個客戶端的下載請求,客戶端請求的是一個超大的文件,服務端開啟一個線程 使用mmp和write將文件拷貝到Socket進行響應,如果此時又有一個客戶端請求對這個文件做出修改, 由于這個文件先前已經被第一個線程mmp了,可能第一個線程會因此出現異常,客戶端也會請求失敗。
解決這個問題的最簡單的一種方法就對這個文件加讀寫鎖,當一個線程對這個文件進行讀或寫時,其他線程不能操作此文件, 不過這樣處理并發(fā)的能力可能就大打折扣了。
文件傳輸(SendFile)
文件傳輸對應JAVA NIO的API為
FileChannel.transferFrom/transferTo
在了解sendfile之前,先來看一下它的函數原型(linux系統(tǒng)的同學可以使用 man sendfile 查看):
#include<sys/sendfile.h>ssize_t sendfile(int out_fd,int in_fd,off_t *offset,size_t count);
sendfile在代表輸入文件的文件描述符 in_fd 和 輸入文件的文件描述符 out_fd 之間傳輸文件內容, 這個傳輸過程完全是在內核之中進行的,程序只需要把輸入文件的描述符和輸出文件的描述符傳遞給 sendfile調用,系統(tǒng)自然會完成拷貝。 當然,sendfile和mmp一樣都有相同的缺點,在傳輸過程中, 如果有其他進程截斷了這個文件的話,用戶程序仍然會被終止。
sendfile傳輸過程如下:
它的拷貝次數與mmp一樣,但是無需像mmp一樣與用戶進程共享內存了。