以前討論零拷貝的時候,咱們知道,兩臺機器之間傳輸文件,最快的方式就是 send file,衆所周知,在 Java 中,該技術對應的則是 FileChannel 類的 transferTo 和 transferFrom 方法。react
在平時使用服務器的時候,好比 nginx ,tomcat ,都有 send file 的選項,利用此技術,可大大提升文件傳輸效能。nginx
另外,可能也有人談論 send file 的缺點,例如不能利用 gzip 壓縮,不能加密。這裏本文不作探討。git
紙上得來終覺淺,絕知此事要躬行。github
那麼,如何使用這兩個 api 實現一個 send file 服務器和客戶端呢?api
想象一下,你寫的 send file 服務器利用 send file 技術,利用萬兆網卡,從各個 client 端 copy 海量文件,瞬間打爆你那 1TB 的磁盤和 48核的 CPU。而且,注意:只需很小的 JVM 內存就能夠實現這樣一臺強悍的服務器。爲何?若是你知道 send file 的原理,就會知道,使用 send file 技術時, 在用戶態中,是不須要多少內存的,數據都在內核態。數組
是否是頗有成就感?什麼?沒有?那打擾了 🤣。緩存
另外,關於 send file,咱們都知道,因爲是直接從內核緩衝區進入到網卡驅動,咱們幾乎能夠稱之爲 「零拷貝」,他的性能十分強勁。tomcat
可是。安全
除了這個,還有其餘的嗎?答案是有的,send file 利用 DMA 的方式 copy 數據,而不是利用 CPU。注意,不利用 CPU 意味着什麼?意味着數據不會進入「緩存行」,進一步,不會進入緩存行,表明着緩存行不會由於這個被污染,再進一步,就是不須要維護緩存一致性。性能優化
還記得咱們由於這個特性搞的那些關於 「僞共享」 的各類黑科技嗎?是否是又學到了一點呢?😎
做爲一個純粹的,高尚的,有趣的 sendFile 服務器或者客戶端,使用場景是嵌入到某個服務中,或者某個中間件中,不須要搞成誇張的容器。咱們能夠借鑑一下,客戶端能夠作成 Jedis 那樣的,若是你想搞個鏈接池也不是不能夠,但 client 自身實例,仍是單鏈接的。服務端能夠作成 sun 的 httpServer 那種輕量的,隨時啓動,隨時關閉。
同時, 支持 oneway 的高性能發送,由於,只要機器不宕機,發送到網卡就意味着發送成功,這樣能大幅提升發送速度,減小客戶端阻塞時間。
另外,也支持帶有 ack 的穩定發送,即只有返回 ack 了,才能確認數據已經寫到目標服務器磁盤了。
server 端支持海量鏈接,必須得是 reactor 網絡模型,但咱們不想在這麼小的組件裏用 netty,過重了,還容易和使用方有 jar 衝突。因此,咱們能夠利用 Java 的 selector + nio 本身實現 Reactor 模型。
設計圖:
如上圖,Server 端支持海量客戶端鏈接。
server 端含有 多個處理器,其中包括 accept 處理器,read 處理器 group, write 處理器 group。
accept 處理器將 serverSocketChannel 做爲 key 註冊到一個單獨的 selector 上。專門用於監聽 accept 事件。相似 netty 的 boss 線程。
當 accept 處理器成功鏈接了一個 socket 時,會隨機將其交給一個 readProcessor(netty worker 線程?) 處理器,readProcessor 又會將其註冊到 readSelector 上,當發生 read 事件時,readProcessor 將接受數據。
能夠看到,readProcessor 能夠認爲是一個多路複用的線程,利用 selector 的能力,他高效的管理着多個 socket。
readProcessor 在讀到數據後,會將其寫入到磁盤中(DMA 的方式,性能炸裂)。
而後,若是 client 在 RPC 協議中聲明「須要回覆(id 不爲 -1)」 時,那就將結果發送到 Reply Queue 中,反之沒必要。
當結果發送到 Reply Queue 後,writer 組中的 寫線程,則會從 Queue 中拉取回復包,而後將結果按照 RPC 協議,寫回到 client socket 中。
client socket 也會監聽着 read 事件,注意:client 是不須要 select 的,由於不必,selector 只是性能優化的一種方式——即一個線程管理海量鏈接,若是沒有 select, 應用層沒法用較低的成本處理海量鏈接,注意,不是不能處理,只是不能高效處理。
回過來,當 client socket 獲得 server 的數據包,會進行解碼反序列化,並喚醒阻塞在客戶端的線程。從而完成一次調用。
設計圖:
如上圖所示。
每一個 Client 實例,維護一個 TCP 鏈接。該 Client 的寫入方法是線程安全的。
當用戶併發寫入時,可併發寫的同時併發回覆,由於寫和回覆是異步的(此時可能會出現,線程 A 先 send ,線程 B 後 send,但因爲網絡延遲,B 先返回)。
server 端維護着一個 ServerSocketChannel 實例,該實例的做用就是接收 accep 事件,且由一個線程維護這個 accept selector 。
當有新的 client 鏈接事件時,accept selector 就將這個鏈接「交給「 read 線程(默認 server 有 4 個 read 線程)。
注意:每一個 read 線程都維護着一個單獨的 selector。 4 個 read 線程,就維護了 4 個 selector。
當 accept 獲得新的客戶端鏈接時,先從 4 個read 線程組裏 get 一個線程,而後將這個 客戶端鏈接 做爲 key 註冊到這個線程所對應的 read selector 上。從而將這個 Socket 「交給」 read 線程。
而這個 read 線程則使用這個 selector 輪詢事件,若是 socket 可讀,那麼就進行讀,讀完以後,利用 DMA 寫進磁盤。
字段名稱 | 字段長度(byte) | 字段做用 |
---|---|---|
magic_num | 4 | 魔數校驗,fast fail |
version | 1 | rpc 協議版本 |
id | 8 | Request id, TCP 多路複用 id |
length | 8 | rpc 實際消息內容的長度 |
Content | length | rpc 實際消息內容(JSON 序列化協議) |
字段名稱 | 字段長度(byte) | 字段做用 |
---|---|---|
magic_num | 4 | 魔數校驗,fast fail |
id | 8 | Request id, TCP 多路複用 id, 默認 -1,表示不回覆 |
nameContent | 2 | Request id, TCP 多路複用 id |
bodyLength | 8 | rpc 實際消息內容的長度 |
nameContent | bodyLength | 文件名 UTF-8 數組 |
爲何 發送包和返回包協議不一樣?爲了高效。
注意:這是一個能用的,性能不錯的,輕量的 SendFile 服務器實現,本地測試時, IO寫盤達到 824MB/S,4c 4.2g inter i7 CPU 滿載。
代碼地址:https://github.com/stateIs0/send_file
同時,歡迎你們 star, pr,issue。我來改進。