HTTP 協議中你必須知道的三種資料格式
實習中的一個主要工作就是分析 HTTP 中的協議,自己也用 Python 寫過正則表示式對 HTTP 請求和響應的內容進行匹配,然後把關鍵欄位抽離出來放到一個字典中以備使用(可以稍微改造一下就是一個爬蟲工具)。
HTTP 協議中的很多坑,自己都遇到過,我就針對自己遇到的幾種 HTTP 常見的資料格式,來做一個總結。
Zlib 壓縮資料
對於 Zlib,一點也不陌生,我們平時用它來壓縮檔案,常見型別有 zip、rar 和 7z 等。Zlib 是一種流行的檔案壓縮演算法,應用十分廣泛,尤其是在 Linux 平臺。當應用 Zlib 壓縮到一個純文字檔案時,效果是非常明顯的,大約可以減少70%以上的檔案大小,這取決於檔案中的內容。
Zlib 也適用於 Web 資料傳輸,比如利用 Apache 中的 Gzip (後面會提到,一種壓縮演算法) 模組,我們可以使用 Gzip 壓縮演算法來對 Apache 伺服器釋出的網頁內容進行壓縮後再傳輸到客戶端瀏覽器。這樣經過壓縮後實際上降低了網路傳輸的位元組數,最明顯的好處就是可以加快網頁載入的速度。
網頁載入速度加快的好處不言而喻,節省流量,改善使用者的瀏覽體驗。而這些好處並不僅僅限於靜態內容,PHP 動態頁面和其他動態生成的內容均可以通過使用 Apache 壓縮模組壓縮,加上其他的效能調整機制和相應的伺服器端 快取規則,這可以大大提高網站的效能。因此,對於部署在 Linux 伺服器上的 PHP 程式,在伺服器支援的情況下,建議你開啟使用 Gzip Web 壓縮。
Gzip 壓縮兩種型別
壓縮演算法不同,可以產生不同的壓縮資料(目的都是為了減小檔案大小)。目前 Web 端流行的壓縮格式有兩種,分別是 Gzip 和 Defalte。
Apache 中的就是 Gzip 模組,Deflate 是同時使用了 LZ77 演算法與哈夫曼編碼(Huffman Coding)的一個無損資料壓縮演算法。Deflate 壓縮與解壓的原始碼可以在自由、通用的壓縮庫 zlib 上找到。
更高壓縮率的 Deflate 是 7-zip 所實現的。AdvanceCOMP 也使用這種實現,它可以對 gzip、PNG、MNG 以及 ZIP 檔案進行壓縮從而得到比 zlib 更小的檔案大小。在 Ken Silverman的 KZIP 與 PNGOUT 中使用了一種更加高效同時要求更多使用者輸入的 Deflate 程式。
deflate 使用
inflateInit()
,而 gzip 使用 inflateInit2() 進行初始化,比
inflateInit()
多一個引數: -MAX_WBITS,表示處理 raw deflate 資料。因為 gzip 資料中的 zlib 壓縮資料塊沒有 zlib header 的兩個位元組。使用 inflateInit2 時要求 zlib 庫忽略 zlib header。在 zlib 手冊中要求 windowBits 為 8..15,但是實際上其它範圍的資料有特殊作用,如負數表示 raw deflate。
其實說這麼多,總結一句話,Deflate 是一種壓縮演算法,是 huffman 編碼的一種加強。 deflate 與 gzip 解壓的程式碼幾乎相同,可以合成一塊程式碼。
Web 伺服器處理資料壓縮的過程
-
Web伺服器接收到瀏覽器的HTTP請求後,檢查瀏覽器是否支援HTTP壓縮(Accept-Encoding 資訊);
-
如果瀏覽器支援HTTP壓縮,Web伺服器檢查請求檔案的字尾名;
-
如果請求檔案是HTML、CSS等靜態檔案,Web伺服器到壓縮緩衝目錄中檢查是否已經存在請求檔案的最新壓縮檔案;
-
如果請求檔案的壓縮檔案不存在,Web伺服器向瀏覽器返回未壓縮的請求檔案,並在壓縮緩衝目錄中存放請求檔案的壓縮檔案;
-
如果請求檔案的最新壓縮檔案已經存在,則直接返回請求檔案的壓縮檔案;
-
如果請求檔案是動態檔案,Web伺服器動態壓縮內容並返回瀏覽器,壓縮內容不存放到壓縮快取目錄中。
舉個栗子
說了這麼多,下面舉一個例子,開啟抓包軟體,訪問我們學校的官網( www.ecnu.edu.cn ),請求頭如下:
GET /_css/tpl2/system.css HTTP/1.1
Host: www.ecnu.edu.cn
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/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.1 200 OK
Date: Sat, 22 Oct 2016 11:41:19 GMT
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壓縮。這三種對應的壓縮方式分別是:
RFC 1950 (zlib compressed format)
RFC 1951 (deflate compressed format)
RFC 1952 (gzip compressed format)
雖說是 Python 庫,但是底層還是 C(C++) 來實現的,這個
http-parser
也是 C 實現的原始碼,Nodejs 的
http-parser
也是 C 實現的原始碼,zlib
的 C
原始碼在這裡。C 真的好牛逼呀!
在解壓縮的過程中,需要選擇 windowBits 引數:
to (de-)compress deflate format, use wbits = -zlib.MAX_WBITS
to (de-)compress zlib format, use wbits = zlib.MAX_WBITS
to (de-)compress gzip format, use wbits = zli
例如,解壓gzip資料,就可以使用zlib.decompress(data, zlib.MAX_WBITS | 16),解壓deflate資料可以使用zlib.decompress(data,- zlib.MAX_WBITS)。
當然,對於gzip檔案,也可以使用python的gzip包來解決,可以參考下面的程式碼:
>>> import gzip
>>> import StringIO
>>> 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'
剛接觸這些東西的時候,每天都會稀奇古怪的報一些錯誤,基本上 Google 一下都能解決。
分塊傳輸編碼 chunked
分塊傳輸編碼(Chunked transfer encoding)是超文字傳輸協議(HTTP)中的一種資料傳輸機制,允許 HTTP 由網頁伺服器傳送給客戶端應用( 通常是網頁瀏覽器)的資料可以分成多個部分。分塊傳輸編碼只在 HTTP 協議 1.1 版本(HTTP/1.1)中提供。
通常,HTTP 應答訊息中傳送的資料是整個傳送的,Content-Length 訊息頭欄位表示資料的長度。資料的長度很重要,因為客戶端需要知道哪裡是應答訊息的結束,以及後續應答訊息的開始。然而,使用分塊傳輸編碼,資料分解成一系列資料塊,並以一個或多個塊傳送,這樣伺服器可以傳送資料而不需要預先知道傳送內容的總大小。通常資料塊的大小是一致的,但也不總是這種情況。
分塊傳輸的優點
HTTP 1.1引入分塊傳輸編碼提供了以下幾點好處:
-
HTTP 分塊傳輸編碼允許伺服器為動態生成的內容維持 HTTP 持久連結。通常,持久連結需要伺服器在開始傳送訊息體前傳送 Content-Length 訊息頭欄位,但是對於動態生成的內容來說,在內容建立完之前是不可知的。
-
分塊傳輸編碼允許伺服器在最後傳送訊息頭欄位。對於那些頭欄位值在內容被生成之前無法知道的情形非常重要,例如訊息的內容要使用雜湊進行簽名,雜湊的結果通過 HTTP 訊息頭欄位進行傳輸。沒有分塊傳輸編碼時,伺服器必須緩衝內容直到完成後計算頭欄位的值並在傳送內容前傳送這些頭欄位的值。
-
HTTP 伺服器有時使用壓縮 (gzip 或 deflate)以縮短傳輸花費的時間。分塊傳輸編碼可以用來分隔壓縮物件的多個部分。在這種情況下,塊不是分別壓縮的,而是整個負載進行壓縮,壓縮的輸出使用本文描述的方案進行分塊傳輸。在壓縮的情形中,分塊編碼有利於一邊進行壓縮一邊傳送資料,而不是先完成壓縮過程以得知壓縮後資料的大小。
注:以上內容來自於維基百科。
分塊傳輸的格式
如果一個 HTTP 訊息(請求訊息或應答訊息)的 Transfer-Encoding 訊息頭的值為 chunked,那麼,訊息體由數量未定的塊組成,並以最後一個大小為 0 的塊為結束。
每一個非空的塊都以該塊包含資料的位元組數(位元組數以十六進位制表示)開始,跟隨一個 CRLF(回車及換行),然後是資料本身,最後塊 CRLF 結束。在一些實現中,塊大小和 CRLF 之間填充有白空格(0x20)。
最後一塊是單行,由塊大小(0),一些可選的填充白空格,以及 CRLF。最後一塊不再包含任何資料,但是可以傳送可選的尾部,包括訊息頭欄位。
訊息最後以 CRLF 結尾。例如下面就是一個 chunked 格式的響應體。
HTTP/1.1 200 OK
Date: Wed, 06 Jul 2016 06:59:55 GMT
Server: Apache
Accept-Ranges: bytes
Transfer-Encoding: chunked
Content-Type: text/html
Content-Encoding: gzip
Age: 35
X-Via: 1.1 daodianxinxiazai58:88 (Cdn Cache Server V2.0), 1.1 yzdx147:1 (Cdn
Cache Server V2.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
[email protected]`^[email protected] $...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\n data(1) \r\n length(2) \r\n data(2)…… 迴圈組成,通過下面的函式進行處理,再根據壓縮型別解壓出最終的資料。
Python 處理的過程如下:
unchunked = b''
pos = 0
while pos <= len(data):
chunkNumLen = data.find(b'\r\n', pos)-pos
//從第一個元素開始,發現第一個\r\n,計算length長度
chunkLen=int(data[pos:pos+chunkNumLen], 16)
//把length的長度轉換成int
if chunkLen == 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位置向後移動
return unchunked
//此時處理後unchunked就是普通的壓縮資料,可以用zlib解壓函式進行解壓
實際中,我們會同時遇到既時 chunked 又是壓縮資料的響應,這個時候處理的思路應該是:先處理 chunked,在處理壓縮資料,順序不能反。
MultiPart 資料
MultiPart 的本質就是 Post 請求,MultiPart出現在請求中,用來對一些檔案(圖片或文件)進行處理,在請求頭中出現
Content-Type: multipart/form-data; boundary=::287032381131322
則表示為 MultiPart 格式資料包,下面這個是 multipart 資料包格式:
POST /cgi-bin/qtest HTTP/1.1
Host: aram
User-Agent: Mozilla/5.0 Gecko/2009042316 Firefox/3.0.10
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: 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} 代表分割符,可以任意規定,但為了避免和正常文字重複,儘量使用複雜一點的內容,如::287032381131322
-
multipart/form-data 的請求體也是一個字串,不過和 post 的請求體不同的是它的構造方式,post 是簡單的 name=value 值連線,而 multipart/form-data 則是添加了分隔符等內容的構造體。
multipart 的資料格式有一定的特點,首先是頭部規定了一個 ${bound},上面那個例子中的 ${bound} 為
::287032381131322
,由多個內容相同的塊組成,每個塊的格式以--加 ${bound} 開始的,然後是該部分內容的描述資訊,然後一個\r\n,然後是描述資訊的具體內容。如果傳送的內容是一個檔案的話,那麼還會包含檔名資訊,以及檔案內容的型別。
小結,要傳送一個 multipart/form-data 的請求,需要定義一個自己的 ${bound} ,按照格式來發請求就好,對於 multipart 的資料格式並沒有過多介紹,感覺和 chunked 很類似,不難理解。
總結
本文介紹的三種資料格式,都比較基礎,一些框架自動把它們處理,比如爬蟲。還有影象上傳,對於 multipart/data 格式的請求頭,瞭解一些概念性的東西也非常有意思。共勉。