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()進行報文資料的解壓。
這裡把所用的程式打包發上來,供大家參考。額,發現好像不能傳檔案是麼,那隻好傳個連結了原始碼下載希望對大家有用吧。