HTTP协议中你必须知道的三种数据格式
实习中的一个主要工作就是分析HTTP中的协议,自己也用Python写过正则表达式对HTTP请求和响应的内容进行匹配,然后把关键字段抽离出来放到一个字典中以备使用(可以稍微改造一下就是一个爬虫工具)。HTTP协议中的很多坑,自己都遇到过,我就针对自己遇到的几种HTTP常见的数据格式,来做一个总结。
作者:songjz来源:segmentfault|2016-11-1013:00收藏分享
实习中的一个主要工作就是分析HTTP中的协议,自己也用Python写过正则表达式对HTTP请求和响应的内容进行匹配,然后把关键字段抽离出来放到一个字典中以备使用(可以稍微改造一下就是一个爬虫工具)。
HTTP协议中的很多坑,自己都遇到过,我就针对自己遇到的几种HTTP常见的数据格式,来做一个总结。
Zlib压缩数据
对于Zlib,一点也不陌生,我们平时用它来压缩文件,常见类型有zip、rar和7z等。Zlib是一种流行的文件压缩算法,应用十分广泛,尤其是在Linux平台。当应用Zlib压缩到一个纯文本文件时,效果是非常明显的,大约可以减少70%以上的文件大小,这取决于文件中的内容。
Zlib也适用于Web数据传输,比如利用Apache中的Gzip(后面会提到,一种压缩算法)模块,我们可以使用Gzip压缩算法来对Apache服务器发布的网页内容进行压缩后再传输到客户端浏览器。这样经过压缩后实际上降低了网络传输的字节数,最明显的好处就是可以加快网页加载的速度。
网页加载速度加快的好处不言而喻,节省流量,改善用户的浏览体验。而这些好处并不仅仅限于静态内容,PHP动态页面和其他动态生成的内容均可以通过使用Apache压缩模块压缩,加上其他的性能调整机制和相应的服务器端缓存规则,这可以大大提高网站的性能。因此,对于部署在Linux服务器上的PHP程序,在服务器支持的情况下,建议你开启使用GzipWeb压缩。
Gzip压缩两种类型
压缩算法不同,可以产生不同的压缩数据(目的都是为了减小文件大小)。目前Web端流行的压缩格式有两种,分别是Gzip和Defalte。
Apache中的就是Gzip模块,Deflate是同时使用了LZ77算法与哈夫曼编码(HuffmanCoding)的一个无损数据压缩算法。Deflate压缩与解压的源代码可以在自由、通用的压缩库zlib上找到。
更高压缩率的Deflate是7-zip所实现的。AdvanceCOMP也使用这种实现,它可以对gzip、PNG、MNG以及ZIP文件进行压缩从而得到比zlib更小的文件大小。在KenSilverman的KZIP与PNGOUT中使用了一种更加高效同时要求更多用户输入的Deflate程序。
deflate使用inflateInit(),而gzip使用inflateInit2()进行初始化,比inflateInit()多一个参数:-MAX_WBITS,表示处理rawdeflate数据。因为gzip数据中的zlib压缩数据块没有zlibheader的两个字节。使用inflateInit2时要求zlib库忽略zlibheader。在zlib手册中要求windowBits为8..15,但是实际上其它范围的数据有特殊作用,如负数表示rawdeflate。
其实说这么多,总结一句话,Deflate是一种压缩算法,是huffman编码的一种加强。deflate与gzip解压的代码几乎相同,可以合成一块代码。
更多知识请见维基百科zlib。
Web服务器处理数据压缩的过程
Web服务器接收到浏览器的HTTP请求后,检查浏览器是否支持HTTP压缩(Accept-Encoding信息);
如果浏览器支持HTTP压缩,Web服务器检查请求文件的后缀名;
如果请求文件是HTML、CSS等静态文件,Web服务器到压缩缓冲目录中检查是否已经存在请求文件的最新压缩文件;
如果请求文件的压缩文件不存在,Web服务器向浏览器返回未压缩的请求文件,并在压缩缓冲目录中存放请求文件的压缩文件;
如果请求文件的最新压缩文件已经存在,则直接返回请求文件的压缩文件;
如果请求文件是动态文件,Web服务器动态压缩内容并返回浏览器,压缩内容不存放到压缩缓存目录中。
举个栗子
说了这么多,下面举一个例子,打开抓包软件,访问我们学校的官网(www.ecnu.edu.cn),请求头如下:
GET/_css/tpl2/system.cssHTTP/1.1
Host:www.ecnu.edu.cn
Connection:keep-alive
User-Agent:Mozilla/5.0(WindowsNT10.0;WOW64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/54.0.2840.59Safari/537.36
Accept:text/css,/;q=0.1
Referer:http://www.ecnu.edu.cn/
Accept-Encoding:gzip,deflate
Accept-Language:zh-CN,zh;q=0.8
Cookie:a10-default-cookie-persist-20480-sg_bluecoat_a=AFFIHIMKFAAA
在第七行,Accept-Encoding显示的是gzip,deflate,这句话的意思是,浏览器告诉服务器支持gzip和deflate两种数据格式,服务器收到这种请求之后,会进行gzip或deflate压缩(一般都是返回gzip格式的数据)。
Python的urllib2就可以设置这个参数:
request=urllib2.Request(url)
request.add_header(''Accept-encoding'',''gzip'')
//或者设置成deflate
request.add_header(''Accept-encoding'',''deflate'')
//或者两者都设置
request.add_header(''Accept-encoding'',''gzip,deflate'')
服务器给的响应一般如下:
HTTP/1.1200OK
Date:Sat,22Oct201611:41:19GMT
Content-Type:text/javascript;charset=utf-8
Transfer-Encoding:chunked
Connection:close
Vary:Accept-Encoding
tracecode:24798560510951725578102219
Server:Apache
Content-Encoding:gzip
400a
............ks#I....W...,....>..T..]..Z...Y..].MK..2..L..(略)
//响应体为压缩数据
从响应头来看,Content-Encoding:gzip这段话说明响应体的压缩方式是gzip压缩,一般有几种情况,字段为空表示明文无压缩,还有Content-Encoding:gzip和Content-Encoding:deflate两种。
实际上Gzip网站要远比Deflate多,之前写过一个简单爬虫从hao123的主页开始爬,爬几千个网页(基本涵盖所有常用的),专门分析响应体的压缩类型,得到的结果是:
Accept-Encoding不设置参数:会返回一个无压缩的响应体(浏览器比较特别,他们会自动设置Accept-Encoding:gzip:deflate来提高传输速度);
Accept-Encoding:gzip,100%的网站都会返回gzip压缩,但不保证互联网所有网站都支持gzip(万一没开启);
Accept-Encoding:deflate:只有不到10%的网站返回一个deflate压缩的响应,其他的则返回一个没有压缩的响应体。
Accept-Encoding:gzip,deflate:返回的结果也都是gzip格式的数据,说明在优先级上gzip更受欢迎。
响应头的Encoding字段很有帮助,比如我们写个正则表达式匹配响应头是什么压缩:
(?<=Content-Encoding:).+(?=\r\n)
匹配到内容为空说明没有压缩,为gzip说明响应体要经过gzip解压,为deflate说明为deflate压缩。
Python中的zlib库
在python中有zlib库,它可以解决gzip、deflate和zlib压缩。这三种对应的压缩方式分别是:
RFC1950(zlibcompressedformat)
RFC1951(deflatecompressedformat)
RFC1952(gzipcompressedformat)
虽说是Python库,但是底层还是C(C++)来实现的,这个http-parser也是C实现的源码,Nodejs的http-parser也是C实现的源码,zlib的C源码在这里。C真的好牛逼呀!
在解压缩的过程中,需要选择windowBits参数:
to(de-)compressdeflateformat,usewbits=-zlib.MAX_WBITS
to(de-)compresszlibformat,usewbits=zlib.MAX_WBITS
to(de-)compressgzipformat,usewbits=zli
例如,解压gzip数据,就可以使用zlib.decompress(data,zlib.MAX_WBITS|16),解压deflate数据可以使用zlib.decompress(data,-zlib.MAX_WBITS)。
当然,对于gzip文件,也可以使用python的gzip包来解决,可以参考下面的代码:
>>>importgzip
>>>importStringIO
>>>fio=StringIO.StringIO(gzip_data)
>>>f=gzip.GzipFile(fileobj=fio)
>>>f.read()
''test''
>>>f.close()
也可以在解压的时候自动加入头检测,把32加入头中就可以触发头检测,例如:
>>>zlib.decompress(gzip_data,zlib.MAX_WBITS|32)
''test''
>>>zlib.decompress(zlib_data,zlib.MAX_WBITS|32)
''test''
以上参考stackoverflowHowcanIdecompressagzipstreamwithzlib?。
刚接触这些东西的时候,每天都会稀奇古怪的报一些错误,基本上Google一下都能解决。
分块传输编码chunked
分块传输编码(Chunkedtransferencoding)是超文本传输协议(HTTP)中的一种数据传输机制,允许HTTP由网页服务器发送给客户端应用(通常是网页浏览器)的数据可以分成多个部分。分块传输编码只在HTTP协议1.1版本(HTTP/1.1)中提供。
通常,HTTP应答消息中发送的数据是整个发送的,Content-Length消息头字段表示数据的长度。数据的长度很重要,因为客户端需要知道哪里是应答消息的结束,以及后续应答消息的开始。然而,使用分块传输编码,数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小。通常数据块的大小是一致的,但也不总是这种情况。
分块传输的优点
HTTP1.1引入分块传输编码提供了以下几点好处:
HTTP分块传输编码允许服务器为动态生成的内容维持HTTP持久链接。通常,持久链接需要服务器在开始发送消息体前发送Content-Length消息头字段,但是对于动态生成的内容来说,在内容创建完之前是不可知的。
分块传输编码允许服务器在最后发送消息头字段。对于那些头字段值在内容被生成之前无法知道的情形非常重要,例如消息的内容要使用散列进行签名,散列的结果通过HTTP消息头字段进行传输。没有分块传输编码时,服务器必须缓冲内容直到完成后计算头字段的值并在发送内容前发送这些头字段的值。
HTTP服务器有时使用压缩(gzip或deflate)以缩短传输花费的时间。分块传输编码可以用来分隔压缩对象的多个部分。在这种情况下,块不是分别压缩的,而是整个负载进行压缩,压缩的输出使用本文描述的方案进行分块传输。在压缩的情形中,分块编码有利于一边进行压缩一边发送数据,而不是先完成压缩过程以得知压缩后数据的大小。
注:以上内容来自于维基百科。
分块传输的格式
如果一个HTTP消息(请求消息或应答消息)的Transfer-Encoding消息头的值为chunked,那么,消息体由数量未定的块组成,并以最后一个大小为0的块为结束。每一个非空的块都以该块包含数据的字节数(字节数以十六进制表示)开始,跟随一个CRLF(回车及换行),然后是数据本身,最后块CRLF结束。在一些实现中,块大小和CRLF之间填充有白空格(0x20)。
最后一块是单行,由块大小(0),一些可选的填充白空格,以及CRLF。最后一块不再包含任何数据,但是可以发送可选的尾部,包括消息头字段。
消息最后以CRLF结尾。例如下面就是一个chunked格式的响应体。
HTTP/1.1200OK
Date:Wed,06Jul201606:59:55GMT
Server:Apache
Accept-Ranges:bytes
Transfer-Encoding:chunked
Content-Type:text/html
Content-Encoding:gzip
Age:35
X-Via:1.1daodianxinxiazai58:88(CdnCacheServerV2.0),1.1yzdx147:1(Cdn
CacheServerV2.0)
Connection:keep-alive
a
....k.|W..
166
..OO.0...&~..;........]..(F=V.A3.X..~z...-.l8......y....).?....,....j..h.6
....s.~.>..mZ.8/..,.)B.G.`"Dq.P].f=0..Q..d.....h......8....F..y......q.....4
{F..M.A...a.rAra.....n>.D
..o@.`^.....!@$...p...%a\D..K...d{2...UnF,C[....T.....c....V...."%.`U......?
D....#..K..<.....D.e....IFK0.<...)]K.V/eK.Qz...^....t...S6...m...^..CK.XRU?m..
.........Z..#Uik......
0
Transfer-Encoding:chunked字段可以看出响应体是否为chunked压缩,chunked数据很有意思,采用的格式是长度\r\n内容\r\n长度\r\n..0\r\n,而且长度还是十六进制的,最后以0\r\n结尾(不保证都有)。因为上面的数据是gzip压缩,看起来不够直观,下面举个简单的例子:
5\r\n
ababa\r\n
f\r\n
123451234512345\r\n
14\r\n
12345123451234512345\r\n
0\r\n
上述例子chunked解码后的数据ababa12345...,另外\r\n是不可见的,我手动加的。
和gzip一样,一样可以写一个正则表达式来匹配:
(?<=Transfer-Encoding:).+(?=\r\n)
处理chunked数据
从前面的介绍可以知道,response-body部分其实由length(1)\r\ndata(1)\r\nlength(2)\r\ndata(2)……循环组成,通过下面的函数进行处理,再根据压缩类型解压出最终的数据。
Python处理的过程如下:
unchunked=b''''
pos=0
whilepos<=len(data):
chunkNumLen=data.find(b''\r\n'',pos)-pos
//从第一个元素开始,发现第一个\r\n,计算length长度
chunkLen=int(data[pos:pos+chunkNumLen],16)
//把length的长度转换成int
ifchunkLen==0:
break
//如果长度为0,则说明到结尾
chunk=data[pos+chunkNumLen+len(''\r\n''):pos+chunkNumLen+len(''\r\n'')+chunkLen]
unchunked+=chunk
//将压缩数据拼接
pos+=chunkNumLen+len(''\r\n'')+chunkLen+len(''\r\n'')
//同时pos位置向后移动
returnunchunked
//此时处理后unchunked就是普通的压缩数据,可以用zlib解压函数进行解压
实际中,我们会同时遇到既时chunked又是压缩数据的响应,这个时候处理的思路应该是:先处理chunked,在处理压缩数据,顺序不能反。
MultiPart数据
MultiPart的本质就是Post请求,MultiPart出现在请求中,用来对一些文件(图片或文档)进行处理,在请求头中出现Content-Type:multipart/form-data;boundary=::287032381131322则表示为MultiPart格式数据包,下面这个是multipart数据包格式:
POST/cgi-bin/qtestHTTP/1.1
Host:aram
User-Agent:Mozilla/5.0Gecko/2009042316Firefox/3.0.10
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
Accept-Langwww.baiyuewang.netuage:en-us,en;q=0.5
Accept-Encoding:gzip,deflate
Accept-Charset:ISO-8859-1,utf-8;q=0.7,;q=0.7
Keep-Alive:300
Connection:keep-alive
Referer:http://aram/~martind/banner.htm
Content-Type:multipart/form-data;boundary=::287032381131322
Content-Length:514
--::287032381131322
Content-Disposition:form-data;name="datafile1";filename="r.gif"
Content-Type:image/gif
GIF87a.............,...........D..;
--::287032381131322
Content-Disposition:form-data;name="datafile2";filename="g.gif"
Content-Type:image/gif
GIF87a.............,...........D..;
--::287032381131322
Content-Disposition:form-data;name="datafile3";filename="b.gif"
Content-Type:image/gif
GIF87a.............,...........D..;
--::287032381131322—
http协议本身的原始方法不支持multipart/form-data请求,那这个请求自然就是由这些原始的方法演变而来的,具体如何演变且看下文:
multipart/form-data的基础方法是post,也就是说是由post方法来组合实现的
multipart/form-data与post方法的不同之处:请求头,请求体。
multipart/form-data的请求头必须包含一个特殊的头信息:Content-Type,且其值也必须规定为multipart/form-data,同时还需要规定一个内容分割符用于分割请求体中的多个post内容,如文件内容和文本内容自然需要分割,不然接收方就无法正常解析和还原这个文件。具体的头信息如:Content-Type:multipart/form-data;boundary=${bound},${bound}代表分割符,可以任意规定,但为了避免和正常文本重复,尽量使用复杂一点的内容,如::287032381131322
multipart/form-data的请求体也是一个字符串,不过和post的请求体不同的是它的构造方式,post是简单的name=value值连接,而multipart/form-data则是添加了分隔符等内容的构造体。
维基百科上关于multipart的介绍。
multipart的数据格式有一定的特点,首先是头部规定了一个${bound},上面那个例子中的${bound}为::287032381131322,由多个内容相同的块组成,每个块的格式以--加${bound}开始的,然后是该部分内容的描述信息,然后一个\r\n,然后是描述信息的具体内容。如果传送的内容是一个文件的话,那么还会包含文件名信息,以及文件内容的类型。
小结,要发送一个multipart/form-data的请求,需要定义一个自己的${bound},按照格式来发请求就好,对于multipart的数据格式并没有过多介绍,感觉和chunked很类似,不难理解。
总结
本文介绍的三种数据格式,都比较基础,一些框架自动把它们处理,比如爬虫。还有图像上传,对于multipart/data格式的请求头,了解一些概念性的东西也非常有意思。共勉。
|
|