在網絡傳輸中只將數據看作是原始的字節序列。然則,我們的應用程序需要把這些字節序列組成有意義的信息。將應用程序的數據轉換爲網絡格式,以及將網絡格式轉換爲應用程序的數據的組件分別叫作編碼器和解碼器,同時具有這兩種功能的單一組件叫作編解碼器。
1、粘包 & 拆包
基於前面的分析我們知道 dubbo 的遠程調用是基於 Netty 這個 Nio 框架進行基於 TCP/IP 的 Socket 通信。
TCP 是一個“流”協議,所謂流就是沒有界限的一串數據。可以想像一個河裏的流水是連成一片的,其間沒有分界線。TCP 底層並不瞭解上層業務數據的具體含義,它會根據 TCP 緩衝區的實際情況進行包的劃分,所以在業務上認爲,一個完整的包可能會被 TCP 拆分成多個包進行發送。也有可能把多個小的包封裝成一個大的數據包發送,這就是所謂的 TCP 粘包和拆包問題。
1.1 TCP 粘包 & 拆包問題說明
下面就通過以下的圖來說明 TCP 粘包與拆包問題:
假設客戶端分別發送了兩個數據包 D1 和 D2 給服務端,由於服務端一次讀取到的字節數據是不確定的,所以可能存在以下 4 種情況。
- 服務端兩次讀取到了兩個獨立的數據包,分別是 D1 和 D2,沒有粘包和拆包
- 服務端一次接收到了兩個數據包, D1 和 D2 粘合在一起,被稱爲 TCP 粘包
- 服務端分兩次讀取到了兩個數據包,第一次讀取到了完整的 D1 包和 D2 包的部分內部,第二次讀取到了 D2 包的剩餘內部,這被稱爲 TCP 拆包
- 服務端兩次讀取到了兩個數據包,第一次讀取到了 D1 包的部分內部 D1_1,第二次讀取到了 D1 包的剩餘內部 D1_2 和 D2 包的整包。
如果此時服務端 TCP 接收滑窗非常小,而數據包 D1 和 D2 比較大 ,很有可能會發生第五種可能,即服務端分多次才能將 D1 和 D2 包接收完全,期間發生多次拆包。
1.2 解決粘包 & 拆包
由於底層的 TCP 無法理解上層的業務數據,所以在底層是無法保證數據包不被拆分和重組的。這個問題只能通過上層的應用協議棧設計來解決,主流的解決方案如下:
- 消息定長,例如每個報文的大小爲固定長度 200 字節,如果不夠,空位被空格。
- 在包尾增加回車換行符進行分割,例如 TFP 協議
- 將消息分爲消息頭和消息體,消息頭中包含表示消息總長度(或者消息體長度)的字段。
netty 對於前 2 種都有自己的實現,而 dubbo 採用的是第 3 種來解決粘包與拆包問題的。
2、dubbo 自定義協議
Netty 對於開發者而言,其實就是操作 ChannelHandler 這個組件。之前我們分析了 dubbo 網絡請求的發送與接收是實現了 ChannelHandler 的 NettyServerHandler。針對於編解碼同樣也是實現 ChannelHandler 來進行的。
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();
}
在進行服務暴露的時候,在初始化的時候通過 ChannelPipeline 添加編碼、解碼與處理請求響應的具體 ChannelHandler 實現類。dubbo 編解碼的具體都是通過 NettyCodecAdapter 來處理的。
下面我們來看一下 dubbo 的協議頭約定:
dubbo 使用長度爲 16 的 byte 數組作爲協議頭。1 個 byte 對應 8 位。所以 dubbo 的協議頭有 128 位 (也就是上圖的從 0 到 127)。我們來看一下這 128 位協議頭分別代表什麼意思。
- 0 ~ 7 : dubbo 魔數(
(short) 0xdabb
) 高位,也就是 (short) 0xda。 - 8 ~ 15: dubbo 魔數(
(short) 0xdabb
) 低位,也就是 (short) 0xbb。 - 16 ~ 20:序列化 id(Serialization id),也就是 dubbo 支持的序列化中的
contentTypeId
,比如 Hessian2Serialization#ID 爲 2 - 21 :是否事件(event )
- 22 : 是否 Two way 模式(Two way)。默認是 Two-way 模式,
<dubbo:method>
標籤的 return 屬性配置爲false,則是oneway模式 - 23 :標記是請求對象還是響應對象(Req/res)
- 24 ~ 31:response 的結果響應碼 ,例如 OK=20
- 32 ~ 95:id(long),異步變同步的全局唯一ID,用來做consumer和provider的來回通信標記。
- 96 ~ 127: data length,請求或響應數據體的數據長度也就是消息頭+請求數據的長度。用於處理 dubbo 通信的粘包與拆包問題。
我們就根據源碼來分析一下 dubbo 是如何進行編解碼的。
3、協議源碼分析
dubbo 的編解碼可以分爲以下 4 個部分來分析:
- consumer 請求編碼
- consumer響應結果解碼
- provider 請求解碼
- provider 響應結果編碼
在 dubbo 進行服務暴露的時候是通過 NettyCodecAdapter 來獲取到需要添加的編碼器與解碼器。在 NettyCodecAdapter 裏面定義內部類 InternalEncoder (繼承 netty 中的 MessageToByteEncoder)實現 dubbo 的自定義編碼器,定義內部類 ByteToMessageDecoder (繼承 netty 中的 ByteToMessageDecoder) 實現 dubbo 自定義解碼器。不管是自定義的編碼器還是解碼器最終都會調用到 dubbo 的 SPI 接口 Codec2 默認使用 DubboCodec。下面就具體的分析一下 dubbo 這 4 個編解碼過程。
3.1 consumer 請求編碼
consumer 在請求 provider 的時候需要把 Request 對象轉化成 byte 數組,所以它是一個需要編碼的過程。
3.2 consumer響應結果解碼
consumer 在接收 provider 響應的時候需要把 byte 數組轉化成 Response 對象,所以它是一個需要解碼的過程。
3.3 provider 請求解碼
provider 在接收 consumer 請求的時候需要把 byte 數組轉化成 Request 對象,所以它是一個需要解碼的過程。
3.4 響應結果編碼
provider 在處理完成 consumer 請求需要響應結果的時候需要把 Response 對象轉化成 byte 數組,所以它是一個需要編碼的過程。