如何編寫一個 SendFile 服務器

如何編寫一個 SendFile 服務器

前言

以前討論零拷貝的時候,咱們知道,兩臺機器之間傳輸文件,最快的方式就是 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 模型。

設計

IO 模型設計

設計圖:

如上圖,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 的數據包,會進行解碼反序列化,並喚醒阻塞在客戶端的線程。從而完成一次調用。

線程模型

設計圖:

image-20191029093524267

如上圖所示。

在 client 端:

每一個 Client 實例,維護一個 TCP 鏈接。該 Client 的寫入方法是線程安全的。

當用戶併發寫入時,可併發寫的同時併發回覆,由於寫和回覆是異步的(此時可能會出現,線程 A 先 send ,線程 B 後 send,但因爲網絡延遲,B 先返回)。

在 server 端:

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 寫進磁盤。

RPC 協議

Server RPC 回覆包協議

字段名稱 字段長度(byte) 字段做用
magic_num 4 魔數校驗,fast fail
version 1 rpc 協議版本
id 8 Request id, TCP 多路複用 id
length 8 rpc 實際消息內容的長度
Content length rpc 實際消息內容(JSON 序列化協議)

Client RPC 發送包協議

字段名稱 字段長度(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 滿載。

image-20191029120446781

代碼地址:https://github.com/stateIs0/send_file

同時,歡迎你們 star, pr,issue。我來改進。

相關文章
相關標籤/搜索