扒一扒 HTTP 的構成
HTTP全稱為HyperText Transfer Protocol,從名字不難看出這是一種基於文字的網路協議,對於初學者來說比較友好,容易上手。各平臺上的一些第三方庫都對HTTP做了進一步的封裝,讓HTTP變得更加親民,但往往拿來就用的技術,很容易忽視其背後隱藏的細節。今天一起來扒一扒HTTP到底是如何構成的。
初窺全貌
HTTP第一眼看上去非常簡單,先來看看Request部分:
上圖主要分為三部分:request line,header和body,中間的CRLF為換行符。如果能將我們平常傳送的http請求對應到上述三個部分,就能形成初步的印象了。
我們以一個實際的http request例子,抓包來看一看詳細的內部構造。假設我們的請求URL為:
http://www.baidu.com/res/static/thirdparty/connect.jpg?t=1480992153433
後續的分析都是以此請求為基礎。
Request Line
Request Line的結構為:
Request-Line = Method SP Request-URI SP HTTP-Version CRLF
Method也就是我們平常談論最多的POST和GET所處的部分(除了POST和GET,還有其他型別的Method)。
SP是個分隔符,我用Wireshark抓包看了下,就一個位元組大小,值為0×20,對應ASCII碼中的空格。
Request-URI我們就更熟悉了,上述請求對應為:/res/static/thirdparty/connect.jpg?t=1480992153.564331。這裡值得注意的一點是:實際傳輸的時候Request-URI有兩種可能的形式,一種是完整的absoluteURI,包含Schema和Host,另一種是abs_path,並沒有包含Schema(http)和Host(mrpeak.cn)部分,Host部分被移交到了Header當中。所以平時我們抓包,有時看到的是完整的URI,有時則只有路徑資訊。
HTTP-Version也很直觀,文字展示形式為:HTTP/1.1,代表我們當前使用的版本。
CRLF由兩個位元組組成。CR值為16進位制的0x0D,對應ASCII中的Enter鍵,LF值為0x0A,對應ASCII中的換行鍵,CRLF合起來就是我們平常所說的\r\n。
所以上述請求的Request-Line的文字展示:
GET 空格 /res/static/thirdparty/connect.jpg?t=1480992153.564331 空格 HTTP/1.1 CRLF
Header
header其本質上是一些文字鍵值對,一個典型的例子如下圖所示:
每個鍵值對的形式為:Key:空格 Value CRLF。
上面講述Request-URI的時候,缺失的Host就以鍵值對的形式存在於header中,比如,Host: pan.baidu.com。
將若干個上述格式的鍵值對組合起來,就成了我們HTTP請求的完整header。最後一個鍵值對之後再跟一個CRLF,就表示我們的header結束了。
HTTP本身定義了一些header key,另外也允許開發者新增自己的key,自定義的key一般以X開頭,比如可以定義X-APP-VERSION來記錄客戶端的版本號。
Body
body裡面包含請求的實際資料。
對於Method=GET的請求來說,body體是為空的,或者說不存在body體,Header最後的兩個CRLF就標識著請求的結尾。我們一般呼叫請求的業務引數是通過Request Line當中的Request-URI來傳遞的,比如上述請求中的?t=1480992153.564331,也就是URI的query string部分。這部分同樣是以鍵值對的形式存在,不過是位於Request Line當中。
對於Method=POST的請求來說,body體一般不為空,我們實際的業務資料都存放於body當中,資料在body體中是以何種形式存在,其實大有門道,後面再細說。至於Request-URI當中的query string部分,我們依然可以選擇放置一部分資料在其中,但更普遍的做法是使用body體。
HTTP Response
response的結構和request結構大致相同,可以用下圖表示:
不過是將Request Line換成了Status Line。
Status Line的結構如下:
Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
這裡關鍵在於Status-Code的記憶,記住常見的Status-Code值,對於我們平時分析網路錯誤十分有幫助,不需要記住每個值的含義,只需理解每個類別的含義即可:
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。
可以用來攜帶資料的部分
分析至此,我們可以總結一個http請求,哪些地方是可以用來攜帶業務資料的。
Request Line當中的Request-URI是一個選擇,也是標準的GET請求用來傳遞資料的位置,一般以query string的格式存在於URI當中。一些瀏覽器或者Framework對於query string的長度會有一定的限制,所以此處不適宜於傳遞較大的資料。
Header也是一個選擇,我們可以選擇協議中的一些標準header key,比如Host,User-Agent等,將我們的業務資料存放其Value中。或者我們通過自定義key,比如上面提到的X-APP-VERSION,使用X-開頭是業界預設的習慣,雖然RFC 6648當中建議大家不要再使用X-作為Prefix,但這一習慣今天依舊還在持續。
Body體是我們的第三個選擇,POST請求可以根據Header中的Content-Type值,以不同的形式將資料儲存在body體中。
一些隱藏的細節
可以看出http是一種基於文字解析的協議,上面提到的空格(0×20),換行(0x0D0A)都是HTTP用來做文字解析的輔助符號。
解析HTTP的text流程,其實也比較好理解。一個簡化的流程大致是這樣:當我們從TCP層拿到應用層的buffer之後,以CLRF(\r\n)為分割符,將整個buffer分成若干行,第一行自然是我們的Request Line,之後每一行代表一個Header,如果連續讀到兩個CLRF,則表示header結束,如果是Method=POST,讀取Header中的Content-Length值,最後根據這個值讀取固定長度的body體。這樣就完成了我們上述三個主要部分的讀取。當然,上述是個簡化的流程,實際解析場景會更多一些。
我們再深入看下Request Line的解析
我們從TCP層拿到的實際上是一個位元組流,要將位元組流解析成我們能夠閱讀交流的形式,我們需要將位元組碼進行編碼和解碼。Request Line使用的編解碼格式是US-ASCII,也就是我們平時接觸的ASCII碼中的一種。
Request Line通過ASCII碼做還原之後,我們得到的是類似這樣的結果:
GET /res/static/thirdparty/connect.jpg?a=1&b=2 HTTP/1.1
URI的解析也自有一套規範,我們需要特別注意的是query string部分。我們平時編寫業務程式碼的時候,可能會在query string當中塞入自己的資料,這些資料可能是任意形式的位元組流,而Request Line和URI的解析都依賴於一些特殊字元來做分割,比如空格,/,?等等,所以為了能正確,安全的解析整個Request Line和URI,我們需要對query string中的位元組流做進一步的編碼約束,只允許其中出現安全的ASCII碼,這也是我們為什麼需要UrlEncode的原因。
UrlEncode的過程也比較簡單,它將位元組流中的所有位元組,對照ASCII碼錶分為,安全的ASCII碼和不安全的ASCII碼。安全的ASCII碼不用做任何處理,不安全的ASCII碼(比如空格0×20)則做進一步的編碼處理,編碼的思路也簡單:用安全的ASCII碼來代替不安全的ASCII碼。比如空格(0×20)被編碼成%20,由一個ASCII碼(空格)變成了三個ASCII碼(%,2,0)。對於原本就不是ASCII碼的內容來說,比如中文,則先以UTF-8編碼成位元組流,再對照ASCII碼做編碼。比如中文字「高」,其UTF-8的表現形式為:\xE9\xAB\x98,再進一步做ASCII編碼,最後UrlEncode的結果就為:%E9%AB%98。
由此可見,UrlEncode是出於URL安全解析的需要,Encode的結果是由%和一部分安全的ASCII碼所組成。UrlEncode的缺點也比較明顯,Encode非ASCII碼的時候(比如中文),一個位元組會被encode成3個位元組,長度整整是原先的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體裡的資料格式。
body的解析本身比較簡單,從header中知道Content-Length之後,讀取固定長度的位元組流即完成了body的獲取,關鍵的環節是獲取之後,如何讀取其中的資料並遞交給應用層,所以HTTP協議本身並沒有對Body中的內容編碼做約束,而是把它交給協議的使用者去決定,我們甚至可以在body體裡存放二進位制流,對應的Content-Type為application/octet-stream。
我們來看看平時傳送HTTP請求時,以AFNetworking為例,使用最頻繁的幾種Content-Type:
- multipart/form-data
- application/x-www-form-urlencoded
- application/json
當我們向Server傳送資料的時候,需要和Server約定好所使用的Content-Type,客戶端在傳送Request的時候也要注意API的差別,以AFNetworking為例,傳送json則使用:
AFJSONRequestSerializer* jsonSerializer = [AFJSONRequestSerializer serializer]; request = [jsonSerializer requestWithMethod:@"POST" URLString:requestUrl parameters:requestParams error:nil];
傳送multipart/form-data:
request = [self.requestSerializer multipartFormRequestWithMethod:@"POST" URLString:requestUrl parameters:requestParams constructingBodyWithBlock:nil error:nil];
傳送x-www-form-urlencoded:
request = [self.requestSerializer requestWithMethod:@"POST" URLString:requestUrl parameters:requestParams error:nil];
json不用多說,大家都非常熟悉的資料交換格式。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體當中的內容。
那我們什麼時候用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要足夠長,長到在位元組流當中出現重複的概率幾乎為0,否則就會導致錯誤的傳輸,AFNetworking中生成Boundary的方法如下:
static NSString * AFCreateMultipartFormBoundary() { return [NSString stringWithFormat:@"Boundary+%08X%08X", arc4random(), arc4random()]; }
我們可以看下一個例子,如果使用multipart/form-data,body中具體的資料格式:
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,由於位元組流當中存在非常多的非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,而對於普通業務資料,則使用application/x-www-form-urlencoded或者application/json。
總結
上述的分析,更多的是站在客戶端的角度去看的,實際HTTP協議的構成細節非常之多,需要曠日持久的深入學習和積累。功夫越深,坑越少
相關文章
- 扒一扒安卓的渲染原理安卓
- 扒一扒安卓渲染原理安卓
- 扒一扒ELF檔案
- 扒一扒 EventServiceProvider 原始碼IDE原始碼
- 扒一扒PROMISE的原理,大家不要怕!Promise
- 扒一扒Kotlin協程的底褲Kotlin
- 扒一扒React計算狀態的原理React
- 扒一扒 CSS 語言的誕生史CSS
- 扒一扒程式語言排行榜
- 扒一扒 Jetpack Compose 實現原理Jetpack
- 扒一扒「清華系」的 AI 安防大佬們AI
- 用大資料扒一扒蔡徐坤的真假流量粉大資料
- 扒一扒Bean注入到Spring的那些姿勢BeanSpring
- 扒一扒我們生活中常見的品牌小程式
- 扒一扒「黑客軍團」中用到的黑客工具黑客
- 扒一扒移動網際網路裡的流氓
- 扒一扒隨機數(Random Number)的誕生歷史隨機random
- BEM實戰之扒一扒淘票票頁面
- 防扒
- 扒一扒JVM的垃圾回收機制,下次面試你準備好了嗎JVM面試
- 非得從零開始學習?扒一扒強化學習的致命缺陷強化學習
- 扒一扒9.3閱兵直播如何採用虛擬現實技術
- 日入50000元,扒扒抖音本地生活小程式的變現模式模式
- 扒一下Redis的配置檔案Redis
- 從“掃月亮”到“掃福字”,扒一扒背後的支付寶AR框架體系框架
- 扒一扒spring,dom4j實現模擬實現讀取xmlSpringXML
- 釋出防扒提示,
- 人剛畢業,顛覆整個AI界:扒一扒Sora兩帶頭人博士論文AISora
- 基於node的微小爬蟲——扒了一下知乎爬蟲
- 怎麼用python扒網頁?Python網頁
- SiteSucker pro 最新漢化版,Mac扒站神器Mac
- 性感的Promise,擁抱ta然後扒光taPromise
- 淺扒Android動態設定字型大小Android
- 《吃透MQ系列》之扒開Kafka的神祕面紗MQKafka
- 五招扒掉創業公司的假資料創業
- 素材火基於thinkphp開發,免費扒模板PHP
- 從X86指令深扒JVM的位移操作JVM
- 我扒了Bugly的資料,只是想出個報表