1、TCP協(xié)議傳輸過程
TCP協(xié)議是面向流的協(xié)議,是流式的,沒有業(yè)務(wù)上的分段,只會(huì)根據(jù)當(dāng)前套接字緩沖區(qū)的情況進(jìn)行拆包或者粘包:
發(fā)送端的字節(jié)流都會(huì)先傳入緩沖區(qū),再通過網(wǎng)絡(luò)傳入到接收端的緩沖區(qū)中,最終由接收端獲取。
2、TCP粘包和拆包概念
因?yàn)門CP會(huì)根據(jù)緩沖區(qū)的實(shí)際情況進(jìn)行包的劃分,在業(yè)務(wù)上認(rèn)為,有的包被拆分成多個(gè)包進(jìn)行發(fā)送,也可能多個(gè)曉小的包封裝成一個(gè)大的包發(fā)送,這就是TCP的粘包或者拆包。
3、TCP粘包和拆包圖解
假設(shè)客戶端分別發(fā)送了兩個(gè)數(shù)據(jù)包D1和D2給服務(wù)端,由于服務(wù)端一次讀取到字節(jié)數(shù)是不確定的,故可能存在以下幾種情況:
- 服務(wù)端分兩次讀取到兩個(gè)獨(dú)立的數(shù)據(jù)包,分別是D1和D2,沒有粘包和拆包。
- 服務(wù)端一次接收到了兩個(gè)數(shù)據(jù)包,D1和D2粘在一起,發(fā)生粘包。
- 服務(wù)端分兩次讀取到數(shù)據(jù)包,第一次讀取到了完整的D1包和D2包的部分內(nèi)容,第二次讀取到了D2包的剩余內(nèi)容,發(fā)生拆包。
- 服務(wù)端分兩次讀取到數(shù)據(jù)包,第一次讀取到部分D1包,第二次讀取到剩余的D1包和全部的D2包。
當(dāng)TCP緩存再小一點(diǎn)的話,會(huì)把D1和D2分別拆成多個(gè)包發(fā)送。
4、TCP粘包和拆包解決策略
因?yàn)門CP只負(fù)責(zé)數(shù)據(jù)發(fā)送,并不處理業(yè)務(wù)上的數(shù)據(jù),所以只能在上層應(yīng)用協(xié)議棧解決,目前的解決方案歸納:
- 消息定長,每個(gè)報(bào)文的大小固定,如果數(shù)據(jù)不夠,空位補(bǔ)空格。
- 在包的尾部加回車換行符標(biāo)識(shí)。
- 將消息分為消息頭與消息體,消息頭中包含消息總長度。
- 設(shè)計(jì)更復(fù)雜的協(xié)議。
5、Netty中的解決辦法
Netty提供了多種默認(rèn)的編碼器解決粘包和拆包:
5.1、LineBasedFrameDecoder
基于回車換行符的解碼器,當(dāng)遇到"\n"或者 "\r\n"結(jié)束符時(shí),分為一組。支持?jǐn)y帶結(jié)束符或者不帶結(jié)束符兩種編碼方式,也支持配置單行的最大長度。
LineBasedFrameDecoder與StringDecoder搭配時(shí),相當(dāng)于按行切換的文本解析器,用來支持TCP的粘包和拆包。
使用例子:
private void start() throws Exception {
//創(chuàng)建 EventLoopGroup
NioEventLoopGroup group = new NioEventLoopGroup();
NioEventLoopGroup work = new NioEventLoopGroup();
try {
//創(chuàng)建 ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
b.group(group, work)
//指定使用 NIO 的傳輸 Channel
.channel(NioServerSocketChannel.class)
//設(shè)置 socket 地址使用所選的端口
.localAddress(new InetSocketAddress(port))
//添加 EchoServerHandler 到 Channel 的 ChannelPipeline
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
p.addLast(new LineBasedFrameDecoder(1024));
p.addLast(new StringDecoder());
p.addLast(new StringEncoder());
p.addLast(new EchoServerHandler());
}
});
//綁定的服務(wù)器;sync 等待服務(wù)器關(guān)閉
ChannelFuture f = b.bind().sync();
System.out.println(EchoServer.class.getName() + " started and listen on " + f.channel().localAddress());
//關(guān)閉 channel 和 塊,直到它被關(guān)閉
f.channel().closeFuture().sync();
} finally {
//關(guān)機(jī)的 EventLoopGroup,釋放所有資源。
group.shutdownGracefully().sync();
}
}
注意ChannelPipeline 中ChannelHandler的順序,
5.2、DelimiterBasedFrameDecoder
分隔符解碼器,可以指定消息結(jié)束的分隔符,它可以自動(dòng)完成以分隔符作為碼流結(jié)束標(biāo)識(shí)的消息的解碼?;剀嚀Q行解碼器實(shí)際上是一種特殊的DelimiterBasedFrameDecoder解碼器。
使用例子(后面的代碼只貼ChannelPipeline部分):
ChannelPipeline p = ch.pipeline();
p.addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("制定的分隔符".getBytes())));
p.addLast(new StringDecoder());
p.addLast(new StringEncoder());
p.addLast(new EchoServerHandler());
5.3、FixedLengthFrameDecoder
固定長度解碼器,它能夠按照指定的長度對消息進(jìn)行自動(dòng)解碼,當(dāng)制定的長度過大,消息過短時(shí)會(huì)有資源浪費(fèi),但是使用起來簡單。
ChannelPipeline p = ch.pipeline();
p.addLast(new FixedLengthFrameDecoder(1 << 5));
p.addLast(new StringDecoder());
p.addLast(new StringEncoder());
p.addLast(new EchoServerHandler());
5.4、LengthFieldBasedFrameDecoder
通用解碼器,一般協(xié)議頭中帶有長度字段,通過使用LengthFieldBasedFrameDecoder傳入特定的參數(shù),來解決拆包粘包。
io.netty.handler.codec.LengthFieldBasedFrameDecoder的實(shí)例化:
/**
* Creates a new instance.
*
* @param maxFrameLength 最大幀長度。也就是可以接收的數(shù)據(jù)的最大長度。如果超過,此次數(shù)據(jù)會(huì)被丟棄。
* @param lengthFieldOffset 長度域偏移。就是說數(shù)據(jù)開始的幾個(gè)字節(jié)可能不是表示數(shù)據(jù)長度,需要后移幾個(gè)字節(jié)才是長度域。
* @param lengthFieldLength 長度域字節(jié)數(shù)。用幾個(gè)字節(jié)來表示數(shù)據(jù)長度。
* @param lengthAdjustment 數(shù)據(jù)長度修正。因?yàn)殚L度域指定的長度可以是header+body的整個(gè)長度,也可以只是body的長度。如果表示header+body的整個(gè)長度,那么我們需要修正數(shù)據(jù)長度。
* @param initialBytesToStrip 跳過的字節(jié)數(shù)。如果你需要接收header+body的所有數(shù)據(jù),此值就是0,如果你只想接收body數(shù)據(jù),那么需要跳過header所占用的字節(jié)數(shù)。
* @param failFast 如果為true,則在解碼器注意到幀的長度將超過maxFrameLength時(shí)立即拋出TooLongFrameException,而不管是否已讀取整個(gè)幀。
* 如果為false,則在讀取了超過maxFrameLength的整個(gè)幀之后引發(fā)TooLongFrameException。
*/
public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
//略
}
- maxFrameLength
最大幀長度。也就是可以接收的數(shù)據(jù)的最大長度。如果超過,此次數(shù)據(jù)會(huì)被丟棄。 - lengthFieldOffset
長度域偏移。就是說數(shù)據(jù)開始的幾個(gè)字節(jié)可能不是表示數(shù)據(jù)長度,需要后移幾個(gè)字節(jié)才是長度域。 - lengthFieldLength
長度域字節(jié)數(shù)。用幾個(gè)字節(jié)來表示數(shù)據(jù)長度。 - lengthAdjustment
數(shù)據(jù)長度修正。因?yàn)殚L度域指定的長度可以是header+body的整個(gè)長度,也可以只是body的長度。如果表示header+body的整個(gè)長度,那么我們需要修正數(shù)據(jù)長度。 - initialBytesToStrip
跳過的字節(jié)數(shù)。如果你需要接收header+body的所有數(shù)據(jù),此值就是0,如果你只想接收body數(shù)據(jù),那么需要跳過header所占用的字節(jié)數(shù)。 - failFast
如果為true,則在解碼器注意到幀的長度將超過maxFrameLength時(shí)立即拋出TooLongFrameException,而不管是否已讀取整個(gè)幀。
如果為false,則在讀取了超過maxFrameLength的整個(gè)幀之后引發(fā)TooLongFrameException。
下面通過Netty源碼中LengthFieldBasedFrameDecoder的注釋幾個(gè)例子看一下參數(shù)的使用:
5.4.1、2 bytes length field at offset 0, do not strip header
本例中的length字段的值是12 (0x0C),它表示“HELLO, WORLD”的長度。默認(rèn)情況下,解碼器假定長度字段表示長度字段后面的字節(jié)數(shù)。
- lengthFieldOffset = 0: 開始的2個(gè)字節(jié)就是長度域,所以不需要長度域偏移。
- lengthFieldLength = 2: 長度域2個(gè)字節(jié)。
- lengthAdjustment = 0: 數(shù)據(jù)長度修正為0,因?yàn)殚L度域只包含數(shù)據(jù)的長度,所以不需要修正。
- initialBytesToStrip = 0: 發(fā)送和接收的數(shù)據(jù)完全一致,所以不需要跳過任何字節(jié)。
5.4.2、2 bytes length field at offset 0, strip header
因?yàn)槲覀兛梢酝ㄟ^調(diào)用readableBytes()來獲得內(nèi)容的長度,所以可能希望通過指定initialbystrip來刪除長度字段。在本例中,我們指定2(與length字段的長度相同)來去掉前兩個(gè)字節(jié)。
- lengthFieldOffset = 0: 開始的2個(gè)字節(jié)就是長度域,所以不需要長度域偏移。
- lengthFieldLength = 2 :長度域2個(gè)字節(jié)。
- lengthAdjustment = 0: 數(shù)據(jù)長度修正為0,因?yàn)殚L度域只包含數(shù)據(jù)的長度,所以不需要修正。
- initialBytesToStrip = 2 :我們發(fā)現(xiàn)接收的數(shù)據(jù)沒有長度域的數(shù)據(jù),所以要跳過長度域的2個(gè)字節(jié)。
5.4.3、2 bytes length field at offset 0, do not strip header, the length field represents the length of the whole message
在大多數(shù)情況下,length字段僅表示消息體的長度,如前面的示例所示。但是,在一些協(xié)議中,長度字段表示整個(gè)消息的長度,包括消息頭。在這種情況下,我們指定一個(gè)非零長度調(diào)整。因?yàn)檫@個(gè)示例消息中的長度值總是比主體長度大2,所以我們指定-2作為補(bǔ)償?shù)拈L度調(diào)整。
- lengthFieldOffset = 0: 開始的2個(gè)字節(jié)就是長度域,所以不需要長度域偏移。
- lengthFieldLength = 2: 長度域2個(gè)字節(jié)。
- lengthAdjustment = -2 :因?yàn)殚L度域?yàn)榭傞L度,所以我們需要修正數(shù)據(jù)長度,也就是減去2。
- initialBytesToStrip = 0 :發(fā)送和接收的數(shù)據(jù)完全一致,所以不需要跳過任何字節(jié)。
5.4.4、3 bytes length field at the end of 5 bytes header, do not strip header
下面的消息是第一個(gè)示例的簡單變體。一個(gè)額外的頭值被預(yù)先寫入消息中。長度調(diào)整再次為零,因?yàn)樽g碼器在計(jì)算幀長時(shí)總是考慮到預(yù)寫數(shù)據(jù)的長度。
- lengthFieldOffset = 2 :(= the length of Header 1)跳過2字節(jié)之后才是長度域
- lengthFieldLength = 3:長度域3個(gè)字節(jié)。
- lengthAdjustment = 0:數(shù)據(jù)長度修正為0,因?yàn)殚L度域只包含數(shù)據(jù)的長度,所以不需要修正。
- initialBytesToStrip = 0:發(fā)送和接收的數(shù)據(jù)完全一致,所以不需要跳過任何字節(jié)。
5.4.5、3 bytes length field at the beginning of 5 bytes header, do not strip header
這是一個(gè)高級示例,展示了在長度字段和消息正文之間有一個(gè)額外頭的情況。您必須指定一個(gè)正的長度調(diào)整,以便解碼器將額外的標(biāo)頭計(jì)數(shù)到幀長度計(jì)算中。
- lengthFieldOffset = 0:開始的就是長度域,所以不需要長度域偏移。
- lengthFieldLength = 3:長度域3個(gè)字節(jié)。
- lengthAdjustment = 2 :(= the length of Header 1) 長度修正2個(gè)字節(jié),加2
- initialBytesToStrip = 0:發(fā)送和接收的數(shù)據(jù)完全一致,所以不需要跳過任何字節(jié)。
5.4.6、2 bytes length field at offset 1 in the middle of 4 bytes header, strip the first header field and the length field
這是上述所有示例的組合。在長度字段之前有預(yù)寫的header,在長度字段之后有額外的header。預(yù)先設(shè)置的header會(huì)影響lengthFieldOffset,而額外的leader會(huì)影響lengthAdjustment。我們還指定了一個(gè)非零initialBytesToStrip來從幀中去除長度字段和預(yù)定的header。如果不想去掉預(yù)寫的header,可以為initialBytesToSkip指定0。
- lengthFieldOffset = 1 :(= the length of HDR1) ,跳過1個(gè)字節(jié)之后才是長度域
- lengthFieldLength = 2:長度域2個(gè)字節(jié)
- lengthAdjustment = 1: (= the length of HDR2)
- initialBytesToStrip = 3 :(= the length of HDR1 + LEN)
5.4.7、2 bytes length field at offset 1 in the middle of 4 bytes header, strip the first header field and the length field, the length field represents the length of the whole message
讓我們對前面的示例進(jìn)行另一個(gè)修改。與前一個(gè)示例的惟一區(qū)別是,length字段表示整個(gè)消息的長度,而不是消息正文的長度,就像第三個(gè)示例一樣。我們必須把HDR1的長度和長度計(jì)算進(jìn)長度調(diào)整里。請注意,我們不需要考慮HDR2的長度,因?yàn)閘ength字段已經(jīng)包含了整個(gè)頭的長度。
- lengthFieldOffset = 1:長度域偏移1個(gè)字節(jié),之后才是長度域。
- lengthFieldLength = 2:長度域2個(gè)字節(jié)。
- lengthAdjustment = -3: (= the length of HDR1 + LEN, negative)數(shù)據(jù)長度修正-3個(gè)字節(jié)。
- initialBytesToStrip = 3:因?yàn)榻邮艿臄?shù)據(jù)比發(fā)送的數(shù)據(jù)少3個(gè)字節(jié),所以跳過3個(gè)字節(jié)。