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()进行报文数据的解压。
这里把所用的程序打包发上来,供大家参考。额,发现好像不能传文件是么,那只好传个链接了源码下载希望对大家有用吧。

你可能感兴趣的:(http,解压,chunked)