Vert.x + Protobuf二進制協議解析

這一期介紹如何解析二進制私有協議。

先說幾句題外話,就是絕大多數情況下,可能根本用不着使用私有二進制協議,除非你的業務對性能極其敏感,否則HTTP足矣。

協議

我們的協議非常簡單,先是一個4字節的整數表示數據長度,然後緊接着就是protobuf序列化後的字節數組。proto定義如下:

syntax = "proto2";
package cn.fh.vertx.demo.proto;

option java_multiple_files = true;

message Message {
    required int32 type = 1;
    required string content = 2;
}

在實際業務場景中,消息格式的定義是有講究的,這裏不深究,重點在於vert.x解析。

RecordParser

Vert.x中只有一個二進制協議解析輔助類,即RecordParser,可以很好的解決粘包/拆包問題。它有兩種工作模式,一是delimited mode, 即通過固定分隔符分隔數據包,這種用的其實比較少;二是fixed size mode,即固定數據長度模式。詐一看可能很多人會有疑慮,多數協議都是可變長度的啊,這兩種模式看起來都不能滿足要求。其實模式二支持在處理的過程中隨時動態的改變size值,這樣就可以間接完成對變長協議的解析。

首先,RecordParser有兩類Builder方法,RecordParser.newDelimited()RecordParser.newFixed(4),分別對應上面的模式一和模式二。構造完成以後,跟其它vert.x的方法一樣,我們需要定義一個Handler, RecordParser每分割出一段字節數組都會調用一次Handler,業務邏輯處理就在此Handler中進行。對於上面"長度 + 數據"的協議,我們可以首先構造一個以4字節爲單位的RecordParser,在收到長度數據後,再動態將解析器修改成指定長度狀態,從而完成對數據部分的分割。代碼示例如下:

// 先以長度4構造對象
        RecordParser parser = RecordParser.newFixed(4);

        // 設置處理器
        parser.setOutput(new Handler<Buffer>() {
            // 表示當前數據長度
            int size = -1;

            @Override
            public void handle(Buffer buffer) {
                // -1表示當前還沒有長度信息,需要從收到的數據中取出長度
                if (-1 == size) {
                    // 取出長度
                    size = buffer.getInt(0);
                    // 動態修改長度
                    parser.fixedSizeMode(size);

                } else {
                    // 如果size != -1, 說明已經接受到長度信息了,接下來的數據就是protobuf可識別的字節數組
                    byte[] buf = buffer.getBytes();
                    Message msg = null;
                    try {
                        msg = Message.parseFrom(buf);
                    } catch (InvalidProtocolBufferException e) {
                        System.out.println(e.getMessage());
                        socket.close();
                        return;
                    }
                    
                    System.out.println(msg);
                    // 處理完後要將長度改回4
                    parser.fixedSizeMode(4);
                    // 重置size變量
                    size = -1;
                }
            }
        });

雖然用起來比較彆扭,但這的確是標準使用方法。

如果表示長度的數據不是從0開始的,比如0 ~ 3爲消息類型,4 ~ 7才表示body的長度,那麼可以在構造解析器時先將長度設爲8, 然後取後4字節做爲新的長度即可。

其實這個類所做的事正是Netty裏LengthFieldBasedFrameDecoder的功能,希望Vert.x以後能直接提供好類似的處理器,不要讓用戶再手動取長度了,畢竟低層的Netty都支持,你更高級的封裝怎麼可以沒有呢。

這個RecordParser怎麼用呢?看一下完整的代碼吧:

        Vertx vertx = Vertx.vertx();
        
        // 創建TCP Server
        NetServer server = vertx.createNetServer();
        // 設置Handler
        server.connectHandler(socket -> {
            // 構造parser
            RecordParser parser = RecordParser.newFixed(4);
            parser.setOutput(new Handler<Buffer>() {
                int size = -1;

                @Override
                public void handle(Buffer buffer) {
                    if (-1 == size) {
                        size = buffer.getInt(0);
                        parser.fixedSizeMode(size);
                        
                    } else {
                        byte[] buf = buffer.getBytes();
                        Message msg = null;
                        try {
                            msg = Message.parseFrom(buf);
                        } catch (InvalidProtocolBufferException e) {
                            System.out.println(e.getMessage());
                            socket.close();
                            return;
                        }
                        parser.fixedSizeMode(4);
                        size = -1;
                    }
                }
            });

            socket.handler(parser);
        });
        
        // 監聽
        server.listen(8008, "localhost", res -> {
            if (res.succeeded()) {
                System.out.println("tcp server is listening at 8008");

            } else {
                System.out.println(res.cause());
            }
        });

寫起來會有一點點奇怪,適應就好。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章