HTTP全稱為HyperText Transfer Protocol,從名字不難看出這是一種基于文本的網(wǎng)絡(luò)協(xié)議,對(duì)于初學(xué)者來(lái)說(shuō)比較友好,容易上手。各平臺(tái)上的一些第三方庫(kù)都對(duì)HTTP做了進(jìn)一步的封裝,讓HTTP變得更加親民,但往往拿來(lái)就用的技術(shù),很容易忽視其背后隱藏的細(xì)節(jié)。今天一起來(lái)扒一扒HTTP到底是如何構(gòu)成的。
初窺全貌
HTTP第一眼看上去非常簡(jiǎn)單,先來(lái)看看Request部分:

上圖主要分為三部分:request line,header和body,中間的CRLF為換行符。如果能將我們平常發(fā)送的http請(qǐng)求對(duì)應(yīng)到上述三個(gè)部分,就能形成初步的印象了。
我們以一個(gè)實(shí)際的http request例子,抓包來(lái)看一看詳細(xì)的內(nèi)部構(gòu)造。假設(shè)我們的請(qǐng)求URL為:
http://www.baidu.com/res/static/thirdparty/connect.jpg?t=1480992153433
后續(xù)的分析都是以此請(qǐng)求為基礎(chǔ)。
Request Line
Request Line的結(jié)構(gòu)為:
Request-Line = Method SP Request-URI SP HTTP-Version CRLF
Method也就是我們平常談?wù)撟疃嗟腜OST和GET所處的部分(除了POST和GET,還有其他類型的Method)。
SP是個(gè)分隔符,我用Wireshark抓包看了下,就一個(gè)字節(jié)大小,值為0x20,對(duì)應(yīng)ASCII碼中的空格。
Request-URI我們就更熟悉了,上述請(qǐng)求對(duì)應(yīng)為:/res/static/thirdparty/connect.jpg?t=1480992153.564331。這里值得注意的一點(diǎn)是:實(shí)際傳輸?shù)臅r(shí)候Request-URI有兩種可能的形式,一種是完整的absoluteURI,包含Schema和Host,另一種是abs_path,并沒(méi)有包含Schema(http)和Host(mrpeak.cn)部分,Host部分被移交到了Header當(dāng)中。所以平時(shí)我們抓包,有時(shí)看到的是完整的URI,有時(shí)則只有路徑信息。
HTTP-Version也很直觀,文本展示形式為:HTTP/1.1,代表我們當(dāng)前使用的版本。
CRLF由兩個(gè)字節(jié)組成。CR值為16進(jìn)制的0x0D,對(duì)應(yīng)ASCII中的回車鍵,LF值為0x0A,對(duì)應(yīng)ASCII中的換行鍵,CRLF合起來(lái)就是我們平常所說(shuō)的\r\n。
所以上述請(qǐng)求的Request-Line的文本展示:
GET 空格 /res/static/thirdparty/connect.jpg?t=1480992153.564331 空格 HTTP/1.1 CRLF
Header
header其本質(zhì)上是一些文本鍵值對(duì),一個(gè)典型的例子如下圖所示:

每個(gè)鍵值對(duì)的形式為:Key:空格 Value CRLF。
上面講述Request-URI的時(shí)候,缺失的Host就以鍵值對(duì)的形式存在于header中,比如,Host: pan.baidu.com。
將若干個(gè)上述格式的鍵值對(duì)組合起來(lái),就成了我們HTTP請(qǐng)求的完整header。最后一個(gè)鍵值對(duì)之后再跟一個(gè)CRLF,就表示我們的header結(jié)束了。
HTTP本身定義了一些header key,另外也允許開(kāi)發(fā)者添加自己的key,自定義的key一般以X開(kāi)頭,比如可以定義X-APP-VERSION來(lái)記錄客戶端的版本號(hào)。
Body
body里面包含請(qǐng)求的實(shí)際數(shù)據(jù)。
對(duì)于Method=GET的請(qǐng)求來(lái)說(shuō),body體是為空的,或者說(shuō)不存在body體,Header最后的兩個(gè)CRLF就標(biāo)識(shí)著請(qǐng)求的結(jié)尾。我們一般調(diào)用請(qǐng)求的業(yè)務(wù)參數(shù)是通過(guò)Request Line當(dāng)中的Request-URI來(lái)傳遞的,比如上述請(qǐng)求中的?t=1480992153.564331,也就是URI的query string部分。這部分同樣是以鍵值對(duì)的形式存在,不過(guò)是位于Request Line當(dāng)中。
對(duì)于Method=POST的請(qǐng)求來(lái)說(shuō),body體一般不為空,我們實(shí)際的業(yè)務(wù)數(shù)據(jù)都存放于body當(dāng)中,數(shù)據(jù)在body體中是以何種形式存在,其實(shí)大有門道,后面再細(xì)說(shuō)。至于Request-URI當(dāng)中的query string部分,我們依然可以選擇放置一部分?jǐn)?shù)據(jù)在其中,但更普遍的做法是使用body體。
HTTP Response
response的結(jié)構(gòu)和request結(jié)構(gòu)大致相同,可以用下圖表示:

不過(guò)是將Request Line換成了Status Line。
Status Line的結(jié)構(gòu)如下:
Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
這里關(guān)鍵在于Status-Code的記憶,記住常見(jiàn)的Status-Code值,對(duì)于我們平時(shí)分析網(wǎng)絡(luò)錯(cuò)誤十分有幫助,不需要記住每個(gè)值的含義,只需理解每個(gè)類別的含義即可:
1xx: Informational - Request received, continuing process。
2xx: Success - The action was successfully received, understood, and accepted。
3xx: Redirection - Further action must be taken in order to complete the request。
4xx: Client Error - The request contains bad syntax or cannot be fulfilled。
5xx: Server Error - The server failed to fulfill an apparently valid request。
可以用來(lái)攜帶數(shù)據(jù)的部分
分析至此,我們可以總結(jié)一個(gè)http請(qǐng)求,哪些地方是可以用來(lái)攜帶業(yè)務(wù)數(shù)據(jù)的。
Request Line當(dāng)中的Request-URI是一個(gè)選擇,也是標(biāo)準(zhǔn)的GET請(qǐng)求用來(lái)傳遞數(shù)據(jù)的位置,一般以query string的格式存在于URI當(dāng)中。一些瀏覽器或者Framework對(duì)于query string的長(zhǎng)度會(huì)有一定的限制,所以此處不適宜于傳遞較大的數(shù)據(jù)。
Header也是一個(gè)選擇,我們可以選擇協(xié)議中的一些標(biāo)準(zhǔn)header key,比如Host,User-Agent等,將我們的業(yè)務(wù)數(shù)據(jù)存放其Value中。或者我們通過(guò)自定義key,比如上面提到的X-APP-VERSION,使用X-開(kāi)頭是業(yè)界默認(rèn)的習(xí)慣,雖然RFC 6648當(dāng)中建議大家不要再使用X-作為Prefix,但這一習(xí)慣今天依舊還在持續(xù)。
Body體是我們的第三個(gè)選擇,POST請(qǐng)求可以根據(jù)Header中的Content-Type值,以不同的形式將數(shù)據(jù)保存在body體中。
一些隱藏的細(xì)節(jié)
可以看出http是一種基于文本解析的協(xié)議,上面提到的空格(0x20),換行(0x0D0A)都是HTTP用來(lái)做文本解析的輔助符號(hào)。
解析HTTP的text流程,其實(shí)也比較好理解。一個(gè)簡(jiǎn)化的流程大致是這樣:當(dāng)我們從TCP層拿到應(yīng)用層的buffer之后,以CLRF(\r\n)為分割符,將整個(gè)buffer分成若干行,第一行自然是我們的Request Line,之后每一行代表一個(gè)Header,如果連續(xù)讀到兩個(gè)CLRF,則表示header結(jié)束,如果是Method=POST,讀取Header中的Content-Length值,最后根據(jù)這個(gè)值讀取固定長(zhǎng)度的body體。這樣就完成了我們上述三個(gè)主要部分的讀取。當(dāng)然,上述是個(gè)簡(jiǎn)化的流程,實(shí)際解析場(chǎng)景會(huì)更多一些。
我們?cè)偕钊肟聪翿equest Line的解析
我們從TCP層拿到的實(shí)際上是一個(gè)字節(jié)流,要將字節(jié)流解析成我們能夠閱讀交流的形式,我們需要將字節(jié)碼進(jìn)行編碼和解碼。Request Line使用的編解碼格式是US-ASCII,也就是我們平時(shí)接觸的ASCII碼中的一種。
Request Line通過(guò)ASCII碼做還原之后,我們得到的是類似這樣的結(jié)果:
GET /res/static/thirdparty/connect.jpg?a=1&b=2 HTTP/1.1
URI的解析也自有一套規(guī)范,我們需要特別注意的是query string部分。我們平時(shí)編寫業(yè)務(wù)代碼的時(shí)候,可能會(huì)在query string當(dāng)中塞入自己的數(shù)據(jù),這些數(shù)據(jù)可能是任意形式的字節(jié)流,而Request Line和URI的解析都依賴于一些特殊字符來(lái)做分割,比如空格,/,?等等,所以為了能正確,安全的解析整個(gè)Request Line和URI,我們需要對(duì)query string中的字節(jié)流做進(jìn)一步的編碼約束,只允許其中出現(xiàn)安全的ASCII碼,這也是我們?yōu)槭裁葱枰猆rlEncode的原因。
UrlEncode的過(guò)程也比較簡(jiǎn)單,它將字節(jié)流中的所有字節(jié),對(duì)照ASCII碼表分為,安全的ASCII碼和不安全的ASCII碼。安全的ASCII碼不用做任何處理,不安全的ASCII碼(比如空格0x20)則做進(jìn)一步的編碼處理,編碼的思路也簡(jiǎn)單:用安全的ASCII碼來(lái)代替不安全的ASCII碼。比如空格(0x20)被編碼成%20,由一個(gè)ASCII碼(空格)變成了三個(gè)ASCII碼(%,2,0)。對(duì)于原本就不是ASCII碼的內(nèi)容來(lái)說(shuō),比如中文,則先以UTF-8編碼成字節(jié)流,再對(duì)照ASCII碼做編碼。比如中文字「高」,其UTF-8的表現(xiàn)形式為:\xE9\xAB\x98,再進(jìn)一步做ASCII編碼,最后UrlEncode的結(jié)果就為:%E9%AB%98。
由此可見(jiàn),UrlEncode是出于URL安全解析的需要,Encode的結(jié)果是由%和一部分安全的ASCII碼所組成。UrlEncode的缺點(diǎn)也比較明顯,Encode非ASCII碼的時(shí)候(比如中文),一個(gè)字節(jié)會(huì)被encode成3個(gè)字節(jié),長(zhǎng)度整整是原先的3倍,造成流量的浪費(fèi)。
我見(jiàn)過(guò)有人使用base64來(lái)對(duì)query string做encode,這是把概念搞混淆了,至少base64 encode之后的=就不是一個(gè)URL安全的字符,=在UrlEncode之后對(duì)應(yīng)%3d。
Header的解析
對(duì)于Header的解析可以先按CRLF分割成一個(gè)個(gè)的鍵值對(duì),鍵值對(duì)里面的值,也就是我們所說(shuō)的field content其實(shí)也有編碼要求。RFC 7230中有闡述:
Historically, HTTP has allowed field content with text in the
ISO-8859-1 charset [ISO-8859-1], supporting other charsets only
through use of [RFC2047] encoding. In practice, most HTTP header
field values use only a subset of the US-ASCII charset [USASCII].
Newly defined header fields SHOULD limit their field values to
US-ASCII octets. A recipient SHOULD treat other octets in field
content (obs-text) as opaque data.
簡(jiǎn)單來(lái)說(shuō),我們?cè)趯?shí)際使用當(dāng)中使用ASCII碼來(lái)限制field content。我們常用幾個(gè)Field,諸如Host,User-Agent等,使用ASCII碼字符也已綽綽有余,一般不會(huì)對(duì)值做進(jìn)一步的encode處理。
Body的解析
body的解析是我們平時(shí)打交道最多的部分,不是說(shuō)我們需要知道如何去解析body,而是要了解body體里的數(shù)據(jù)格式。
body的解析本身比較簡(jiǎn)單,從header中知道Content-Length之后,讀取固定長(zhǎng)度的字節(jié)流即完成了body的獲取,關(guān)鍵的環(huán)節(jié)是獲取之后,如何讀取其中的數(shù)據(jù)并遞交給應(yīng)用層,所以HTTP協(xié)議本身并沒(méi)有對(duì)Body中的內(nèi)容編碼做約束,而是把它交給協(xié)議的使用者去決定,我們甚至可以在body體里存放二進(jìn)制流,對(duì)應(yīng)的Content-Type為application/octet-stream。
我們來(lái)看看平時(shí)發(fā)送HTTP請(qǐng)求時(shí),以AFNetworking為例,使用最頻繁的幾種Content-Type:
- multipart/form-data
- application/x-www-form-urlencoded
- application/json
當(dāng)我們向Server發(fā)送數(shù)據(jù)的時(shí)候,需要和Server約定好所使用的Content-Type,客戶端在發(fā)送Request的時(shí)候也要注意API的差別,以AFNetworking為例,發(fā)送json則使用:
AFJSONRequestSerializer* jsonSerializer = [AFJSONRequestSerializer serializer];
request = [jsonSerializer requestWithMethod:@"POST" URLString:requestUrl parameters:requestParams error:nil];
發(fā)送multipart/form-data:
request = [self.requestSerializer multipartFormRequestWithMethod:@"POST" URLString:requestUrl parameters:requestParams constructingBodyWithBlock:nil error:nil];
發(fā)送x-www-form-urlencoded:
request = [self.requestSerializer requestWithMethod:@"POST" URLString:requestUrl parameters:requestParams error:nil];
json不用多說(shuō),大家都非常熟悉的數(shù)據(jù)交換格式。multipart/form-data和x-www-form-urlencoded比較容易引起混淆。
在AFNetworking中有這樣一段代碼:
//AFURLRequestSerialization
if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
[mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
}
可見(jiàn)當(dāng)我們的Request沒(méi)有設(shè)置Content-Type的時(shí)候,默認(rèn)使用的就是application/x-www-form-urlencoded。這里的urlencoded和前面Request-URI中的urlencode是一回事,只不過(guò)encode的是body體當(dāng)中的內(nèi)容。
那我們什么時(shí)候用application/x-www-form-urlencoded,什么時(shí)候用multipart/form-data呢?
先來(lái)看下使用Content-Type為multipart/form-data時(shí),我們的Request有什么變化,下圖是使用mitmproxy抓包一個(gè)文件上傳Request的headers示意圖:

Content-Type的完整值為:multipart/form-data; boundary=Boundary+2BBBEA582E48968C。
multipart把body體分成多個(gè)塊,多個(gè)塊之間依賴于boundary值去做分割,所以生成的boundary要足夠長(zhǎng),長(zhǎng)到在字節(jié)流當(dāng)中出現(xiàn)重復(fù)的概率幾乎為0,否則就會(huì)導(dǎo)致錯(cuò)誤的傳輸,AFNetworking中生成Boundary的方法如下:
static NSString * AFCreateMultipartFormBoundary() {
return [NSString stringWithFormat:@"Boundary+%08X%08X", arc4random(), arc4random()];
}
我們可以看下一個(gè)例子,如果使用multipart/form-data,body中具體的數(shù)據(jù)格式:
Boundary+2BBBEA582E48968C
Content-Disposition: form-data; name="text1"
text
Boundary+2BBBEA582E48968C
Content-Disposition: form-data; name="text2"
another text
可以看到在body中多出了Boundary+2BBBEA582E48968C和Content-Disposition,這些會(huì)增加body的傳輸大小。
假設(shè)我們有一個(gè)大文件需要上傳,如果使用application/x-www-form-urlencoded作為Content-Type,由于字節(jié)流當(dāng)中存在非常多的非ASCII碼,文件的長(zhǎng)度會(huì)變至原本的2-3倍,所以此時(shí)multipart/form-data更合適。
假設(shè)我們只有少量的鍵值對(duì)需要上傳,如果使用multipart/form-data作為Content-Type,由于boundary和Content-Disposition帶來(lái)的額外流量,又顯得得不償失,所以此時(shí)使用application/x-www-form-urlencoded更為合適。
這也是為什么我們使用multipart/form-data作為文件類Request的Content-Type,而對(duì)于普通業(yè)務(wù)數(shù)據(jù),則使用application/x-www-form-urlencoded或者application/json。
總結(jié)
上述的分析,更多的是站在客戶端的角度去看的,實(shí)際HTTP協(xié)議的構(gòu)成細(xì)節(jié)非常之多,需要曠日持久的深入學(xué)習(xí)和積累。功夫越深,坑越少 ;)