什么是寫半包
寫半包:一份數據,一次發送沒有把他全部發送,需要循環發送,那么第一次的操作稱為寫半包
什么情況下會出現寫半包:
發送方發送200byte,但是接收方只能接受100byte,因此發送方只會發送小于100byte的數據。
說到這里,機智的小伙伴已經想到了這跟TCP滑動窗口和消息中間件中常見的消息堆積是一個道理。
總的來說:接收方頂不住來自發送方的數據壓力。
對?.NETty來說就是,這個時刻TCP發送緩沖區滿了,無法再接收整包數據,剩下的數據則會通過Channel去監聽寫操作,當觸發寫操作的時候,再把這部分數據給帶上,那么這部分數據才完整地傳輸。
Netty中的寫半包處理
前提知識:Netty中的網絡數據讀寫,都先經過ByteBuf
- 讀操作:從
ByteBuf
中讀取數據 - 寫操作:將數據寫入到
ByteBuf
,然后再通過其他方式把ByteBuf的數據寫入(#doWrite
)
Netty中的網路操作都是通過Channel
和里面聚合的對象Unsafe
對象進行操作,簡單介紹一下。
Channel
Channel的作用:給Netty用來進行網絡網絡
JDK 也有自己原生的Channel,但是為了方便框架擴展使用,Netty采用的是封裝了一層Facade
(門面模式)。
最重要的是能夠支持Netty的自定義Channel來應對不同的業務場景。
Channel會被注冊到EventLoop
上,在注冊的時候定義好感興趣的事件,他采用的是基于事件觸發的方式,當Channel上觸發相對應的事件時,就會主動回調通知,然后交給對應的ChannelHandler
進行處理。
由于本篇講的是寫半包,因此不再過多解釋。
總的來說: Channel就是Netty用來處理網絡數據流的
回到本篇的主題:寫半包
AbstractNioByteChannel
主要負責處理寫半包
總的流程如圖:
ChannelOutboundBuffer:環形發送數組
-
不停地從ChannelOutboundBuffer讀取數據,看是否有可以發送的數據
-
如果有,并且是ByteBuf類型的,可以選擇發送數據
- 如果一次發送沒有發送完,則采取一定次數的循環發送(寫半包)
-
數據最后還是沒有發送完,則會開一條新線程專門進行剩余數據的發送
-
在最后會去同步數據寫入進度
源碼解析 #doWrite
不停地去環形發送數組里面取數據出來
- 如果是空了,代表發送完了,把寫標志位置空(
clearOpWrite
)
如果不是空數據,則判斷是不是ByteBuf
數據
- 對其進行強轉,若可讀字節數是0,代表消息不可讀(reidIndex >= writeIndex),則把他在環形發送數組中移除。
第一次讀的時候,會先去獲取循環發送次數writeSpinCount
。循環發送次數就是指:第一次發送沒有完成時(寫半包)進行循環發送的次數。
給他設置一個閾值,為的就是當循環發送的時候,IO線程會一直嘗試寫操作,此時IO線程無法處理其他操作,相當于局部阻塞、死鎖、假死的情況。
像這種處理手法非常常見,比如一般我們會給分布式鎖設置一個鎖的超時時間,除此之外還需要設置一個客戶端的超時時間,避免客戶端在拿到鎖的時候,這把鎖已經過期了。客戶端的超時時間會比鎖的超時時間要短。
然后就是進行循環發送了
消息發送操作完成時候,會調用ChannelOutboundBuffer更新發送進度的消息,并且還會判斷是否需要寫半包處理
如果沒有發完,則設置寫半包標識位,啟動專門的寫半包線程繼續發后續的消息
總結
寫半包問題本質上是:對于接收方來說,來自發送方的數據壓力太大了,因此不得不采取的一種降保護措施
可以在發送方進行解決、也可以在接收方進行解決
Netty并沒有采取說,遇到TCP緩沖區滿了之后,這個數據包就等下一次再等發,而是能發多少就發多少,不夠的 下次再發,是一種追求性能的選擇。
像消息中間件遇到消息堆積問題,在消接收方(消費者)增大消費的速度,比如:加消費隊列或擴充消費者群組等。
又或者限制發送方(生產者)的發送速度,比如TCP的滑動窗口。
所以互聯想的技術都是有相關聯的,能看到互相的影子。