Netty - 粘包和半包(下)
接上篇《TCP 粘包和半包 介紹及解決(上)》
上一篇介紹了粘包和半包及其通用的解決方案,今天重點來看一下 Netty 是如何實現封裝成幀(Framing)方案的。
解碼核心流程
之前介紹過三種解碼器FixedLengthFrameDecoder、DelimiterBasedFrameDecoder、LengthFieldBasedFrameDecoder,它們都繼承自ByteToMessageDecoder,而ByteToMessageDecoder繼承自ChannelInboundHandlerAdapter,其核心方法為channelRead。因此,我們來看看ByteToMessageDecoder的channelRead方法:
- @Override
- public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
- if (msg instanceof ByteBuf) {
- CodecOutputList out = CodecOutputList.newInstance();
- try {
- // 將傳入的消息轉化為data
- ByteBuf data = (ByteBuf) msg;
- // 最終實現的目標是將數據全部放進cumulation中
- first = cumulation == null;
- // 第一筆數據直接放入
- if (first) {
- cumulation = data;
- } else {
- // 不是第一筆數據就進行追加
- cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
- }
- // 解碼
- callDecode(ctx, cumulation, out);
- }
- // 以下代碼省略,因為不屬于解碼過程
- }
再來看看callDecode方法:
- protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
- try {
- while (in.isReadable()) {
- int outoutSize = out.size();
- if (outSize > 0) {
- // 以下代碼省略,因為初始狀態時,outSize 只可能是0,不可能進入這里
- }
- int oldInputLength = in.readableBytes();
- // 在進行 decode 時,不執行handler的remove操作。
- // 只有當 decode 執行完之后,開始清理數據。
- decodeRemovalReentryProtection(ctx, in, out);
- // 省略以下代碼,因為后面的內容也不是解碼的過程
再來看看decodeRemovalReentryProtection方法:
- final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
- throws Exception {
- // 設置當前狀態為正在解碼
- decodeState = STATE_CALLING_CHILD_DECODE;
- try {
- // 解碼
- decode(ctx, in, out);
- } finally {
- // 執行hander的remove操作
- boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING;
- decodeState = STATE_INIT;
- if (removePending) {
- handlerRemoved(ctx);
- }
- }
- }
- // 子類都重寫了該方法,每種實現都會有自己特殊的解碼方式
- protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
從上面的過程可以總結出,在解碼之前,需要先將數據寫入cumulation,當解碼結束后,需要通過 handler 進行移除。
具體解碼過程
剛剛說到decode方法在子類中都有實現,那針對我們說的三種解碼方式,一一看其實現。
1. FixedLengthFrameDecoder
其源碼為:
- @Override
- protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
- Object decodedecoded = decode(ctx, in);
- if (decoded != null) {
- out.add(decoded);
- }
- }
- protected Object decode(
- @SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception {
- // 收集到的數據是否小于固定長度,小于就代表無法解析
- if (in.readableBytes() < frameLength) {
- return null;
- } else {
- return in.readRetainedSlice(frameLength);
- }
- }
就和這個類的名字一樣簡單,就是固定長度進行解碼,因此,在設置該解碼器的時候,需要在構造方式里傳入frameLength。
2. DelimiterBasedFrameDecoder
其源碼為:
- @Override
- protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
- Object decodedecoded = decode(ctx, in);
- if (decoded != null) {
- out.add(decoded);
- }
- }
- protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
- // 當前的分割符是否是換行分割符(\n或者\r\n)
- if (lineBasedDecoder != null) {
- return lineBasedDecoder.decode(ctx, buffer);
- }
- // Try all delimiters and choose the delimiter which yields the shortest frame.
- int minFrameLength = Integer.MAX_VALUE;
- ByteBuf minDelim = null;
- // 其他分割符進行一次切分
- for (ByteBuf delim: delimiters) {
- int frameLength = indexOf(buffer, delim);
- if (frameLength >= 0 && frameLength < minFrameLength) {
- minFrameLength = frameLength;
- minDelim = delim;
- }
- }
- // 以下代碼省略
根據它的名字可以知道,分隔符才是它的核心。它將分割符分成兩類,只有換行分割符(n或者rn)和其他。因此,需要注意的是,你可以定義多種分割符,它都是支持的。
3. LengthFieldBasedFrameDecoder
該類比較復雜,如果直接看方法容易把自己看混亂,因此我準備結合類上的解釋,先看看其私有變量。
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
Let's give another twist to the previous example. The only difference from the previous example is that the length field represents the length of the whole message instead of the message body, just like the third example. We have to count the length of HDR1 and Length into lengthAdjustment. Please note that we don't need to take the length of HDR2 into account because the length field already includes the whole header length. |
- * BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes)
- * +------+--------+------+----------------+ +------+----------------+
- * | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
- * | 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
- * +------+--------+------+----------------+ +------+----------------+
- lengthFieldOffset :該字段代表 Length 字段是從第幾個字節開始的。上面的例子里,Length 字段是從第1個字節開始(HDR1 是第0個字節),因此該值即為0。
- lengthFieldLength:該字段代表 Length 字段所占用的字節數。上面的例子里,Length 字段占用2個字節,因此該值為2。
- lengthAdjustment:該字段代表 Length 字段結束位置到真正的內容開始位置的距離。上面例子里,因為 Length 字段的含義是整個消息(包括 HDR1、Length、HDR2、Actual Content,一般 Length 指的只是 Actual Content),所以 Length 末尾到真正的內容開始位置(HDR1的開始處),相當于減少3個字節,所以是-3。
- initialBytesToStrip: 展示時需要從 Length 字段末尾開始跳過幾個字節。上面例子里,因為真正的內容是從 HDR1 開始的,最終展示的內容是從 HDR2 開始的,所以中間差了3個字節,所以該值是3。
該類的解碼方法比較復雜,有興趣的同學可以試著自己分析一下。
總結
這一篇主要是結合 Netty 里的源代碼講解了 Netty 中封裝成幀(Framing)的三種方式,相信你一定有了不一樣的理解。