netty系列之:netty中的核心解碼器json

flydean發表於2022-04-26

簡介

程式和程式之間的資料傳輸方式有很多,可以通過二進位制協議來傳輸,比較流行的像是thrift協議或者google的protobuf。這些二進位制協議可以實現資料的有效傳輸,並且通過二進位制的形式可以節省資料的體積,在某些速度和效率優先的情況下是非常有效的。並且如果不同的程式語言之間的相互呼叫,也可以通過這種二進位制的協議來實現。

雖然二進位制更加快速和有效,但是對於程式設計師來說不是很友好,因為一個人很難直接讀取二進位制檔案,雖然也存在一些一些文字的資料傳輸方式,比如XML,但是XML的繁瑣的標籤導致了XML在使用中有諸多的不便。於是一種通用的文字檔案傳輸格式json誕生了。

能讀到這篇文章的朋友肯定對json不陌生了,當然還有一些更加簡潔的檔案格式,比如YAML,感興趣的朋友可以更深入的瞭解一下。

這裡我們想要講的是netty對json的解碼。

java中對json的支援

在java中我們json的使用通常是將一個物件轉換成為json進行資料傳輸,或者將接收到json進行解析,將其轉換成為物件。

可惜的是在JDK中並沒有提供給一個好用的JSON工具,所以我們一般需要藉助第三方的JSON包來實現Object和JSON之間的轉換工作。

通常使用的有google的GSON,阿里的FastJSON和jackson等。

這裡我們使用google的GSON來進行介紹。

這裡我們主要講解的是java中物件和json的互相轉換,所以GSON中其他更加強大的功能這裡就不介紹了。

首先我們建立一個JAVA物件,我們定義一個Student類,如下所示:

    static class Student {
        String name;
        String phone;
        Integer age;

        public Student(String name, String phone, Integer age) {
            this.name = name;
            this.phone = phone;
            this.age = age;
        }
    }

這個類中,我們為Student定義了幾個不同的屬性和一個建構函式。接下來我們看下如何使用GSON來對這個物件進行JSON的轉換:

        Student obj = new Student("tina","188888888",18);
        Gson gson = new Gson();
        String json = gson.toJson(obj);
        System.out.println(json);
        Student obj2 = gson.fromJson(json, Student.class);
        System.out.println(obj2);

GSON使用起來非常簡單,我們構建好Gson物件之後,直接呼叫它的toJson方法即可將物件轉換成為json字串。

然後呼叫json的fromJson方法就可以將json字串轉換成為物件。

上面的程式碼輸出如下:

{"name":"tina","phone":"188888888","age":18}
com.flydean.JsonTest$Student@4534b60d

netty對json的解碼

netty為json提供了一個解碼器叫做JsonObjectDecoder,先來看下JsonObjectDecoder的定義:

public class JsonObjectDecoder extends ByteToMessageDecoder

和前面講解的base64,byte陣列不同的是,JsonObjectDecoder繼承的是ByteToMessageDecoder而不是MessageToMessageDecoder。

這說明JsonObjectDecoder是直接從ByteBuf轉換成為Json Object物件。

我們知道JDK中並沒有JSON這個物件,所有的物件都是從第三方包中引入的,netty並沒有引入新的物件,所以netty中從Json中解析出來的物件還是一個ByteBuf物件,在這個ByteBuf中包含了一個Json物件。

JsonObjectDecoder的解析邏輯是怎麼樣的呢?

首先來看下JsonObjectDecoder中定義的4個state:

    private static final int ST_CORRUPTED = -1;
    private static final int ST_INIT = 0;
    private static final int ST_DECODING_NORMAL = 1;
    private static final int ST_DECODING_ARRAY_STREAM = 2;

ST_INIT表示的是decode的初始狀態,ST_CORRUPTED表示的是decode中出現的異常狀態。

ST_DECODING_NORMAL代表的是一個普通的json,如下所示:

{
    "source": "web",
    "type": "product_info",
    "time": 1641967014440,
    "data": {
        "id": 30000084318055,
        "staging": false
    },
    "dataId": "123456"
}

ST_DECODING_ARRAY_STREAM代表的是一個陣列,對於陣列來說,陣列也是一個物件,所以陣列也可以用json表示,下面就是一個常見的json陣列:

[ "Google", "Runoob", "Taobao" ]

JsonObjectDecoder的解碼邏輯比較簡單,它主要是讀取ByteBuf中的資料,通過判斷讀取的資料和json中特有的大括號,中括號,逗號等分隔符來分割和解析json物件。

要注意的是,JsonObjectDecoder要解碼的ByteBuf中的訊息應該是UTF-8編碼格式的,為什麼需要UTF-8格式呢?

這是因為json中那些特有的分隔符,即使在UTF-8中也是用一個byte來儲存的,這樣我們在讀取資料的過程中,可以通過讀取的byte值和json的分隔符進行比較,從而來確定json中不同物件的界限。

如果換成其他的編碼方式,json中的分隔符可能會用多個byte來表示,這樣對我們的解析就提高了難度,因為我們需要知道什麼時候是分隔符的開始,什麼時候是分隔符的結束。

它的核心解碼邏輯如下,首先從ByteBuf中讀取一個byte:

byte c = in.getByte(idx);

然後通過呼叫decodeByte(c, in, idx); 來判斷當前的位置是開括號,還是閉括號,是在一個物件的字串中,還是一個新的物件字串。

首先需要對當前的state做一個判斷,state判斷呼叫的是initDecoding方法:

    private void initDecoding(byte openingBrace) {
        openBraces = 1;
        if (openingBrace == '[' && streamArrayElements) {
            state = ST_DECODING_ARRAY_STREAM;
        } else {
            state = ST_DECODING_NORMAL;
        }
    }

接著就是對當前的state和自定義的4個狀態進行比較,如果是普通的json物件,並且物件已經是閉括號狀態,說明該物件已經讀取完成,可以將其進行轉換並輸出了:

 if (state == ST_DECODING_NORMAL) {
                decodeByte(c, in, idx);
                if (openBraces == 0) {
                    ByteBuf json = extractObject(ctx, in, in.readerIndex(), idx + 1 - in.readerIndex());
                    if (json != null) {
                        out.add(json);
                    }
    ...

如果state表示目前是一個陣列物件,陣列物件中可能包含多個物件,這些物件是通過逗號來區分的。逗號之間還可能會有空格,所以需要對這些資料進行特殊判斷和處理,如下所示:

else if (state == ST_DECODING_ARRAY_STREAM) {
                decodeByte(c, in, idx);

                if (!insideString && (openBraces == 1 && c == ',' || openBraces == 0 && c == ']')) {
                    for (int i = in.readerIndex(); Character.isWhitespace(in.getByte(i)); i++) {
                        in.skipBytes(1);
                    }
                    int idxNoSpaces = idx - 1;
                    while (idxNoSpaces >= in.readerIndex() && Character.isWhitespace(in.getByte(idxNoSpaces))) {
                        idxNoSpaces--;
                    }
                    ByteBuf json = extractObject(ctx, in, in.readerIndex(), idxNoSpaces + 1 - in.readerIndex());
                    if (json != null) {
                        out.add(json);
                    }
    ....

最後將解析出來的json物件放入byteBuf的out list中,整個解析到此結束。

總結

以上就是netty中json核心解碼器JsonObjectDecoder的使用,它的本質是通過判斷json物件中的分割符來分割多個json字串,然後將分割後的json字串存入ByteBuf中輸出。

看到這裡,大家可能會疑惑了,decoder不是和encoder一起出現的嗎?為什麼netty中只有JsonObjectDecoder,而沒有JsonObjectEncoder呢?

事實上,這裡的Json物件就是一個包含Json字元的字串,這個字串被寫入到ByteBuf中,所以這裡並不需要特殊的encoder。

本文已收錄於 http://www.flydean.com/14-3-netty-codec-json/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章