1. 程式人生 > >http報文中chunked分塊編碼傳輸格式分析及c語言解壓實現

http報文中chunked分塊編碼傳輸格式分析及c語言解壓實現

前面有一篇文章是關於使用zlib庫函式解壓以gzip壓縮方式傳輸的http報文。裡面提到了chunked分塊傳輸格式,現在由於專案需要,做了這部分的研究,現在把成果記錄下來。
首先介紹一下chunked分塊傳輸格式。對於一般的http報文,使用Content-Length欄位標明報文長度,但是對於那些無法事先確定報文大小的網頁而言,就只能使用chunked編碼方式。對於這種方式的報文,一般會使用transfer-coding欄位標明是chunked分塊傳輸格式。
Chunked編碼使用若干個Chunk串連而成,由一個標明長度為0的chunk標示結束。對於使用gzip壓縮格式的報文而言,http報文先被壓縮後被分塊,所以我們應該先把資料包重組,然後再進行解壓。簡單來說,其格式如下:
[Chunk大小][回車][Chunk資料體][回車]…(中間若干個chunk塊)…[0][回車][footer內容(有的話)][回車]
最後一個chunk塊長度為0,footer內容也一般為空。
下面舉個例子,這是我用wireshark抓的一個數據包,頭部的transfer-coding欄位標明為chunked編碼方式。“0d 0a 0d 0a”四個位元組表示頭部結束,接下來便是報文主體。“32 35”為第一個chunk塊的長度,注意chunk-size是以十六進位制的ASCII碼錶示的,所以其長度其實是十六進位制的“25”,即37個位元組。再接下來是第二個chunk塊,然後注意“30 0d 0a 0d 0a”部分,30表示本chunk塊長度為0,也即chunk串的結束標誌。

0000-000F   48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d   HTTP/1.1 200 OK.
0010-001F   0a 43 6f 6e 74 65 6e 74 2d 54 79 70 65 3a 20 74   .Content-Type: t
0020-002F   65 78 74 2f 70 6c 61 69 6e 0d 0a 54 72 61 6e 73   ext/plain..Trans
0030-003F   66 65 72 2d 45 6e 63 6f 64 69 6e 67 3a 20 63 68   fer-Encoding: ch
0040-004F   75 6e 6b 65 64
0d 0a 0d 0a 32 35 0d 0a 54 68 69 unked....25..Thi 0050-005F 73 20 69 73 20 74 68 65 20 64 61 74 61 20 69 6e s is the data in 0060-006F 20 74 68 65 20 66 69 72 73 74 20 63 68 75 6e 6b the first chunk 0070-007F 0d 0a 0d 0a 31 41 0d 0a 61 6e 64 20 74 68 69 73 ....1A..and this 0080-008F 20 69 73 20 74 68 65 20 73 65 63 6f 6e 64
20 6f is the second o 0090-009F 6e 65 0d 0a 30 0d 0a 0d 0a ne..0....

大概瞭解了chunked分塊傳輸的格式,接下來就進行解碼的工作。本著先重組在解壓的原理進行。這裡參考了這篇文章
先說一下用到的幾個重要的函式作用:
1,void *memstr(void *src, size_t src_len, char *sub);這個函式作用類似用strstr()函式,但不同在於strstr函式的字串遇到‘0’就表示字串結束,但是gzip壓縮後的資料中會有很多‘0’字元,所以strstr不再適用。
2,int dechunk(void *input, size_t inlen) ; 重組chunk塊所用的函式。

int dechunk(void *input, size_t inlen)
{
    if (!g_is_running)
    {
        return DCE_LOCK;
    }

    if (NULL == input || inlen <= 0)
    {
        return DCE_ARGUMENT;
    }

    void *data_start = input;
    size_t data_len = inlen;
    if (g_is_first)
    {
        data_start = memstr(data_start, data_len, "\r\n\r\n");
        if (NULL == data_start)
            return DCE_FORMAT;
        data_start += 4;
        data_len -= (data_start - input);
        g_is_first = 0;
    }
    if (!g_is_chunkbegin)
    {
        char *stmp = data_start;
        int itmp = 0; 
        sscanf(stmp, "%x", &itmp);
        itmp = (itmp > 0 ? itmp - 2 : itmp);          // exclude the terminate "\r\n"

        data_start = memstr(stmp, data_len, "\r\n");
        data_start += 2;    // strlen("\r\n")

        data_len        -=  (data_start - (void *)stmp);
        g_chunk_len     =   itmp;
        g_buff_outlen   +=  g_chunk_len;
        g_is_chunkbegin =   1;
        g_chunk_read    =   0;

        if (g_chunk_len > 0 && 0 != g_buff_outlen)
        {
            if (NULL == g_buff_out)
            {
                g_buff_out = (char *)malloc(g_buff_outlen);
                g_buff_pt = g_buff_out;
            }
            else
                g_buff_out = realloc(g_buff_out, g_buff_outlen);
            if (NULL == g_buff_out)
                return DCE_MEM;
        }
    }

#define CHUNK_INIT() \
do \
{ \
g_is_chunkbegin = 0; \
g_chunk_len = 0; \
g_chunk_read = 0; \
} while (0)

    if (g_chunk_read < g_chunk_len)
    {
        size_t cpsize = DC_MIN(g_chunk_len - g_chunk_read, data_len);
        memcpy(g_buff_pt, data_start, cpsize);

        g_buff_pt       += cpsize;
        g_chunk_read    += cpsize;
        data_len        -= (cpsize + 2);
        data_start      += (cpsize + 2);

        if (g_chunk_read >= g_chunk_len)
        {
            CHUNK_INIT();

            if (data_len > 0)
            {
                return dechunk(data_start, data_len);
            }
        }
    }
    else
    {
        CHUNK_INIT();
    }

#undef CHUNK_INIT()

    return DCE_OK;
}

首先判斷是否是http響應的第一個包,因為第一個包中包含有http的相應頭,我們必須把這部分內容給過濾掉,判斷的依據就是尋找兩個連續的CRLF,也就是”\r\n\r\n”。
響應body的第一行,毫無疑問是第一個chunk的size欄位,讀取出來,設定狀態,設定計數器,分配記憶體(如果不是第一個chunk的時候,通過realloc方法動態改變我們所分配的記憶體)。緊接著,就是一個對資料完整性的判斷,如果input中的剩餘資料的大小比我們還未讀取到緩衝區中的chunk的size要小,這很明顯說明了這個chunk分成了好幾次被收到,那麼我們直接按順序拷貝到我們的chunk緩衝區中即可。反之,如果input中的剩餘資料比未讀取的size大,則說明了當前這個input資料包含了不止一個chunk,此時,使用了一個遞迴來保證把資料讀取完。這裡值得注意的一點是讀取資料的時候要把表示資料結束的CRLF字元忽略掉。
總的流程基本就是這個樣子,外部呼叫者通過迴圈把socket獲取到的資料丟進來dechunk,外部迴圈結束條件就是socket接受完資料或者判斷到表示chunk結束的0資料chunk。
此外,main.c函式是用於測試的,函式中建立了一個socket連結,所訪問的網頁使用chunked格式傳輸資料。然後呼叫chunked等函式進行資料的重組。重組完之後適用zlib庫函式中的解壓函式inflate()進行報文資料的解壓。
這裡把所用的程式打包發上來,供大家參考。額,發現好像不能傳檔案是麼,那隻好傳個連結了原始碼下載希望對大家有用吧。