关于以chunked方式传输的HTTP响应报文的解码C语言实现
注意:
该文转载出处已不可访问,找到一篇相似的文章,可以参考:
https://blog.csdn.net/liuchonge/article/details/50061331
– 2019-11-28 更新
转自:http://www.devdiv.com/chunked_http_c_-article-2473-1.html
今天的主要内容还是不会偏离帖子的标题,关于HTTP采用chunked方式传输数据的解码问题。相信“在座”的各位有不少是搞过web系统的哈,但考虑到其他没接触过的XDJMs,这里还是简要的介绍一下:
chunked编码的基本方法是将大块数据分解成多块小数据,每块都可以自指定长度,其具体格式RFC文档中是这样描述的(http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html):
1 | Chunked-Body = *chunk |
这个据说是BNF文法,我本人有点陌生,于是又去维基百科里面找了下,里面有报文举例,这样就一目了然了(http://en.wikipedia.org/wiki/Chunked_transfer_encoding),我摘一段报文如下:
1 | HTTP/1.1 200 OK |
总而言之呢,就是说,这种方式的HTTP响应,头字段中不会带上我们常见的Content-Length字段,而是带上了Transfer-Encoding: chunked的字样。这种响应是未知长度的,有很多段自定义长度的块组合而成,每个块以一个十六进制数开头,该数字表示这块chunk数据的长度(包括数据段末尾的CRLF,但不包括十六进制数后面的CRLF)。
于是,众多Coders在发现了这个真相以后就开始在互联网上共享各种语言的解码代码。我看了C、PHP和Python那几个版本的代码,发现了一个问题就是,他们解析的数据是完整的,也就是说,他们所操纵的数据是假定已经在解码前在外部完成了拼装的,但是这完全不符合我的使用场景,Why?因为我的数据都是直接从Socket里面拿出来的,Socket里面的数据绝对不会有如此漂亮的格式,它们在那个时候都是散装的,当然我也可以选择将他们组装好然后再去解,但是以我粗浅的认识认为,那样子无论是从时间还是从空间的效率上来讲都是极为低下的(当你开发了一个kext程序,就明白我的苦衷了)。于是我又继续搜索,以期待能有高手已经提前帮我解决了这些问题,不过很遗憾,我没能找到。
没办法,自己做吧,比较重要的地方无非就是一个结尾的判断、一个chunk长度的读取、chunk之间的分段问题。看起来貌似比较轻松,不过代码写起来还是花费了不少时间的,今天又单独从项目中提取了这部分功能用C重写了一下。接下来就结合部分代码来说明一下整个过程。
- 先看dechunk.h这个文件
1 | #define DCE_OK 0 |
宏定义就不用说了,都是一些错误码定义。函数一共有5个。dechunk_init、dechunk、dechunk_free这三个是解码的主要流程,dechunk_getbuff则是获取数据的接口。接下来看memstr,这是个很奇怪的名字,也是代码中唯一值得重点提醒一下的地方,其主要功能是在一块内存中寻找能匹配sub表示的字符串的地址。有人肯定要问了,不是有strstr么?对,我也这样想过,并且对于一些chunked网站也是实用的,但是,它不是通用的。主要是因为还有一些网站不仅使用了chunked传输方式,还采用了gzip的内容编码方式。当你碰到这种网站的时候,你再想使用strstr就等着郁闷吧,因为strstr会以字符串中的’\0’字符作为结尾标识,而恰巧经过gzip编码后的数据会有大量的这种字符。
- 接下来看dechunk.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102int 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;
}
其他函数没什么好说的,主要就只是把dechunk这个函数的流程讲一下(本来要是写了注释,我就不啰嗦这么多了,没办法,我们还是要对自己写的代码负责的不是)。
首先判断是否初始化过,全局变量g_is_running的唯一用途就只是用来防止多线程的调用,这只是一种很低级的保护,大家可以在实际使用中仁者见仁,智者见智。接下来,判断是否是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
1 | int main (int argc, const char * argv[]) |
按照惯例,main函数是我们用来测试的函数。
这个函数中,我们首先使用socket创建了跟服务器之间的连接,紧接着我手动构造了一个请求报文,通过socket发送出去,然后循环获取数据。然后通过使用zlib库来对dechunk出来的数据进行解码以确定数据是否正确。关于zlib的使用跟本次讨论的话题不太沾边,这里就不详述,有兴趣的我们可以另行讨论。
解码前和解码后的数据都会被打印到控制台中去,日志比较庞大,这里就不给出具体信息了,大家可以自行调试观察。
关于这个文件我说明一下,网站选的是www.mtime.com,因为它采用的就是chunked + gzip的方式,是一种相对难处理的数据。该网站的IP地址信息,相信各位有不下于100种方法去找到,所以我就没有使用gethostbyname那个方法了,因为那个方法返回的结构体使用起来实在不怎么方便。另外就是关于手动拼装的请求报文哪里来,千万不要告诉我你去用各种专业抓包工具去抓。没那么麻烦,打开你的Chrome,右键选择审查元素,然后访问你要访问的网站,OK,所有请求都会被记录在案。
/**分割一下吧/
关于代码就说这么多,不过我还是声明一下,因为没多少时间,所以我只能尽力把代码写的不那么难看,注释不多,但我在这里也有所弥补,各个函数功能的实现也许有效率方面的问题抑或是各种bug,这个属于我的编程能力不足,大家可以提出宝贵的修改意见,我一定虚心接受。如果有不明白的地方(我这只是以防万一哈^_^)也可以跟帖一起讨论。根据设计的原则,每个函数的功能应该尽量的单一和完整,调用者应该确保传递的数据符合函数的要求,但是我的main函数中却没有一些必要的判断(比如http响应是否是chunked的,是否是gzip的),因为我准备的测试数据已经确定好了的。
写了那么多,也该结尾了,不过还是要送上源码工程以祝大家天天晚上睡好觉!
源码在这里 —>
Vincent,如果您要查看本帖隐藏内容请回复
doors.du说:“我觉得明天起床这个帖子会火的,不管你们信不信,我反正信了……”
http_chunk_demo.zip (19.35 KB, 下载次数: 2742)