JAVA高級架構 https://mp.weixin.qq.com/s?src=11×tamp=1542107581&ver=1242&signature=OoktA0SsUeMrRekqrnaOvpssGtwbcaLe4swyHdYH1taWGQ2t4lIE0idzw2VuqIKrMBNpQUcoBrk1TKt7WTheHVWHT4hzzWVd8gBKPW53qvkaBcXLB9vH732VIGfbLMGG&new=1
結合RPC框架通信談 netty如何解決TCP粘包問題
0.起因
因為自己造一個RPC框架的輪子時,需要解決TCP的粘包問題,特此記錄,希望方便他人。這是我寫的RPC框架的 GitHub地址 https://github.com/yangzhenkun/krpc。
歡迎star,fork。已經寫了多篇文章對這個框架的原理進行說明。對原理有興趣的歡迎交流。
1.什么是粘包
1.1 什么是TCP粘包
TCP粘包就是在TCP數據傳輸過程中,因為某些原因,接收方收到讀取的數據並不是但存的一次數據,而是多個數據包的字節流組裝在一起,導致多個數據粘在一起,接收端在讀取的時候不知道怎么樣把數據分成預期的多組數據,這就是粘包。
1.2 形成原因
TCP之所以造成粘包現象是因為其發送端和接收端的緩沖區及TCP數據流引起的。
例如nagle算法,會將瞬間的很多小包數據拼裝稱一個大的數據,以提高的帶寬的利用率。(具體nagle算法就不展開將)。
但即使關閉了nagle算法,粘包依舊存在。因為這不是造成tcp粘包的根本原因。因為有緩沖區的存在,在緩存區沒有打滿之前是不會發送出去的,同時接收端也是利用緩存區接收數據,在接着從緩存區讀取接收的數據解析。這時有人問,如果數據量很小,總是沒有打滿緩沖區那怎么辦,這就依賴發送和接收端的定時器了,他們會定時的處理數據,要不這不就成了bug了。
就是因為緩沖區的存在以及tcp數據流的形式,造成了多組數據的拼接,形成了粘包,半 包問題。
1.3 如何解決
目前常用的方法是定義 起始 邊屆符+數據長度來告訴接收端一個數據包具體的長度。
不過也有定義固定長度的,不過這樣可能會造成的空白字節的浪費以及超出長度這種不易擴展的方式。純邊界符的方式 怕發生實際消息體與邊界符的碰撞,造成消息的誤截斷。
2.netty如何解決
netty對NIO模式的TCP通信的封裝可謂是完美。可讓人快速寫出可用的tcp通信的服務端和客戶端,並且很簡單的解決粘包問題。
netty有提供基於分隔符和長度的編解碼器,方便開發者使用。
DelimiterBaseFrameDecoder是可以用戶自定義數據分隔符來分割的,LineBaseFrameDecoder是由行尾符(\n或者\r\n)分割,速度比前者還要塊。
還有基於長度的FixedLengthFrameDecoder定長的解碼器,LengthFieldBasedFrameDecoder動態長度的解碼器。這4中方式都有對應的編解碼器。
同時對於數據類型的邊界嗎,netty也支持byte,string,protobuf等,大家可以去看MessageToMessageDecoder的子類,就能發現netty提供很多編解碼的規則。
3.實戰-RPC框架的客戶和服務端實現
在自己寫KRPC時,一開始沒有把NIO的計划提這么早,奈何在第一版用同步IO寫客戶端,壓測時發現竟然那么不堪,遂決定用NIO改寫,一開始覺得用Netty寫客戶端不方便(當時沒到怎么寫),便決定用java原生的NIO來寫客戶端,寫到最后發現處理粘包特別困難,需要自己定義 特殊分界符號,然后設置長度,在客戶端和服務端解析起來特別繁雜。於是嘗試用netty寫,發現特別簡單。
bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
pipeline.addLast("decoder", new ByteArrayDecoder());
pipeline.addLast("encoder", new ByteArrayEncoder());
pipeline.addLast(new ServerHandler());
}
}).option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(64,Global.getInstance().getMaxBuf(),Global.getInstance().getMaxBuf()));
這就是服務端的代碼,有沒有特別簡單,因為TCP將傳輸的數據序列化由壓縮后的數據為 字節數組,所以使用的自帶的ByteArray編解碼器,使用了動態長度的LengthFieldBaseFrame來解決粘包問題。就這樣解決了粘包問題。