在網(wǎng)絡(luò)傳輸中只將數(shù)據(jù)看作是原始的字節(jié)序列。然則,我們的應(yīng)用程序需要把這些字節(jié)序列組成有意義的信息。將應(yīng)用程序的數(shù)據(jù)轉(zhuǎn)換為網(wǎng)絡(luò)格式梆砸,以及將網(wǎng)絡(luò)格式轉(zhuǎn)換為應(yīng)用程序的數(shù)據(jù)的組件分別叫作編碼器和解碼器,同時(shí)具有這兩種功能的單一組件叫作編解碼器园欣。
1帖世、粘包 & 拆包
基于前面的分析我們知道 dubbo 的遠(yuǎn)程調(diào)用是基于 Netty 這個(gè) Nio 框架進(jìn)行基于 TCP/IP 的 Socket 通信。
TCP 是一個(gè)“流”協(xié)議沸枯,所謂流就是沒(méi)有界限的一串?dāng)?shù)據(jù)日矫。可以想像一個(gè)河里的流水是連成一片的绑榴,其間沒(méi)有分界線哪轿。TCP 底層并不了解上層業(yè)務(wù)數(shù)據(jù)的具體含義,它會(huì)根據(jù) TCP 緩沖區(qū)的實(shí)際情況進(jìn)行包的劃分翔怎,所以在業(yè)務(wù)上認(rèn)為窃诉,一個(gè)完整的包可能會(huì)被 TCP 拆分成多個(gè)包進(jìn)行發(fā)送。也有可能把多個(gè)小的包封裝成一個(gè)大的數(shù)據(jù)包發(fā)送赤套,這就是所謂的 TCP 粘包和拆包問(wèn)題飘痛。
1.1 TCP 粘包 & 拆包問(wèn)題說(shuō)明
下面就通過(guò)以下的圖來(lái)說(shuō)明 TCP 粘包與拆包問(wèn)題:
假設(shè)客戶端分別發(fā)送了兩個(gè)數(shù)據(jù)包 D1 和 D2 給服務(wù)端,由于服務(wù)端一次讀取到的字節(jié)數(shù)據(jù)是不確定的容握,所以可能存在以下 4 種情況宣脉。
- 服務(wù)端兩次讀取到了兩個(gè)獨(dú)立的數(shù)據(jù)包,分別是 D1 和 D2剔氏,沒(méi)有粘包和拆包
- 服務(wù)端一次接收到了兩個(gè)數(shù)據(jù)包塑猖, D1 和 D2 粘合在一起,被稱為 TCP 粘包
- 服務(wù)端分兩次讀取到了兩個(gè)數(shù)據(jù)包谈跛,第一次讀取到了完整的 D1 包和 D2 包的部分內(nèi)部羊苟,第二次讀取到了 D2 包的剩余內(nèi)部,這被稱為 TCP 拆包
- 服務(wù)端兩次讀取到了兩個(gè)數(shù)據(jù)包币旧,第一次讀取到了 D1 包的部分內(nèi)部 D1_1践险,第二次讀取到了 D1 包的剩余內(nèi)部 D1_2 和 D2 包的整包猿妈。
如果此時(shí)服務(wù)端 TCP 接收滑窗非常小吹菱,而數(shù)據(jù)包 D1 和 D2 比較大 巍虫,很有可能會(huì)發(fā)生第五種可能,即服務(wù)端分多次才能將 D1 和 D2 包接收完全鳍刷,期間發(fā)生多次拆包占遥。
1.2 解決粘包 & 拆包
由于底層的 TCP 無(wú)法理解上層的業(yè)務(wù)數(shù)據(jù),所以在底層是無(wú)法保證數(shù)據(jù)包不被拆分和重組的输瓜。這個(gè)問(wèn)題只能通過(guò)上層的應(yīng)用協(xié)議棧設(shè)計(jì)來(lái)解決瓦胎,主流的解決方案如下:
- 消息定長(zhǎng),例如每個(gè)報(bào)文的大小為固定長(zhǎng)度 200 字節(jié)尤揣,如果不夠搔啊,空位被空格。
- 在包尾增加回車換行符進(jìn)行分割北戏,例如 TFP 協(xié)議
- 將消息分為消息頭和消息體负芋,消息頭中包含表示消息總長(zhǎng)度(或者消息體長(zhǎng)度)的字段。
netty 對(duì)于前 2 種都有自己的實(shí)現(xiàn)嗜愈,而 dubbo 采用的是第 3 種來(lái)解決粘包與拆包問(wèn)題的旧蛾。
2、dubbo 自定義協(xié)議
Netty 對(duì)于開(kāi)發(fā)者而言蠕嫁,其實(shí)就是操作 ChannelHandler 這個(gè)組件锨天。之前我們分析了 dubbo 網(wǎng)絡(luò)請(qǐng)求的發(fā)送與接收是實(shí)現(xiàn)了 ChannelHandler 的 NettyServerHandler。針對(duì)于編解碼同樣也是實(shí)現(xiàn) ChannelHandler 來(lái)進(jìn)行的剃毒。
NettyServer#doOpen
protected void doOpen() throws Throwable {
bootstrap = new ServerBootstrap();
bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("NettyServerBoss", true));
workerGroup = new NioEventLoopGroup(getUrl().getPositiveParameter(Constants.IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS),
new DefaultThreadFactory("NettyServerWorker", true));
final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);
channels = nettyServerHandler.getChannels();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
.childOption(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
ch.pipeline()//.addLast("logging",new LoggingHandler(LogLevel.INFO))//for debug
.addLast("decoder", adapter.getDecoder())
.addLast("encoder", adapter.getEncoder())
.addLast("handler", nettyServerHandler);
}
});
// bind
ChannelFuture channelFuture = bootstrap.bind(getBindAddress());
channelFuture.syncUninterruptibly();
channel = channelFuture.channel();
}
在進(jìn)行服務(wù)暴露的時(shí)候病袄,在初始化的時(shí)候通過(guò) ChannelPipeline 添加編碼、解碼與處理請(qǐng)求響應(yīng)的具體 ChannelHandler 實(shí)現(xiàn)類迟赃。dubbo 編解碼的具體都是通過(guò) NettyCodecAdapter 來(lái)處理的陪拘。
下面我們來(lái)看一下 dubbo 的協(xié)議頭約定:
dubbo 使用長(zhǎng)度為 16 的 byte 數(shù)組作為協(xié)議頭。1 個(gè) byte 對(duì)應(yīng) 8 位纤壁。所以 dubbo 的協(xié)議頭有 128 位 (也就是上圖的從 0 到 127)左刽。我們來(lái)看一下這 128 位協(xié)議頭分別代表什么意思。
- 0 ~ 7 : dubbo 魔數(shù)(
(short) 0xdabb
) 高位酌媒,也就是 (short) 0xda欠痴。 - 8 ~ 15: dubbo 魔數(shù)(
(short) 0xdabb
) 低位,也就是 (short) 0xbb秒咨。 - 16 ~ 20:序列化 id(Serialization id)喇辽,也就是 dubbo 支持的序列化中的
contentTypeId
,比如 Hessian2Serialization#ID 為 2 - 21 :是否事件(event )
- 22 : 是否 Two way 模式(Two way)雨席。默認(rèn)是 Two-way 模式菩咨,
<dubbo:method>
標(biāo)簽的 return 屬性配置為false,則是oneway模式 - 23 :標(biāo)記是請(qǐng)求對(duì)象還是響應(yīng)對(duì)象(Req/res)
- 24 ~ 31:response 的結(jié)果響應(yīng)碼 ,例如 OK=20
- 32 ~ 95:id(long)抽米,異步變同步的全局唯一ID特占,用來(lái)做consumer和provider的來(lái)回通信標(biāo)記。
- 96 ~ 127: data length云茸,請(qǐng)求或響應(yīng)數(shù)據(jù)體的數(shù)據(jù)長(zhǎng)度也就是消息頭+請(qǐng)求數(shù)據(jù)的長(zhǎng)度是目。用于處理 dubbo 通信的粘包與拆包問(wèn)題。
我們就根據(jù)源碼來(lái)分析一下 dubbo 是如何進(jìn)行編解碼的标捺。
3懊纳、協(xié)議源碼分析
dubbo 的編解碼可以分為以下 4 個(gè)部分來(lái)分析:
- consumer 請(qǐng)求編碼
- consumer響應(yīng)結(jié)果解碼
- provider 請(qǐng)求解碼
- provider 響應(yīng)結(jié)果編碼
在 dubbo 進(jìn)行服務(wù)暴露的時(shí)候是通過(guò) NettyCodecAdapter 來(lái)獲取到需要添加的編碼器與解碼器。在 NettyCodecAdapter 里面定義內(nèi)部類 InternalEncoder (繼承 netty 中的 MessageToByteEncoder)實(shí)現(xiàn) dubbo 的自定義編碼器亡容,定義內(nèi)部類 ByteToMessageDecoder (繼承 netty 中的 ByteToMessageDecoder) 實(shí)現(xiàn) dubbo 自定義解碼器嗤疯。不管是自定義的編碼器還是解碼器最終都會(huì)調(diào)用到 dubbo 的 SPI 接口 Codec2 默認(rèn)使用 DubboCodec。下面就具體的分析一下 dubbo 這 4 個(gè)編解碼過(guò)程闺兢。
3.1 consumer 請(qǐng)求編碼
consumer 在請(qǐng)求 provider 的時(shí)候需要把 Request 對(duì)象轉(zhuǎn)化成 byte 數(shù)組身弊,所以它是一個(gè)需要編碼的過(guò)程。
3.2 consumer響應(yīng)結(jié)果解碼
consumer 在接收 provider 響應(yīng)的時(shí)候需要把 byte 數(shù)組轉(zhuǎn)化成 Response 對(duì)象列敲,所以它是一個(gè)需要解碼的過(guò)程阱佛。
3.3 provider 請(qǐng)求解碼
provider 在接收 consumer 請(qǐng)求的時(shí)候需要把 byte 數(shù)組轉(zhuǎn)化成 Request 對(duì)象,所以它是一個(gè)需要解碼的過(guò)程戴而。
3.4 響應(yīng)結(jié)果編碼
provider 在處理完成 consumer 請(qǐng)求需要響應(yīng)結(jié)果的時(shí)候需要把 Response 對(duì)象轉(zhuǎn)化成 byte 數(shù)組凑术,所以它是一個(gè)需要編碼的過(guò)程。