扒一扒HTTP的構成

HTTP全稱為HyperText Transfer Protocol改化,從名字不難看出這是一種基于文本的網(wǎng)絡協(xié)議,對于初學者來說比較友好宇挫,容易上手稼跳。各平臺上的一些第三方庫都對HTTP做了進一步的封裝,讓HTTP變得更加親民,但往往拿來就用的技術跃巡,很容易忽視其背后隱藏的細節(jié)危号。今天一起來扒一扒HTTP到底是如何構成的。

初窺全貌

HTTP第一眼看上去非常簡單素邪,先來看看Request部分:

上圖主要分為三部分:request line外莲,header和body,中間的CRLF為換行符兔朦。如果能將我們平常發(fā)送的http請求對應到上述三個部分偷线,就能形成初步的印象了。

我們以一個實際的http request例子烘绽,抓包來看一看詳細的內(nèi)部構造淋昭。假設我們的請求URL為:

http://www.baidu.com/res/static/thirdparty/connect.jpg?t=1480992153433

后續(xù)的分析都是以此請求為基礎。

Request Line

Request Line的結(jié)構為:

Request-Line   = Method SP Request-URI SP HTTP-Version CRLF

Method也就是我們平常談論最多的POST和GET所處的部分(除了POST和GET安接,還有其他類型的Method)。

SP是個分隔符英融,我用Wireshark抓包看了下盏檐,就一個字節(jié)大小,值為0x20驶悟,對應ASCII碼中的空格胡野。

Request-URI我們就更熟悉了,上述請求對應為:/res/static/thirdparty/connect.jpg?t=1480992153.564331痕鳍。這里值得注意的一點是:實際傳輸?shù)臅r候Request-URI有兩種可能的形式硫豆,一種是完整的absoluteURI,包含Schema和Host笼呆,另一種是abs_path熊响,并沒有包含Schema(http)和Host(mrpeak.cn)部分,Host部分被移交到了Header當中诗赌。所以平時我們抓包汗茄,有時看到的是完整的URI,有時則只有路徑信息铭若。

HTTP-Version也很直觀洪碳,文本展示形式為:HTTP/1.1,代表我們當前使用的版本叼屠。

CRLF由兩個字節(jié)組成瞳腌。CR值為16進制的0x0D,對應ASCII中的回車鍵镜雨,LF值為0x0A嫂侍,對應ASCII中的換行鍵,CRLF合起來就是我們平常所說的\r\n

所以上述請求的Request-Line的文本展示:

GET 空格 /res/static/thirdparty/connect.jpg?t=1480992153.564331 空格 HTTP/1.1 CRLF

Header

header其本質(zhì)上是一些文本鍵值對吵冒,一個典型的例子如下圖所示:

每個鍵值對的形式為:Key:空格 Value CRLF纯命。

上面講述Request-URI的時候,缺失的Host就以鍵值對的形式存在于header中痹栖,比如亿汞,Host: pan.baidu.com。

將若干個上述格式的鍵值對組合起來揪阿,就成了我們HTTP請求的完整header疗我。最后一個鍵值對之后再跟一個CRLF,就表示我們的header結(jié)束了南捂。

HTTP本身定義了一些header key吴裤,另外也允許開發(fā)者添加自己的key,自定義的key一般以X開頭溺健,比如可以定義X-APP-VERSION來記錄客戶端的版本號麦牺。

Body

body里面包含請求的實際數(shù)據(jù)。

對于Method=GET的請求來說鞭缭,body體是為空的剖膳,或者說不存在body體,Header最后的兩個CRLF就標識著請求的結(jié)尾岭辣。我們一般調(diào)用請求的業(yè)務參數(shù)是通過Request Line當中的Request-URI來傳遞的吱晒,比如上述請求中的?t=1480992153.564331,也就是URI的query string部分沦童。這部分同樣是以鍵值對的形式存在仑濒,不過是位于Request Line當中。

對于Method=POST的請求來說偷遗,body體一般不為空墩瞳,我們實際的業(yè)務數(shù)據(jù)都存放于body當中,數(shù)據(jù)在body體中是以何種形式存在鹦肿,其實大有門道矗烛,后面再細說。至于Request-URI當中的query string部分箩溃,我們依然可以選擇放置一部分數(shù)據(jù)在其中瞭吃,但更普遍的做法是使用body體。

HTTP Response

response的結(jié)構和request結(jié)構大致相同涣旨,可以用下圖表示:

不過是將Request Line換成了Status Line歪架。

Status Line的結(jié)構如下:

Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF

這里關鍵在于Status-Code的記憶,記住常見的Status-Code值霹陡,對于我們平時分析網(wǎng)絡錯誤十分有幫助和蚪,不需要記住每個值的含義止状,只需理解每個類別的含義即可:

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催束。

可以用來攜帶數(shù)據(jù)的部分

分析至此集峦,我們可以總結(jié)一個http請求,哪些地方是可以用來攜帶業(yè)務數(shù)據(jù)的抠刺。

Request Line當中的Request-URI是一個選擇塔淤,也是標準的GET請求用來傳遞數(shù)據(jù)的位置,一般以query string的格式存在于URI當中速妖。一些瀏覽器或者Framework對于query string的長度會有一定的限制高蜂,所以此處不適宜于傳遞較大的數(shù)據(jù)。

Header也是一個選擇罕容,我們可以選擇協(xié)議中的一些標準header key备恤,比如Host,User-Agent等杀赢,將我們的業(yè)務數(shù)據(jù)存放其Value中烘跺。或者我們通過自定義key脂崔,比如上面提到的X-APP-VERSION,使用X-開頭是業(yè)界默認的習慣梧喷,雖然RFC 6648當中建議大家不要再使用X-作為Prefix砌左,但這一習慣今天依舊還在持續(xù)。

Body體是我們的第三個選擇铺敌,POST請求可以根據(jù)Header中的Content-Type值汇歹,以不同的形式將數(shù)據(jù)保存在body體中。

一些隱藏的細節(jié)

可以看出http是一種基于文本解析的協(xié)議偿凭,上面提到的空格(0x20)产弹,換行(0x0D0A)都是HTTP用來做文本解析的輔助符號。

解析HTTP的text流程弯囊,其實也比較好理解痰哨。一個簡化的流程大致是這樣:當我們從TCP層拿到應用層的buffer之后,以CLRF(\r\n)為分割符匾嘱,將整個buffer分成若干行斤斧,第一行自然是我們的Request Line,之后每一行代表一個Header霎烙,如果連續(xù)讀到兩個CLRF撬讽,則表示header結(jié)束蕊连,如果是Method=POST,讀取Header中的Content-Length值游昼,最后根據(jù)這個值讀取固定長度的body體甘苍。這樣就完成了我們上述三個主要部分的讀取。當然烘豌,上述是個簡化的流程载庭,實際解析場景會更多一些。

我們再深入看下Request Line的解析

我們從TCP層拿到的實際上是一個字節(jié)流扇谣,要將字節(jié)流解析成我們能夠閱讀交流的形式昧捷,我們需要將字節(jié)碼進行編碼和解碼。Request Line使用的編解碼格式是US-ASCII罐寨,也就是我們平時接觸的ASCII碼中的一種靡挥。

Request Line通過ASCII碼做還原之后,我們得到的是類似這樣的結(jié)果:

GET /res/static/thirdparty/connect.jpg?a=1&b=2 HTTP/1.1

URI的解析也自有一套規(guī)范鸯绿,我們需要特別注意的是query string部分跋破。我們平時編寫業(yè)務代碼的時候,可能會在query string當中塞入自己的數(shù)據(jù)瓶蝴,這些數(shù)據(jù)可能是任意形式的字節(jié)流毒返,而Request Line和URI的解析都依賴于一些特殊字符來做分割,比如空格舷手,/拧簸,?等等男窟,所以為了能正確盆赤,安全的解析整個Request Line和URI,我們需要對query string中的字節(jié)流做進一步的編碼約束歉眷,只允許其中出現(xiàn)安全的ASCII碼牺六,這也是我們?yōu)槭裁葱枰猆rlEncode的原因。

UrlEncode的過程也比較簡單汗捡,它將字節(jié)流中的所有字節(jié)淑际,對照ASCII碼表分為,安全的ASCII碼和不安全的ASCII碼扇住。安全的ASCII碼不用做任何處理春缕,不安全的ASCII碼(比如空格0x20)則做進一步的編碼處理,編碼的思路也簡單:用安全的ASCII碼來代替不安全的ASCII碼台囱。比如空格(0x20)被編碼成%20淡溯,由一個ASCII碼(空格)變成了三個ASCII碼(%,2簿训,0)咱娶。對于原本就不是ASCII碼的內(nèi)容來說米间,比如中文,則先以UTF-8編碼成字節(jié)流膘侮,再對照ASCII碼做編碼屈糊。比如中文字「高」,其UTF-8的表現(xiàn)形式為:\xE9\xAB\x98琼了,再進一步做ASCII編碼逻锐,最后UrlEncode的結(jié)果就為:%E9%AB%98。

由此可見雕薪,UrlEncode是出于URL安全解析的需要昧诱,Encode的結(jié)果是由%和一部分安全的ASCII碼所組成。UrlEncode的缺點也比較明顯所袁,Encode非ASCII碼的時候(比如中文)盏档,一個字節(jié)會被encode成3個字節(jié),長度整整是原先的3倍燥爷,造成流量的浪費蜈亩。

我見過有人使用base64來對query string做encode,這是把概念搞混淆了前翎,至少base64 encode之后的=就不是一個URL安全的字符稚配,=在UrlEncode之后對應%3d。

Header的解析

對于Header的解析可以先按CRLF分割成一個個的鍵值對港华,鍵值對里面的值道川,也就是我們所說的field content其實也有編碼要求。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.

簡單來說立宜,我們在實際使用當中使用ASCII碼來限制field content愤惰。我們常用幾個Field,諸如Host赘理,User-Agent等,使用ASCII碼字符也已綽綽有余扇单,一般不會對值做進一步的encode處理商模。

Body的解析

body的解析是我們平時打交道最多的部分,不是說我們需要知道如何去解析body蜘澜,而是要了解body體里的數(shù)據(jù)格式施流。

body的解析本身比較簡單,從header中知道Content-Length之后鄙信,讀取固定長度的字節(jié)流即完成了body的獲取瞪醋,關鍵的環(huán)節(jié)是獲取之后,如何讀取其中的數(shù)據(jù)并遞交給應用層装诡,所以HTTP協(xié)議本身并沒有對Body中的內(nèi)容編碼做約束银受,而是把它交給協(xié)議的使用者去決定践盼,我們甚至可以在body體里存放二進制流,對應的Content-Type為application/octet-stream宾巍。

我們來看看平時發(fā)送HTTP請求時咕幻,以AFNetworking為例,使用最頻繁的幾種Content-Type:

  • multipart/form-data
  • application/x-www-form-urlencoded
  • application/json

當我們向Server發(fā)送數(shù)據(jù)的時候顶霞,需要和Server約定好所使用的Content-Type肄程,客戶端在發(fā)送Request的時候也要注意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不用多說蓝厌,大家都非常熟悉的數(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"];
}

可見當我們的Request沒有設置Content-Type的時候拓提,默認使用的就是application/x-www-form-urlencoded。這里的urlencoded和前面Request-URI中的urlencode是一回事描函,只不過encode的是body體當中的內(nèi)容崎苗。

那我們什么時候用application/x-www-form-urlencoded,什么時候用multipart/form-data呢舀寓?

先來看下使用Content-Type為multipart/form-data時胆数,我們的Request有什么變化,下圖是使用mitmproxy抓包一個文件上傳Request的headers示意圖:

Content-Type的完整值為:multipart/form-data; boundary=Boundary+2BBBEA582E48968C互墓。

multipart把body體分成多個塊必尼,多個塊之間依賴于boundary值去做分割,所以生成的boundary要足夠長篡撵,長到在字節(jié)流當中出現(xiàn)重復的概率幾乎為0判莉,否則就會導致錯誤的傳輸,AFNetworking中生成Boundary的方法如下:

static NSString * AFCreateMultipartFormBoundary() {
    return [NSString stringWithFormat:@"Boundary+%08X%08X", arc4random(), arc4random()];
}

我們可以看下一個例子育谬,如果使用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,這些會增加body的傳輸大小膛檀。

假設我們有一個大文件需要上傳锰镀,如果使用application/x-www-form-urlencoded作為Content-Type,由于字節(jié)流當中存在非常多的非ASCII碼咖刃,文件的長度會變至原本的2-3倍泳炉,所以此時multipart/form-data更合適。

假設我們只有少量的鍵值對需要上傳嚎杨,如果使用multipart/form-data作為Content-Type花鹅,由于boundary和Content-Disposition帶來的額外流量,又顯得得不償失,所以此時使用application/x-www-form-urlencoded更為合適酣难。

這也是為什么我們使用multipart/form-data作為文件類Request的Content-Type,而對于普通業(yè)務數(shù)據(jù)楼眷,則使用application/x-www-form-urlencoded或者application/json之景。

總結(jié)

上述的分析斤富,更多的是站在客戶端的角度去看的,實際HTTP協(xié)議的構成細節(jié)非常之多锻狗,需要曠日持久的深入學習和積累满力。功夫越深,坑越少 ;)

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末轻纪,一起剝皮案震驚了整個濱河市油额,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌刻帚,老刑警劉巖潦嘶,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異崇众,居然都是意外死亡掂僵,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門顷歌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锰蓬,“玉大人,你說我怎么就攤上這事眯漩∏叟ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵赦抖,是天一觀的道長舱卡。 經(jīng)常有香客問我,道長队萤,這世上最難降的妖魔是什么轮锥? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮要尔,結(jié)果婚禮上交胚,老公的妹妹穿的比我還像新娘。我一直安慰自己盈电,他們只是感情好,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布杯活。 她就那樣靜靜地躺著匆帚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪旁钧。 梳的紋絲不亂的頭發(fā)上吸重,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天互拾,我揣著相機與錄音,去河邊找鬼嚎幸。 笑死颜矿,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的嫉晶。 我是一名探鬼主播骑疆,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼替废!你這毒婦竟也來了箍铭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤椎镣,失蹤者是張志新(化名)和其女友劉穎诈火,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體状答,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡冷守,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了惊科。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拍摇。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖译断,靈堂內(nèi)的尸體忽然破棺而出授翻,到底是詐尸還是另有隱情,我是刑警寧澤孙咪,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布堪唐,位于F島的核電站,受9級特大地震影響翎蹈,放射性物質(zhì)發(fā)生泄漏淮菠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一荤堪、第九天 我趴在偏房一處隱蔽的房頂上張望合陵。 院中可真熱鬧,春花似錦澄阳、人聲如沸拥知。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽低剔。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間襟齿,已是汗流浹背姻锁。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留猜欺,地道東北人位隶。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像开皿,于是被迫代替她去往敵國和親涧黄。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 組織:中國互動出版網(wǎng)(http://www.china-pub.com/) RFC文檔中文翻譯計劃(http://...
    Palomar閱讀 1,573評論 0 6
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理副瀑,服務發(fā)現(xiàn)弓熏,斷路器,智...
    卡卡羅2017閱讀 134,654評論 18 139
  • 8. 方法定義(Method Definitions) 通用的HTTP/1.0的方法集將在下面定義糠睡,雖然該方法集可...
    Palomar閱讀 3,170評論 0 2
  • 原文https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html...
    梁行之閱讀 1,108評論 0 0
  • Nginx API for Lua Introduction ngx.arg ngx.var.VARIABLE C...
    吃瓜的東閱讀 5,781評論 0 5