1. 程式人生 > 其它 >skynet原始碼分析之sproto使用方法 ——encode編碼與decode解碼、 pack打包與unpack解包

skynet原始碼分析之sproto使用方法 ——encode編碼與decode解碼、 pack打包與unpack解包

技術標籤:skynet遊戲開發luac++經驗分享

上一篇文章介紹sproto的構建流程,這一篇文章介紹sproto如何使用

A端主動給B端傳送請求:呼叫request_encode對lua表進行編碼,再用sproto.pack打包。

B端收到A端的請求:用sproto.unpack解包,再呼叫request_decode解碼成lua表。

B端給A端傳送返回包:用response_encode對lua表進行編碼,然後用sproto.pack打包。

A端收到B端的返回包:用sproto.unpack解包,再呼叫request_decode解碼成lua表。

不管是是request_encode還是response_encode,最終都會呼叫c層的encode介面,request_decode和response_decode都會呼叫c層decode介面。encode負責將lua資料表編碼成二進位制資料塊,而decode負責解碼,二者是互補操作。同樣,pack和unpack也是互補操作。

-- lualib/sproto.lua
function sproto:request_encode(protoname, tbl)
    ...
    return core.encode(request,tbl) , p.tag
end

function sproto:response_encode(protoname, tbl)
    ...
    return core.encode(response,tbl)
end



function sproto:request_decode(protoname, ...)
    ...
    return core.decode(request,...) , p.name
end

function sproto:response_decode(protoname, ...)
    ...
    return core.decode(response,...)
end


sproto.pack = core.pack
sproto.unpack = core.unpack

1. encode編碼

先放一個例子(在github上有),分析原始碼時會用到:

person { name = "Alice" ,  age = 13, marital = false } 

03 00 (fn = 3)
00 00 (id = 0, value in data part)
1C 00 (id = 1, value = 13)
02 00 (id = 2, value = false)
05 00 00 00 (sizeof "Alice")
41 6C 69 63 65 ("Alice")

encode的目的是按指定協議型別將lua表裡的資料轉化成c中的型別,然後按特定格式編碼成一串二進位制資料塊。

最終呼叫sproto_encode api編碼,有5個引數:st,sproto指定型別的c結構;buffer、size,存放編碼結果的緩衝區和大小,如果緩衝區不夠,會擴充緩衝區,重新編碼;cb,對應lsproto.c中encode api,是一個c介面,負責獲取lua表中指定key的值,或陣列中指定索引位置的值;ud,額外資訊,包含lua與c之間互動用的虛擬棧、sproto中對應型別的c結構等。

第3-6行,編碼結果分兩部分:頭部header和資料data,header長度是固定的,等於2位元組field總數+field的數目*2位元組每個field長度。如下圖:header指標指向緩衝區首地址,data指向header+header_sz位置,接下

來編碼每個field資訊時,data指標會往後移動,而header指標保持不動。

第63-65行,將field的總數按大端格式打包長2位元組大小(示例中的03 00),data指向header+header_sz處,最後用memmove將頭部和資料塊連在一起。

接下來就是編碼每一個field資料,根據field型別做不同的處理:

第11-13行,如果是array,呼叫encode_array編碼,稍後介紹。

第33-37行,如果是string或自定義型別,呼叫encode_object編碼,稍後介紹。

第16-32行,如果是integer或boolean型別,呼叫cb(lsproto.c中的encode)獲取lua表中對應field名字的數值,儲存到args.value(即u中)。第21行,變數value等於(原來的值+1)*2,因為編碼後的0有特殊作用,為了區分原來值是0的情況。

第58-59行,最後將value按大端格式編碼2位元組,存到header指定的位置。比如示例中的1C 00,(13+1)*2=28=1C,02 00,(0+1)*2=2=02,注:lua中的false會編碼成0,true編碼成1。如果是array、string或自定義型別,value是0,編碼後是00 00,代表數值在data部分。

第47-56行,如果某些tag沒有設定值,需要把tag資訊編碼到header裡。

// lualib/sproto/sproto.c
int sproto_encode(const struct sproto_type *st, void * buffer, int size, sproto_callback cb, void *ud) {
    uint8_t * header = buffer;
    uint8_t * data;
    int header_sz = SIZEOF_HEADER + st->maxn * SIZEOF_FIELD;
    data = header + header_sz;
    ...
    for (i=0;i<st->n;i++) {
        struct field *f = &st->f[i];
        int type = f->type;
        if (type & SPROTO_TARRAY) {
            args.type = type & ~SPROTO_TARRAY;
            sz = encode_array(cb, &args, data, size);
        } else {
            switch(type) {
                case SPROTO_TINTEGER:
                case SPROTO_TBOOLEAN: {
                    sz = cb(&args);
                    if (sz == sizeof(uint32_t)) {
                        if (u.u32 < 0x7fff) {
                            value = (u.u32+1) * 2;
                            sz = 2; // sz can be any number > 0
                        } else {
                            sz = encode_integer(u.u32, data, size);
                        }
                    } else if (sz == sizeof(uint64_t)) {
                        sz= encode_uint64(u.u64, data, size);
                    } else {
                       return -1;
                    }
                    break;
                }
                case SPROTO_TSTRUCT:
                case SPROTO_TSTRING:
                    sz = encode_object(cb, &args, data, size);
                    break;
                }
            if (sz > 0) {
                uint8_t * record;
                int tag;
                if (value == 0) {
                    data += sz;
                    size -= sz;
                }
                record = header+SIZEOF_HEADER+SIZEOF_FIELD*index;
                tag = f->tag - lasttag - 1;
                if (tag > 0) {
                    // skip tag
                    tag = (tag - 1) * 2 + 1;
                    if (tag > 0xffff)
                        return -1;
                    record[0] = tag & 0xff;
                    record[1] = (tag >> 8) & 0xff;
                    ++index;
                    record += SIZEOF_FIELD;
                }
                ++index;
                record[0] = value & 0xff;
                record[1] = (value >> 8) & 0xff;
                lasttag = f->tag;
           }
       }
       header[0] = index & 0xff;
       header[1] = (index >> 8) & 0xff;          datasz = data - (header+header_sz);          data = header +header_sz;          memmove(header + SIZEOF_HEADER + index * SIZEOF_FIELD, data, datasz);
}

如果是string或自定義型別,呼叫encode_object編碼,4個引數是:cb,即lsproto.c中encode介面;args,額外引數;data,存放編碼結果的緩衝區,由4個位元組的長度+具體資料組成;size,緩衝區長度

第9行,填充4位元組的長度放到data的首地址處,比如示例中05 00 00 00

第5行,資料從data+SIZEOF_LENGTH開始存放,前4個位元組存放資料長度

第26行,如果是字串,拷貝字串到指定位置,比如示例中41 6C 69 63 65("Alice")

第31行,如果是自定義型別,對子型別再次呼叫sproto_encode遞迴處理

// lualib-src/sproto/sproto.c
 static int
 encode_object(sproto_callback cb, struct sproto_arg *args, uint8_t *data, int size) {
     int sz;
     args->value = data+SIZEOF_LENGTH;
     args->length = size-SIZEOF_LENGTH;
     sz = cb(args);
     ...
     return fill_size(data, sz);
 }

 static inline int
 fill_size(uint8_t * data, int sz) {
     data[0] = sz & 0xff;
     data[1] = (sz >> 8) & 0xff;
     data[2] = (sz >> 16) & 0xff;
     data[3] = (sz >> 24) & 0xff;
     return sz + SIZEOF_LENGTH;
 }

// lualib-src/sproto/lsproto.c
static int
encode(const struct sproto_arg *args) {
    ...
    case SPROTO_TSTRING: {
        memcpy(args->value, str, sz);
        ...
    }
    case SPROTO_TSTRUCT: {
        ...
        r = sproto_encode(args->subtype, args->value, args->length, encode, &sub);
    }
}

如果是array型別,呼叫encode_array進行編碼,遍歷陣列,對每一個元素進行編碼,同樣把資料長度編碼成4個位元組填充到前面。例如:

children = {
        { name = "Alice" ,  age = 13 },
        { name = "Carol" ,  age = 5 },
    }
26 00 00 00 (sizeof children)

0F 00 00 00 (sizeof child 1)
02 00 (fn = 2)
00 00 (id = 0, value in data part)
1C 00 (id = 1, value = 13)
05 00 00 00 (sizeof "Alice")
41 6C 69 63 65 ("Alice")

0F 00 00 00 (sizeof child 2)
02 00 (fn = 2)
00 00 (id = 0, value in data part)
0C 00 (id = 1, value = 5)
05 00 00 00 (sizeof "Carol")
43 61 72 6F 6C ("Carol")

注:如果陣列元素是整數,在長度和資料之間會多用一個位元組用來標記是小整數(小於2^32)還是大整數,小整數用4個位元組(32位)存放,大整數用8個位元組(64位)存放,例如:

numbers = { 1,2,3,4,5 }
15 00 00 00 (sizeof numbers)
04 ( sizeof int32 )
01 00 00 00 (1)
02 00 00 00 (2)
03 00 00 00 (3)
04 00 00 00 (4)
05 00 00 00 (5)

小結:編碼後的二進位制資料塊由頭部和資料兩部分組成。頭部包含field總數,以及每個field值。資料部分由長度和具體的數值組成。如果field值為0,表示資料在資料部分(array、string或自定義型別);如果field值最後一位為1,表示該field沒資料;否則field值可直接轉化對應lua資料(integer或boolean型別)。

2. decode解碼

瞭解了encode編碼過程,decode解碼過程就是編碼的逆過程,將二進位制資料塊解碼成lua表。5個引數:st,sproto型別的c結構;data和size,待解碼的二進位制資料塊和長度;cb,是一個c介面,即lsproto.c中decode,負責將c型別的資料push到lua虛擬棧裡,然後供lua層使用;ud,額外引數,包括cb中需要用的lua虛擬棧。

第9-12行,獲取頭兩位元組表示field總數fn,stream指向頭部,datastream指向資料塊

第17行,對每一個field進行解碼

第20行,獲取field的值value。如果value最後一位為1,說明之後value/2個tag都沒資料(第22-25行);

第26行,計算value的實際值,currentdata指向當前資料塊(第27行)。如果小於0,說明是array、string或自定義型別,說明資料在資料部分,計算出資料長度sz,然後把datastream移到下一個field對應的資料塊的位置(28-33行)。

第34-37行,找出tag對應的field資訊,賦值給args,呼叫cb時根據args資訊進行相應轉化。

第61-66行,如果是integer或boolean型別,value即資料本身,呼叫cb,設定lua虛擬棧指定表的指定key的位置。

第49-58行,如果是string或自定義型別,先從資料部分中獲取資料(52行),再呼叫cb。

第39-42行,如果是array型別,呼叫decode_array解碼

// lualib-src/sproto/sproto.c
int
sproto_decode(const struct sproto_type *st, const void * data, int size, sproto_callback cb, void *ud) {
    struct sproto_arg args;
    int total = size;
    uint8_t * stream;
    uint8_t * datastream;
    stream = (void *)data;
    fn = toword(stream);
    stream += SIZEOF_HEADER;
    size -= SIZEOF_HEADER ;
    datastream = stream + fn * SIZEOF_FIELD;
    size -= fn * SIZEOF_FIELD;
    args.ud = ud;

    tag = -1;
    for (i=0;i<fn;i++) {
        uint8_t * currentdata;
        struct field * f;
        int value = toword(stream + i * SIZEOF_FIELD);
        ++ tag;
        if (value & 1) {
            tag += value/2;
            continue;
        }
        value = value/2 - 1;
        currentdata = datastream;
        if (value < 0) {
            uint32_t sz;
            sz = todword(datastream);
            datastream += sz+SIZEOF_LENGTH;
            size -= sz+SIZEOF_LENGTH;
        }
        f = findtag(st, tag);

        args.tagname = f->name;
        ...
        if (value < 0) {
            if (f->type & SPROTO_TARRAY) {
                if (decode_array(cb, &args, currentdata)) {
                    return -1;
                }
            } else {
                switch (f->type) {
                case SPROTO_TINTEGER: {
                    ...
                    break;
                }
                case SPROTO_TSTRING:
                case SPROTO_TSTRUCT: {
                    uint32_t sz = todword(currentdata);
                    args.value = currentdata+SIZEOF_LENGTH;
                    args.length = sz;
                    if (cb(&args))
                        return -1;
                        break;
                }
            }
        } else if (f->type != SPROTO_TINTEGER && f->type != SPROTO_TBOOLEAN) {
            return -1;
        } else {
            uint64_t v = value;
            args.value = &v;
            args.length = sizeof(v);
            cb(&args);
        }
   }
   return total - size;
}

3. pack打包 與unpack解包

將lua表編碼成特定的二進位制資料塊後,再用pack打包。其原理是:每8個位元組為一組,打包後由第一個位元組+原資料不為0的位元組組成,第一個位元組的每一位為0時表示原位元組為0,否則就是跟隨的某個位元組。當第一個位元組是FF時,有特殊含義,假設下一位元組為N,表示接下來(N+1)*8個位元組都是原資料。例如:

unpacked (hex):  08 00 00 00 03 00 02 00   19 00 00 00 aa 01 00 00
packed (hex):  51 08 03 02   31 19 aa 01

51 = 0101 0001,從右到左數,表示該組第1,5,7個位置一次是08,03,02,其餘位置都是0。

呼叫sproto_pack打包,4個引數:srcv、srcsz原資料塊和長度;bufferv、bufsz存放打包後資料的緩衝區和長度。

第5-6行,ff_srcstart,ff_desstart分別指向ff代表的源地址和目的地址

第11行,8個一組進行打包

第17-19行,不足8個,用0填充

第22行,呼叫pack_seg,打包成特定格式,存放在buffer裡

第33,40行,如果ff_n>0,呼叫write_ff,按照ff的含義,重新打包,然後存放在buffer裡。

int
sproto_pack(const void * srcv, int srcsz, void * bufferv, int bufsz) {
    uint8_t tmp[8];
    int i;
    const uint8_t * ff_srcstart = NULL;
    uint8_t * ff_desstart = NULL;
    int ff_n = 0;
    int size = 0;
    const uint8_t * src = srcv;
    uint8_t * buffer = bufferv;
    for (i=0;i<srcsz;i+=8) {
        int n;
        int padding = i+8 - srcsz;
        if (padding > 0) {
            int j;
            memcpy(tmp, src, 8-padding);
            for (j=0;j<padding;j++) {
                tmp[7-j] = 0;
            }
            src = tmp;
        }
        n = pack_seg(src, buffer, bufsz, ff_n);
        bufsz -= n;
        if (n == 10) {
            // first FF
            ff_srcstart = src;
            ff_desstart = buffer;
            ff_n = 1;
        } else if (n==8 && ff_n>0) {
            ++ff_n;
            if (ff_n == 256) {
                if (bufsz >= 0) {
                    write_ff(ff_srcstart, ff_desstart, 256*8);
                }
                ff_n = 0;
            }
        } else {
            if (ff_n > 0) {
                if (bufsz >= 0) {
                    write_ff(ff_srcstart, ff_desstart, ff_n*8);
                }
                ff_n = 0;
            }
        }
        src += 8;
        buffer += n;
        size += n;
    }
    if(bufsz >= 0){
        if(ff_n == 1)
            write_ff(ff_srcstart, ff_desstart, 8);
        else if (ff_n > 1)
            write_ff(ff_srcstart, ff_desstart, srcsz - (intptr_t)(ff_srcstart - (const uint8_t*)srcv));
    }
    return size;
}

瞭解打包原理後,解包就是打包的逆過程,變得很容易了。呼叫sproto_unpack解包:

第11-27行,如果第一個位元組是ff,計算出可直接拷貝的位元組數n,然後拷貝到buffer。

第30-50行,計算第一個位元組的每一位(總共8位),如果是1,複製跟隨的一個位元組給buffer(32-41行);否則,設定buffer為0(42-49行)。

// lualib-src/sproto/sproto.c
int
sproto_unpack(const void * srcv, int srcsz, void * bufferv, int bufsz) {
    const uint8_t * src = srcv;
    uint8_t * buffer = bufferv;
    int size = 0;
    while (srcsz > 0) {
        uint8_t header = src[0];
        --srcsz;
        ++src;
        if (header == 0xff) {
            int n;
            if (srcsz < 0) {
                return -1;
            }
            n = (src[0] + 1) * 8;
            if (srcsz < n + 1)
                return -1;
            srcsz -= n + 1;
            ++src;
            if (bufsz >= n) {
                memcpy(buffer, src, n);
             }
             bufsz -= n;
             buffer += n;
             src += n;
             size += n;
         } else {
             int i;
             for (i=0;i<8;i++) {
                 int nz = (header >> i) & 1;
                 if (nz) {
                     if (srcsz < 0)
                         return -1;
                     if (bufsz > 0) {
                         *buffer = *src;
                          --bufsz;
                          ++buffer;
                      }
                      ++src;
                      --srcsz;
                  } else {
                      if (bufsz > 0) {
                          *buffer = 0;
                          --bufsz;
                          ++buffer;
                      }
                  }
                  ++size;
              }
        }
    }
    return size;
}

本篇文章就寫到這了,

在2021年1月13/14號我會開一個四小時玩轉skynet訓練營,也就是兩個禮拜之後,現在已經開放報名,對遊戲開發感興趣的諸位同好可以訂閱一下,

訓練營內容大概如下:

1. 多核併發程式設計
2. 訊息佇列,執行緒池
3. actor訊息排程
4. 網路模組實現
5. 時間輪定時器實現
6. lua/c介面程式設計
7. skynet程式設計精要
8. demo演示actor程式設計思維

期待大家一起來打造遊戲開發的技術盛宴。

憑藉報名截圖可以進群973961276領取上一期skynet訓練營的錄播以及這期的預習資料哦!