前面有一篇文章是关于使用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()进行报文数据的解压。
这里把所用的程序打包发上来,供大家参考。额,发现好像不能传文件是么,那只好传个链接了源码下载希望对大家有用吧。
本文介绍了HTTP报文中的chunked分块编码传输格式,并提供了C语言实现解压的方法。首先解释了chunked编码的用途和结构,接着详细阐述了数据包重组和解压的原理,包括关键函数的功能描述。最后,通过一个测试示例展示了如何使用该实现来处理chunked编码的HTTP响应数据。
761





