Netty 原始碼中對 Redis 協議的實現

haifeiWu發表於2018-08-09

原文地址: haifeiWu的部落格
部落格地址:www.hchstudio.cn
歡迎轉載,轉載請註明作者及出處,謝謝!

近期一直在做網路協議相關的工作,所以部落格也就與之相關的比較多,今天樓主結合 Redis的協議 RESP 看看在 Netty 原始碼中是如何實現的。

RESP 協議

RESP 是 Redis 序列化協議的簡寫。它是一種直觀的文字協議,優勢在於實現非常簡單,解析效能極好。

Redis 協議將傳輸的結構資料分為 5 種最小單元型別,單元結束時統一加上回車換行符號\r\n,來表示該單元的結束。

  1. 單行字串 以 + 符號開頭。
  2. 多行字串 以 $ 符號開頭,後跟字串長度。
  3. 整數值 以 : 符號開頭,後跟整數的字串形式。
  4. 錯誤訊息 以 - 符號開頭。
  5. 陣列 以 * 號開頭,後跟陣列的長度。

關於 RESP 協議的具體介紹感興趣的小夥伴請移步樓主的另一篇文章Redis協議規範(譯文)

Netty 中 RESP 協議的定義

如下面程式碼中所表示的,Netty中使用對應符號的ASCII碼來表示,感興趣的小夥伴可以查一下ASCII碼錶來驗證一下。


public enum RedisMessageType {
    // 以 + 開頭的單行字串
    SIMPLE_STRING((byte)43, true),
    // 以 - 開頭的錯誤資訊
    ERROR((byte)45, true),
    // 以 : 開頭的整型資料
    INTEGER((byte)58, true),
    // 以 $ 開頭的多行字串
    BULK_STRING((byte)36, false),
    // 以 * 開頭的陣列
    ARRAY_HEADER((byte)42, false),
    ARRAY((byte)42, false);

    private final byte value;
    private final boolean inline;

    private RedisMessageType(byte value, boolean inline) {
        this.value = value;
        this.inline = inline;
    }

    public byte value() {
        return this.value;
    }

    public boolean isInline() {
        return this.inline;
    }

    public static RedisMessageType valueOf(byte value) {
        switch(value) {
        case 36:
            return BULK_STRING;
        case 42:
            return ARRAY_HEADER;
        case 43:
            return SIMPLE_STRING;
        case 45:
            return ERROR;
        case 58:
            return INTEGER;
        default:
            throw new RedisCodecException("Unknown RedisMessageType: " + value);
        }
    }
}

複製程式碼

Netty 中 RESP 解碼器實現

解碼器,顧名思義,就是將伺服器返回的資料根據協議反序列化成易於閱讀的資訊。RedisDecoder 就是根據 RESP 將服務端返回的資訊反序列化出來。下面是指令的編碼格式

SET key value => *3\r\n$5\r\nSET\r\n$1\r\nkey\r\n$1\r\nvalue\r\n
複製程式碼

指令是一個字串陣列,編碼一個字串陣列,首先需要編碼陣列長度*3\r\n。然後依次編碼各個字串引數。編碼字串首先需要編碼字串的長度$5\r\n。然後再編碼字串的內容SET\r\n。Redis 訊息以\r\n作為分隔符,這樣設計其實挺浪費網路傳輸流量的,訊息內容裡面到處都是\r\n符號。但是這樣的訊息可讀性會比較好,便於除錯。RESP 協議是犧牲效能換取可讀,易於實現的一個經典例子。

指令解碼器的實現,Socket讀取網路位元組流的時候存在拆包問題。所謂拆包問題是指一次Read呼叫從Socket讀到的位元組陣列可能只是一個完整訊息的一部分。而另外一部分則需要發起另外一次Read呼叫才可能讀到,甚至要發起多個Read呼叫才可以讀到完整的一條訊息。對於拆包問題感興趣的小夥伴可以檢視樓主的另一篇文章TCP 粘包問題淺析及其解決方案

如果我們拿部分訊息去反序列化成輸入訊息物件肯定是要失敗的,或者說生成的訊息物件是不完整填充的。這個時候我們需要等待下一次Read呼叫,然後將這兩次Read呼叫的位元組陣列拼起來,嘗試再一次反序列化。

問題來了,如果一個輸入訊息物件很大,就可能需要多個Read呼叫和多次反序列化操作才能完整的解包出一個輸入物件。那這個反序列化的過程就會重複了多次。

針對這個問題,Netty 中很巧妙的解決了這個問題,如下所示,Netty 中通過 state 屬性來儲存當前序列化的狀態,然後下次反序列化的時候就可以從上次記錄的 state 直接繼續反序列化。這樣就避免了重複的問題。

    // 保持當前序列化狀態的欄位
    private RedisDecoder.State state;

    public RedisDecoder() {
        this(65536, FixedRedisMessagePool.INSTANCE);
    }

    public RedisDecoder(int maxInlineMessageLength, RedisMessagePool messagePool) {
        this.toPositiveLongProcessor = new RedisDecoder.ToPositiveLongProcessor();
        // 預設初始化狀態為,反序列化指令型別
        this.state = RedisDecoder.State.DECODE_TYPE;
        if (maxInlineMessageLength > 0 && maxInlineMessageLength <= 536870912) {
            this.maxInlineMessageLength = maxInlineMessageLength;
            this.messagePool = messagePool;
        } else {
            throw new RedisCodecException("maxInlineMessageLength: " + maxInlineMessageLength + " (expected: <= " + 536870912 + ")");
        }
    }

    // 解碼器的主要業務邏輯
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        try {
            // 迴圈讀取資訊,將資訊完成的序列化
            while(true) {
                switch(this.state) {
                case DECODE_TYPE:
                    if (this.decodeType(in)) {
                        break;
                    }

                    return;
                case DECODE_INLINE:
                    if (this.decodeInline(in, out)) {
                        break;
                    }

                    return;
                case DECODE_LENGTH:
                    if (this.decodeLength(in, out)) {
                        break;
                    }

                    return;
                case DECODE_BULK_STRING_EOL:
                    if (this.decodeBulkStringEndOfLine(in, out)) {
                        break;
                    }

                    return;
                case DECODE_BULK_STRING_CONTENT:
                    if (this.decodeBulkStringContent(in, out)) {
                        break;
                    }

                    return;
                default:
                    throw new RedisCodecException("Unknown state: " + this.state);
                }
            }
        } catch (RedisCodecException var5) {
            this.resetDecoder();
            throw var5;
        } catch (Exception var6) {
            this.resetDecoder();
            throw new RedisCodecException(var6);
        }
    }

複製程式碼

下面程式碼中,是針對每種資料型別進行反序列化的具體業務邏輯。有小夥伴可能會想,沒有看到解碼胡陣列型別的邏輯呢?實際上在 RESP 協議中陣列就是其他型別的組合,所以完全可以迴圈讀取,按照單個元素解碼。

// 解碼訊息型別
 private boolean decodeType(ByteBuf in) throws Exception {
        if (!in.isReadable()) {
            return false;
        } else {
            this.type = RedisMessageType.valueOf(in.readByte());
            this.state = this.type.isInline() ? RedisDecoder.State.DECODE_INLINE : RedisDecoder.State.DECODE_LENGTH;
            return true;
        }
    }

    // 解碼單行字串,錯誤資訊,或者整型資料型別
    private boolean decodeInline(ByteBuf in, List<Object> out) throws Exception {
        ByteBuf lineBytes = readLine(in);
        if (lineBytes == null) {
            if (in.readableBytes() > this.maxInlineMessageLength) {
                throw new RedisCodecException("length: " + in.readableBytes() + " (expected: <= " + this.maxInlineMessageLength + ")");
            } else {
                return false;
            }
        } else {
            out.add(this.newInlineRedisMessage(this.type, lineBytes));
            this.resetDecoder();
            return true;
        }
    }

    // 解碼訊息長度
    private boolean decodeLength(ByteBuf in, List<Object> out) throws Exception {
        ByteBuf lineByteBuf = readLine(in);
        if (lineByteBuf == null) {
            return false;
        } else {
            long length = this.parseRedisNumber(lineByteBuf);
            if (length < -1L) {
                throw new RedisCodecException("length: " + length + " (expected: >= " + -1 + ")");
            } else {
                switch(this.type) {
                case ARRAY_HEADER:
                    out.add(new ArrayHeaderRedisMessage(length));
                    this.resetDecoder();
                    return true;
                case BULK_STRING:
                    if (length > 536870912L) {
                        throw new RedisCodecException("length: " + length + " (expected: <= " + 536870912 + ")");
                    }

                    this.remainingBulkLength = (int)length;
                    return this.decodeBulkString(in, out);
                default:
                    throw new RedisCodecException("bad type: " + this.type);
                }
            }
        }
    }

    // 解碼多行字串
    private boolean decodeBulkString(ByteBuf in, List<Object> out) throws Exception {
        switch(this.remainingBulkLength) {
        case -1:
            out.add(FullBulkStringRedisMessage.NULL_INSTANCE);
            this.resetDecoder();
            return true;
        case 0:
            this.state = RedisDecoder.State.DECODE_BULK_STRING_EOL;
            return this.decodeBulkStringEndOfLine(in, out);
        default:
            out.add(new BulkStringHeaderRedisMessage(this.remainingBulkLength));
            this.state = RedisDecoder.State.DECODE_BULK_STRING_CONTENT;
            return this.decodeBulkStringContent(in, out);
        }
    }
複製程式碼

Netty 中 RESP 編碼器實現

編碼器,顧名思義,就是將物件根據 RESP 協議序列化成位元組流傳送到服務端。編碼器的實現非常簡單,不用考慮拆包等問題,就是分配一個ByteBuf,然後將將訊息輸出物件序列化的位元組陣列塞到ByteBuf中輸出就可以了。

下面程式碼中就是 encode 方法直接呼叫 writeRedisMessage 方法,根據訊息型別進行寫buffer操作。

    @Override
    protected void encode(ChannelHandlerContext ctx, RedisMessage msg, List<Object> out) throws Exception {
        try {
            writeRedisMessage(ctx.alloc(), msg, out);
        } catch (CodecException e) {
            throw e;
        } catch (Exception e) {
            throw new CodecException(e);
        }
    }

    private void writeRedisMessage(ByteBufAllocator allocator, RedisMessage msg, List<Object> out) {
        // 判斷訊息型別,然後呼叫寫相應訊息的方法。
        if (msg instanceof InlineCommandRedisMessage) {
            writeInlineCommandMessage(allocator, (InlineCommandRedisMessage) msg, out);
        } else if (msg instanceof SimpleStringRedisMessage) {
            writeSimpleStringMessage(allocator, (SimpleStringRedisMessage) msg, out);
        } else if (msg instanceof ErrorRedisMessage) {
            writeErrorMessage(allocator, (ErrorRedisMessage) msg, out);
        } else if (msg instanceof IntegerRedisMessage) {
            writeIntegerMessage(allocator, (IntegerRedisMessage) msg, out);
        } else if (msg instanceof FullBulkStringRedisMessage) {
            writeFullBulkStringMessage(allocator, (FullBulkStringRedisMessage) msg, out);
        } else if (msg instanceof BulkStringRedisContent) {
            writeBulkStringContent(allocator, (BulkStringRedisContent) msg, out);
        } else if (msg instanceof BulkStringHeaderRedisMessage) {
            writeBulkStringHeader(allocator, (BulkStringHeaderRedisMessage) msg, out);
        } else if (msg instanceof ArrayHeaderRedisMessage) {
            writeArrayHeader(allocator, (ArrayHeaderRedisMessage) msg, out);
        } else if (msg instanceof ArrayRedisMessage) {
            writeArrayMessage(allocator, (ArrayRedisMessage) msg, out);
        } else {
            throw new CodecException("unknown message type: " + msg);
        }
    }

複製程式碼

下面程式碼主要是實現對應訊息按照 RESP 協議 進行序列化操作,具體就是上面樓主說的,分配一個ByteBuf,然後將將訊息輸出物件序列化的位元組陣列塞到ByteBuf中輸出即可。

private static void writeInlineCommandMessage(ByteBufAllocator allocator, InlineCommandRedisMessage msg,
                                                 List<Object> out) {
        writeString(allocator, RedisMessageType.INLINE_COMMAND, msg.content(), out);
    }

    private static void writeSimpleStringMessage(ByteBufAllocator allocator, SimpleStringRedisMessage msg,
                                                 List<Object> out) {
        writeString(allocator, RedisMessageType.SIMPLE_STRING, msg.content(), out);
    }

    private static void writeErrorMessage(ByteBufAllocator allocator, ErrorRedisMessage msg, List<Object> out) {
        writeString(allocator, RedisMessageType.ERROR, msg.content(), out);
    }

    private static void writeString(ByteBufAllocator allocator, RedisMessageType type, String content,
                                    List<Object> out) {
        ByteBuf buf = allocator.ioBuffer(type.length() + ByteBufUtil.utf8MaxBytes(content) +
                                         RedisConstants.EOL_LENGTH);
        type.writeTo(buf);
        ByteBufUtil.writeUtf8(buf, content);
        buf.writeShort(RedisConstants.EOL_SHORT);
        out.add(buf);
    }

    private void writeIntegerMessage(ByteBufAllocator allocator, IntegerRedisMessage msg, List<Object> out) {
        ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.LONG_MAX_LENGTH +
                                         RedisConstants.EOL_LENGTH);
        RedisMessageType.INTEGER.writeTo(buf);
        buf.writeBytes(numberToBytes(msg.value()));
        buf.writeShort(RedisConstants.EOL_SHORT);
        out.add(buf);
    }

    private void writeBulkStringHeader(ByteBufAllocator allocator, BulkStringHeaderRedisMessage msg, List<Object> out) {
        final ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH +
                                        (msg.isNull() ? RedisConstants.NULL_LENGTH :
                                                        RedisConstants.LONG_MAX_LENGTH + RedisConstants.EOL_LENGTH));
        RedisMessageType.BULK_STRING.writeTo(buf);
        if (msg.isNull()) {
            buf.writeShort(RedisConstants.NULL_SHORT);
        } else {
            buf.writeBytes(numberToBytes(msg.bulkStringLength()));
            buf.writeShort(RedisConstants.EOL_SHORT);
        }
        out.add(buf);
    }

    private static void writeBulkStringContent(ByteBufAllocator allocator, BulkStringRedisContent msg,
                                               List<Object> out) {
        out.add(msg.content().retain());
        if (msg instanceof LastBulkStringRedisContent) {
            out.add(allocator.ioBuffer(RedisConstants.EOL_LENGTH).writeShort(RedisConstants.EOL_SHORT));
        }
    }

    private void writeFullBulkStringMessage(ByteBufAllocator allocator, FullBulkStringRedisMessage msg,
                                            List<Object> out) {
        if (msg.isNull()) {
            ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.NULL_LENGTH +
                                             RedisConstants.EOL_LENGTH);
            RedisMessageType.BULK_STRING.writeTo(buf);
            buf.writeShort(RedisConstants.NULL_SHORT);
            buf.writeShort(RedisConstants.EOL_SHORT);
            out.add(buf);
        } else {
            ByteBuf headerBuf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.LONG_MAX_LENGTH +
                                                   RedisConstants.EOL_LENGTH);
            RedisMessageType.BULK_STRING.writeTo(headerBuf);
            headerBuf.writeBytes(numberToBytes(msg.content().readableBytes()));
            headerBuf.writeShort(RedisConstants.EOL_SHORT);
            out.add(headerBuf);
            out.add(msg.content().retain());
            out.add(allocator.ioBuffer(RedisConstants.EOL_LENGTH).writeShort(RedisConstants.EOL_SHORT));
        }
    }

    /**
     * Write array header only without body. Use this if you want to write arrays as streaming.
     */
    private void writeArrayHeader(ByteBufAllocator allocator, ArrayHeaderRedisMessage msg, List<Object> out) {
        writeArrayHeader(allocator, msg.isNull(), msg.length(), out);
    }

    /**
     * Write full constructed array message.
     */
    private void writeArrayMessage(ByteBufAllocator allocator, ArrayRedisMessage msg, List<Object> out) {
        if (msg.isNull()) {
            writeArrayHeader(allocator, msg.isNull(), RedisConstants.NULL_VALUE, out);
        } else {
            writeArrayHeader(allocator, msg.isNull(), msg.children().size(), out);
            for (RedisMessage child : msg.children()) {
                writeRedisMessage(allocator, child, out);
            }
        }
    }

    private void writeArrayHeader(ByteBufAllocator allocator, boolean isNull, long length, List<Object> out) {
        if (isNull) {
            final ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.NULL_LENGTH +
                                                   RedisConstants.EOL_LENGTH);
            RedisMessageType.ARRAY_HEADER.writeTo(buf);
            buf.writeShort(RedisConstants.NULL_SHORT);
            buf.writeShort(RedisConstants.EOL_SHORT);
            out.add(buf);
        } else {
            final ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.LONG_MAX_LENGTH +
                                                   RedisConstants.EOL_LENGTH);
            RedisMessageType.ARRAY_HEADER.writeTo(buf);
            buf.writeBytes(numberToBytes(length));
            buf.writeShort(RedisConstants.EOL_SHORT);
            out.add(buf);
        }
    }
複製程式碼

小結

對於 Netty 原始碼,樓主一直是一種敬畏的態度,沒想到今天竟然從另一個方面對 Netty 的冰山一角展開解讀,畢竟萬事開頭難,有了這一次希望之後可以更順利,在技術成長的道路上一起加油。

參考連結

相關文章