http chunk 动态长度传输

  哈罗,大家好,又是好久没灌水了!近一个月来,纠结于Kext开发过程中的各种疑难杂症,好不容易在昨天算是云开日现了,可惜,夏天就在这无声无息中飘然而逝。为了缅怀一下夏天MM们的短裙,于是,今天送上这期间的一点心得体会,希望大家喜欢。

今天的主要内容还是不会偏离帖子的标题,关于HTTP采用chunked方式传输数据的解码问题。相信“在座”的各位有不少是搞过web系统的哈,但考虑到其他没接触过的XDJMs,这里还是简要的介绍一下:

chunked编码的基本方法是将大块数据分解成多块小数据,每块都可以自指定长度,其具体格式RFC文档中是这样描述的( http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html):
  1.        Chunked-Body   = *chunk
  2.                         last-chunk
  3.                         trailer
  4.                         CRLF
  5.        chunk          = chunk-size [ chunk-extension ] CRLF
  6.                         chunk-data CRLF
  7.        chunk-size     = 1*HEX
  8.        last-chunk     = 1*("0") [ chunk-extension ] CRLF
  9.        chunk-extension= *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
  10.        chunk-ext-name = token
  11.        chunk-ext-val  = token | quoted-string
  12.        chunk-data     = chunk-size(OCTET)
  13.        trailer        = *(entity-header CRLF)
复制代码
这个据说是BNF文法,我本人有点陌生,于是又去维基百科里面找了下,里面有报文举例,这样就一目了然了( http://en.wikipedia.org/wiki/Chunked_transfer_encoding),我摘一段报文如下:
  1. HTTP/1.1 200 OK
  2. Content-Type: text/plain
  3. Transfer-Encoding: chunked

  4. 25
  5. This is the data in the first chunk

  6. 1C
  7. and this is the second one

  8. 3
  9. con
  10. 8
  11. sequence
  12. 0
复制代码
总而言之呢,就是说,这种方式的HTTP响应,头字段中不会带上我们常见的 Content-Length字段,而是带上了 Transfer-Encoding: chunked的字样。这种响应是 未知长度的,有很多段自定义长度的块组合而成,每个块以一个 十六进制数开头,该数字表示这块chunk数据的长度( 包括数据段末尾的CRLF,但不包括十六进制数后面的CRLF)。

于是,众多Coders在发现了这个真相以后就开始在互联网上共享各种语言的解码代码。我看了C、PHP和Python那几个版本的代码,发现了一个问题就是,他们解析的数据是完整的,也就是说,他们所操纵的数据是假定已经在解码前在外部完成了拼装的,但是这完全不符合我的使用场景,Why?因为我的数据都是直接从Socket里面拿出来的,Socket里面的数据绝对不会有如此漂亮的格式,它们在那个时候都是散装的,当然我也可以选择将他们组装好然后再去解,但是以我粗浅的认识认为,那样子无论是从时间还是从空间的效率上来讲都是极为低下的(当你开发了一个kext程序,就明白我的苦衷了 )。于是我又继续搜索,以期待能有高手已经提前帮我解决了这些问题,不过很遗憾,我没能找到。

没办法,自己做吧,比较重要的地方无非就是一个结尾的判断、一个chunk长度的读取、chunk之间的分段问题。看起来貌似比较轻松,不过代码写起来还是花费了不少时间的,今天又单独从项目中提取了这部分功能用C重写了一下。接下来就结合部分代码来说明一下整个过程。

1. 先看dechunk.h这个文件
  1. #define DCE_OK              0
  2. #define DCE_ISRUNNING       1
  3. #define DCE_FORMAT          2
  4. #define DCE_ARGUMENT        3
  5. #define DCE_MEM             4
  6. #define DCE_LOCK            5

  7. int dechunk_init();
  8. int dechunk(void *input, size_t inlen);
  9. int dechunk_getbuff(void **buff, size_t *buf_size);
  10. int dechunk_free();

  11. void *memstr(void *src, size_t src_len, char *sub);
复制代码
宏定义就不用说了,都是一些错误码定义。函数一共有5个。dechunk_init、dechunk、dechunk_free这三个是解码的主要流程,dechunk_getbuff则是获取数据的接口。接下来看memstr,这是个很奇怪的名字,也是代码中唯一值得重点提醒一下的地方,其主要功能是在一块内存中寻找能匹配sub表示的字符串的地址。有人肯定要问了,不是有strstr么?对,我也这样想过,并且对于一些chunked网站也是实用的,但是,它不是通用的。主要是因为还有一些网站不仅使用了chunked传输方式,还采用了gzip的内容编码方式。当你碰到这种网站的时候,你再想使用strstr就等着郁闷吧,因为strstr会以字符串中的'\0'字符作为结尾标识,而恰巧经过gzip编码后的数据会有大量的这种字符。

2. 接下来看dechunk.c
  1. int dechunk(void *input, size_t inlen)
  2. {
  3.     if (!g_is_running)
  4.     {
  5.         return DCE_LOCK;
  6.     }
  7.    
  8.     if (NULL == input || inlen <= 0)
  9.     {
  10.         return DCE_ARGUMENT;
  11.     }
  12.    
  13.     void *data_start = input;
  14.     size_t data_len = inlen;
  15.    
  16.     if (g_is_first)
  17.     {
  18.         data_start = memstr(data_start, data_len, "\r\n\r\n");
  19.         if (NULL == data_start)
  20.         {
  21.             return DCE_FORMAT;
  22.         }
  23.         
  24.         data_start += 4;
  25.         data_len -= (data_start - input);
  26.         
  27.         g_is_first = 0;
  28.     }
  29.    
  30.     if (!g_is_chunkbegin)
  31.     {
  32.         char *stmp = data_start;
  33.         int itmp = 0;
  34.         
  35.         sscanf(stmp, "%x", &itmp);
  36.         itmp = (itmp > 0 ? itmp - 2 : itmp);          // exclude the terminate "\r\n"
  37.         
  38.         data_start = memstr(stmp, data_len, "\r\n");
  39.         data_start += 2;    // strlen("\r\n")
  40.         
  41.         data_len        -=  (data_start - (void *)stmp);
  42.         g_chunk_len     =   itmp;
  43.         g_buff_outlen   +=  g_chunk_len;
  44.         g_is_chunkbegin =   1;
  45.         g_chunk_read    =   0;
  46.         
  47.         if (g_chunk_len > 0 && 0 != g_buff_outlen)
  48.         {
  49.             if (NULL == g_buff_out)
  50.             {
  51.                 g_buff_out = (char *)malloc(g_buff_outlen);
  52.                 g_buff_pt = g_buff_out;
  53.             }
  54.             else
  55.             {
  56.                 g_buff_out = realloc(g_buff_out, g_buff_outlen);
  57.             }
  58.             
  59.             if (NULL == g_buff_out)
  60.             {
  61.                 return DCE_MEM;
  62.             }
  63.         }
  64.     }
  65.    
  66. #define CHUNK_INIT() \
  67. do \
  68. { \
  69. g_is_chunkbegin = 0; \
  70. g_chunk_len = 0; \
  71. g_chunk_read = 0; \
  72. } while (0)

  73.     if (g_chunk_read < g_chunk_len)
  74.     {
  75.         size_t cpsize = DC_MIN(g_chunk_len - g_chunk_read, data_len);
  76.         memcpy(g_buff_pt, data_start, cpsize);
  77.         
  78.         g_buff_pt       += cpsize;
  79.         g_chunk_read    += cpsize;
  80.         data_len        -= (cpsize + 2);
  81.         data_start      += (cpsize + 2);
  82.         
  83.         if (g_chunk_read >= g_chunk_len)
  84.         {
  85.             CHUNK_INIT();
  86.             
  87.             if (data_len > 0)
  88.             {
  89.                 return dechunk(data_start, data_len);
  90.             }
  91.         }
  92.     }
  93.     else
  94.     {
  95.         CHUNK_INIT();
  96.     }
  97.    
  98. #undef CHUNK_INIT()
  99.    
  100.     return DCE_OK;
  101. }
复制代码
其他函数没什么好说的,主要就只是把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。

3. 最后看一下main.c
  1. int main (int argc, const char * argv[])
  2. {
  3.     const char              *server_ipstr   = "59.151.32.20";   // the host address of "www.mtime.com"
  4.                                                                 // you can also use function gethostbyname
  5.                                                                 // to get the address
  6.     const unsigned short    server_port     = 80;               // default port of http protocol
  7.    
  8.     int sock_http_get = -1;
  9.    
  10.     do
  11.     {
  12.         sock_http_get = socket(PF_INET, SOCK_STREAM, 0);
  13.         
  14.         if (-1 == sock_http_get)
  15.         {
  16.             printf("socket() error %d.\n", errno);
  17.             break;
  18.         }

  19.         struct sockaddr_in addr_server;
  20.         bzero(&addr_server, sizeof(addr_server));
  21.         
  22.         addr_server.sin_family          = AF_INET;
  23.         addr_server.sin_addr.s_addr     = inet_addr(server_ipstr);
  24.         addr_server.sin_port            = htons(server_port);
  25.    
  26.         if (-1 == connect(sock_http_get, (struct sockaddr *)&addr_server, sizeof(addr_server)))
  27.         {
  28.             printf("connect() error %d.\n", errno);
  29.             break;
  30.         }
  31.         
  32.         printf("connected...\n");
  33.         
  34.         char *request =
  35.         "GET / HTTP/1.1\r\n"
  36.         "Host: www.mtime.com\r\n"
  37.         "Connection: keep-alive\r\n"
  38.         "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.220 Safari/535.1\r\n"
  39.         "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
  40.         "Accept-Encoding: gzip,deflate,sdch\r\n"
  41.         "Accept-Language: zh-CN,zh;q=0.8\r\n"
  42.         "Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3\r\n"
  43.         "Cookie: DefaultCity-CookieKey=561; DefaultDistrict-CookieKey=0; _userCode_=2011725182104293; _userIdentity_=2011725182109188; _movies_=105799.87871.91873.68867; __utma=196937584.1484842614.1299113024.1314613017.1315205344.4; __utmz=196937584.1299113024.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _urm=0%7C0%7C0%7C0%7C0; _urmt=Mon%2C%2005%20Sep%202011%2006%3A49%3A04%20GMT\r\n"
  44.         "\r\n";

  45.         if (-1 == send(sock_http_get, request, strlen(request), 0))
  46.         {
  47.             printf("send() error %d.\n", errno);
  48.             break;
  49.         }
  50.         
  51. #define MY_BUF_SIZE     1024
  52.         char buff[MY_BUF_SIZE] = {0};
  53.         int recv_bytes = 0;
  54.         int chunkret = DCE_OK;
  55.         
  56.         if (DCE_OK == dechunk_init())
  57.         {
  58.             while (-1 != (recv_bytes = recv(sock_http_get, buff, MY_BUF_SIZE, 0))
  59.                    && 0 != recv_bytes)
  60.             {
  61.                 printf("%s", buff);
  62.                 if (DCE_OK != (chunkret = dechunk(buff, recv_bytes)))
  63.                 {
  64.                     printf("\nchunkret = %d\n", chunkret);
  65.                     break;
  66.                 }
  67.                
  68.                 if (NULL != memstr(buff, recv_bytes, "\r\n0\r\n"))
  69.                 {
  70.                     break;
  71.                 }
  72.                
  73.                 bzero(buff, MY_BUF_SIZE);
  74.             }
  75.             
  76.             printf("\n*********************************\n");
  77.             printf("receive finished.\n");
  78.             printf("*********************************\n");
  79.             
  80.             void *zipbuf = NULL;
  81.             size_t zipsize = 0;
  82.             dechunk_getbuff(&zipbuf, &zipsize);
  83.             
  84.             printf("\n%s\n", (char *)zipbuf);
  85.             
  86.             z_stream strm;
  87.             bzero(&strm, sizeof(strm));

  88.             printf("BEGIN:decompress...\n");
  89.             printf("*********************************\n");
  90.             
  91.             if (Z_OK == inflateInit2(&strm, 31))    // 31:decompress gzip
  92.             {
  93.                 strm.next_in    = zipbuf;
  94.                 strm.avail_in   = zipsize;
  95.                
  96.                 char zbuff[MY_BUF_SIZE] = {0};
  97.                
  98.                 do
  99.                 {
  100.                     bzero(zbuff, MY_BUF_SIZE);
  101.                     strm.next_out = (Bytef *)zbuff;
  102.                     strm.avail_out = MY_BUF_SIZE;
  103.                     
  104.                     int zlibret = inflate(&strm, Z_NO_FLUSH);
  105.                     
  106.                     if (zlibret != Z_OK && zlibret != Z_STREAM_END)
  107.                     {
  108.                         printf("\ninflate ret = %d\n", zlibret);
  109.                         break;
  110.                     }
  111.                     
  112.                     printf("%s", zbuff);
  113.                     
  114.                 } while (strm.avail_out == 0);
  115.             }
  116.             
  117.             printf("\n");
  118.             printf("*********************************\n");
  119.             
  120.             dechunk_free();
  121.         }
  122. #undef MY_BUF_SIZE
  123.         
  124.     } while (0);
  125.    
  126.     close(sock_http_get);
  127.    
  128.     return 0;
  129. }
复制代码
按照惯例,main函数是我们用来测试的函数。


这个函数中,我们首先使用socket创建了跟服务器之间的连接,紧接着我手动构造了一个请求报文,通过socket发送出去,然后循环获取数据。然后通过使用zlib库来对dechunk出来的数据进行解码以确定数据是否正确。关于zlib的使用跟本次讨论的话题不太沾边,这里就不详述,有兴趣的我们可以另行讨论。

解码前和解码后的数据都会被打印到控制台中去,日志比较庞大,这里就不给出具体信息了,大家可以自行调试观察。
关于这个文件我说明一下,网站选的是 www.mtime.com,因为它采用的就是chunked + gzip的方式,是一种相对难处理的数据。该网站的IP地址信息,相信各位有不下于100种方法去找到,所以我就没有使用gethostbyname那个方法了,因为那个方法返回的结构体使用起来实在不怎么方便。另外就是关于手动拼装的请求报文哪里来,千万不要告诉我你去用各种专业抓包工具去抓。没那么麻烦,打开你的Chrome,右键选择审查元素,然后访问你要访问的网站,OK,所有请求都会被记录在案。

/*******************************分割一下吧*****************************/

关于代码就说这么多,不过我还是声明一下,因为没多少时间,所以我只能尽力把代码写的不那么难看,注释不多,但我在这里也有所弥补,各个函数功能的实现也许有效率方面的问题抑或是各种bug,这个属于我的编程能力不足,大家可以提出宝贵的修改意见,我一定虚心接受。如果有不明白的地方( 我这只是以防万一哈^_^)也可以跟帖一起讨论。根据设计的原则,每个函数的功能应该尽量的单一和完整,调用者应该确保传递的数据符合函数的要求,但是我的main函数中却没有一些必要的判断(比如http响应是否是chunked的,是否是gzip的),因为我准备的测试数据已经确定好了的。
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值