真正“搞”懂HTTP協(xié)議之body的玩法
我們聊完了HTTP的特點和起始行的部分,并且著重的聊了聊請求方法和狀態(tài)碼。這兩個東西十分重要,因為它們往往會配合頭字段使用,我一再強調(diào),后續(xù)的內(nèi)容在涉及到相關內(nèi)容的時候。從這一章開始,直到HTTP/2為止,我會帶大家學習并通過Node來實踐HTTP/1的核心頭字段部分,HTTP的一些能力,其實大部分都是通過頭字段來擴展的。
那么這一章,我們就來學一學跟body有關的頭字段部分。
我們先來回憶一下,關于body的部分,到目前為止,我們已知的內(nèi)容有哪些呢?在0.9的時代,可以說是只有響應返回的body的,而沒有請求的body。到了1.0才有了請求體和響應體,也就是請求和響應才雙雙有了body,到了1.1則擴展了一些關于body的字段。我們下面就來看看關于body的頭字段的內(nèi)容及其協(xié)商的方式。
一、MIME
我之前簡單的聊到過這個東西,想必大家有點印象,MIME在HTTP的body體系中發(fā)揮了十分重要的作用。我們需要深入的了解下。我之前說過,當我們需要傳遞一些數(shù)據(jù)或者內(nèi)容的時候,我怎么才能把數(shù)據(jù)傳遞給對方并且正確的翻譯這份數(shù)據(jù)呢?傳遞的事情交給TCP我們不做太多的深究,而翻譯的工作則是由溝通雙方,或者說客戶端和服務器來做。
客戶端傳給服務器一個圖片,服務器怎么知道這是個圖片呢?或者反過來,客戶端怎么知道這是個圖片呢?理論上講,無論用什么辦法都不行。除非,我把”這是個圖片“告訴你。是不是感覺有點簡單,說白了就是協(xié)商。甚至于服務器收到了客戶端的消息,知道了這是一個圖片,但是我就是不按照圖片來解析,直接給你報個錯,你也沒辦法。
但是“標準”的意義就是我們要按照標準來,所以……雖然服務器可以不按規(guī)矩來,但是我們得按照規(guī)矩來學。
我們繼續(xù),哈哈哈,MIME是啥呢?MIME的全稱叫做多用途互聯(lián)網(wǎng)郵件擴展(Multipurpose Internet Mail Extensions),它本來是用在電子郵件系統(tǒng)里的,是為了可以讓郵件發(fā)送多類型的數(shù)據(jù),在這里有比較詳細的介紹,大家有興趣可以自己看。當HTTP也需要這個東西的時候,就發(fā)現(xiàn),欸?MIME不錯,我可以直接拿來用,省著我再自己搞一套了,所以HTTP順手牽羊就拿過來一部分用了。
MIME把數(shù)據(jù)分為了八大類,格式差不多是這樣的:type/subtype。它的八大類型差不多有這些:
- Text:用于標準化地表示的文字訊息,文字訊息可以是多種字符集和或者多種格式的;
- Multipart:用于連接消息體的多個部分構成一個消息,這些部分可以是不同類型的資料;
- Application:用于傳輸應用程序資料或者二進制資料;
- Message:用于包裝一個E-mail訊息;
- Image:用于傳輸靜態(tài)圖片資料;
- Audio:用于傳輸音頻或者音聲資料;
- Video:用于傳輸動態(tài)影像資料,可以是與音頻編輯在一起的視訊資料格式;
- Font:用于傳輸字體文件;
- Model:用于傳輸3D模型文件。
大家對其中的一些類型是不是都比較熟悉,比如Text、Multipart、Application、Image、Video等等,咱們在實際的工作中肯定都或多多少的接觸過。然后,我們再來看看子類型有哪些:
- text/plain(純文字)
- text/html(HTML文件)
- application/xhtml+xml(XHTML文件)
- image/gif(GIF圖片)
- image/jpeg(JPEG圖片)
- image/png(PNG圖片)
- audio/mpeg(MP3音頻)
- audio/aac(AAC音頻)
- video/mpeg(MPEG視頻)
- video/mp4(MPEG-4視頻)
- application/octet-stream(任意的二進制數(shù)據(jù))
- application/json(JSON文件)
- application/pdf(PDF文件)
- application/msword(Microsoft Word文件)
- application/vnd.openxmlformats-officedocument.wordprocessingml.document(Microsoft Word 2007文件)
- application/vnd.wap.xhtml+xml (wap1.0+)
- application/xhtml+xml (wap2.0+)
- message/rfc822(RFC 822形式)
- multipart/alternative(HTML郵件的HTML形式和純文本形式,相同內(nèi)容使用不同形式表示)
- application/x-www-form-urlencoded(使用HTTP的POST方法送出的表單)
- multipart/form-data(同上,但主要用于表單送出時伴隨文件上傳的場合)
我列出了大多數(shù)的數(shù)據(jù)類型,以及其子類型,當然,這些東西并不一定要求大家都完全理解,見名知意即可。而我加粗字體的部分,其實就是我們?nèi)粘9ぷ髦凶畛R姷膸追N數(shù)據(jù)類型。
二、數(shù)據(jù)類型
在HTTP中,我們可以通過Accept字段來告知服務器希望接收什么類型的數(shù)據(jù),服務器則用Content頭字段來告知客戶端實際發(fā)送了什么數(shù)據(jù)。注意Accept和Content是一個分類,也是我們本章要聊的核心內(nèi)容。其中包含了不少的頭字段,我們也會慢慢說。
我們繼續(xù)說回來這個表示數(shù)據(jù)類型的頭字段,Accept字段會表示客戶端可以理解的MIME type,可以用“,”分割,列出多個類型,讓服務器有更多選擇的可能,比如:
這就是告訴服務器,我能解析的數(shù)據(jù)類型有json、html以及xml,可以給我這些類型范圍內(nèi)的數(shù)據(jù)。
相應的,服務器會使用Content-Type頭字段告知客戶端實體數(shù)據(jù)的真實類型:
這樣瀏覽器讀取Content-Type就知道是個json文件,然后通過引擎解析,就完事了。
很簡單對吧。
然后……我還是要強調(diào)一下,如果服務器收到了客戶端想要的數(shù)據(jù)類型,但是我就不按照你想要的給你,咋滴,那其實也一點問題沒有,所以,在早期的RFC1945中,Accept是附加在其他功能中的。直到1.1的時候,才正式加入標準。
三、數(shù)據(jù)壓縮
通常情況下,我們在傳輸數(shù)據(jù)的時候,為了可以更好的節(jié)省帶寬,都會對數(shù)據(jù)進行壓縮后再傳輸。在HTTP中也是如此,那壓縮數(shù)據(jù)的方式往往有很多種,當然,這個很多就比MIME要少很多了,只有三種:
gzip:熟悉吧,也就是GNU zip壓縮格式,也是互聯(lián)網(wǎng)上最最最流行的壓縮格式;
deflate:zlib(deflate)壓縮格式,流行程度僅次于 gzip;
br:一種專門為 HTTP 優(yōu)化的新壓縮算法(Brotli)。
那么客戶端就可以使用Accept-Encoding字段來標記支持的壓縮格式,也可以通過“,”來分割多個支持的格式,服務器則會把實際使用的壓縮格式放到Content-Encoding字段里。
在實際使用中,這兩個字段是可以省略的,客戶端省略意味著不支持壓縮,服務器的省略則是告知客戶端傳輸?shù)倪@份數(shù)據(jù)沒有被壓縮。
四、語言類型
有了數(shù)據(jù)類型和壓縮類型,可以讓機器識別出傳輸?shù)臄?shù)據(jù)是什么以及如何解壓了。但是全球各地有這么多的國家和地區(qū),不同的國家和地區(qū)都使用不同的語言,甚至是相同國家和地區(qū)的人都可能使用不同的語言,那瀏覽器怎么顯示出每個人都可以理解的語言文字呢?換句話說,就是我如何根據(jù)不同的情況來正確的編碼這份數(shù)據(jù)呢?再換句話說,其實就是國際化的問題。
我猜已經(jīng)學到了這里的你已經(jīng)知道怎么解決了,協(xié)商唄,字段唄。哈哈哈,感覺有點無聊。。一點懸念都沒有。
對于請求頭來說使用的字段是Accept-Language,對于響應報文中的實體頭字段則是Content-Language,這里大家要注意一點,Accept頭字段是請求頭字段,而Content則是實體頭字段,不是響應頭字段噢。這個大家要注意一下。
例子如下:
很簡單,也不復雜,但是還沒完,這些頭字段對應的值是什么東東?嗯……這些東西叫做語言類型,就是 人類使用的自然語言,比如英語、漢語、法語等等,而這些自然語言也有其下屬方言,所以跟數(shù)據(jù)類型類似,也是type-subtype的形式,而與語言類型不同的是。數(shù)據(jù)類型是用"/"來分割父類與子類,語言類型則是用“-”來分割。
比如,en表示英語,en-US表示美式英語,en-GB表示英式英語,當然還有更多的語言類型,大家可以自行了解下,這里多說無益。
到了這里,服務器知道了用什么類型的語言,但是你要知道計算機的底層本質(zhì)就是0和1,我要怎么把0和1翻譯成對應的語言呢?這就要用到字符集了,在計算機發(fā)展的早期,十分的混亂,各個國家和地區(qū)的人們都自己定義了一套體系,發(fā)明了許多編碼來處理各自的文字,比如英語用ASCII,漢語用GBK。這就導致同樣一段文字,用不同的編碼就可能會顯示的一點都不一樣。
所以后來就出現(xiàn)了Unicode和UTF-8,把世界上所有的語言都容納在一種方案里。
在HTTP中的請求頭中,可以通過Accept-Charset來表達客戶端可以接受的編碼類型,但是響應頭里卻沒有對應的字段,而是在Content-Type字段的數(shù)據(jù)類型后面用“charset=xxx”來表示,這點你要特別注意。
不過在現(xiàn)代的瀏覽器里都支持多種字符集,所以通常情況下,不會發(fā)送Accept-Charset請求頭,服務器也不會返回Content-Language,因為使用的語言完全可以通過字符集推斷出來,所以一般在請求頭里只有Accent-Language,響應頭里只有Content-Type。
五、質(zhì)量值
質(zhì)量值的英文名叫做quality factory,直譯過來叫做質(zhì)量因數(shù),其實就是權重的意思啦。它在HTTP中使用q作為一個參數(shù),形式就是“q=value”,這個value可以是0到1之間,包含0和1的兩位小數(shù)。與字段中的值用“;”來分割。
這里要強調(diào)一點的是,在其它大多數(shù)語言中,就比如JavaScript吧,分號“;”的斷句語氣是要強于逗號“,”的,但是在HTTP中則相反。我們看個例子:
這段話是啥意思呢,就是瀏覽器最希望服務器傳過來的是html文件,不寫默認權重就是1,其次是xml文件,權重是0.9,最后就是權重為0.8的任意文件類型。服務器收到請求后,就會根據(jù)這段內(nèi)容,來優(yōu)先返回HTML。
六、Vary
這個東西有點怪怪的,我們來學學。它的意思是,我返回給你的響應報文,參考了哪些頭字段。也就是說,客戶端與瀏覽器在協(xié)商確定響應報文該如何返回的過程,其實并不透明,你不知道是咋協(xié)商的,或者服務器根本就不管你協(xié)商不協(xié)商都是有可能的。
但是友好一點的服務器會在響應頭里多加一個Vary字段,記錄服務器在內(nèi)容協(xié)商時參考的請求頭字段,給出一點信息。
上面的例子表示服務器參考了Accept-Encoding,User-Agent,Accept這三個字段后,返回了響應報文。
Vary 字段可以認為是響應報文的一個特殊的“版本標記”。每當 Accept 等請求頭變化時,Vary 也會隨著響應報文一起變化。也就是說,同一個 URI 可能會有多個不同的“版本”,主要用在傳輸鏈路中間的代理服務器實現(xiàn)緩存服務,這個之后講“HTTP 緩存”時還會再提到。
七、分塊傳輸
我們前六個小節(jié),聊了聊數(shù)據(jù)是如何在HTTP中協(xié)商才可以讓客戶端與服務器雙方知道怎么處理該數(shù)據(jù)。并且如果數(shù)據(jù)體積過大,我們還可以通過協(xié)商壓縮方式來給傳輸?shù)臄?shù)據(jù)進行壓縮傳輸??雌饋恚孟褚磺卸纪γ篮玫?,但是如果我要傳輸?shù)奈募w積特別大呢?比如一個視頻……小的有幾百兆,大的有幾個G,并且針對視頻的壓縮效率是很低的,那你怎么傳輸呢?
嗯……標題就是答案。我們沒辦法把一個大體積的數(shù)據(jù)整體變小,那么我們只能把這個特別大的數(shù)據(jù)進行切分,切分成一小塊一小塊的,服務器把這些小塊的數(shù)據(jù)傳輸給瀏覽器,瀏覽器收到后再按照一定的規(guī)則組裝復原。
這種思路在HTTP中就叫做chunked,也就是分塊傳輸編碼,在響應報文里可以使用“Transfer-Encoding: chunked”來表示,意思就是響應報文中的body不是一次性發(fā)送的,而是分成了許多的塊逐次發(fā)送的。
大家要注意的一點是,一個響應報文的長度要么已知,要么未知,不可能即知又不知,什么意思呢?就是Transfer-Encoding: chunked和Content-length是互斥的,不能同時出現(xiàn)在響應頭里。
八、范圍請求
有了分塊傳輸,我們可以把一份體積龐大的數(shù)據(jù)逐一發(fā)送,解決大文件在傳輸過程中的卡死問題。我們還是拿視頻來舉例,你在騰訊視頻或者愛奇藝上看電視劇,看的正開心呢,突然彈出來一個視頻內(nèi)廣告,或者你不想看電視劇的開頭和結尾,你會通過拖動進度條來跳過這部分內(nèi)容,那這中實現(xiàn)就需要用到范圍請求。
換句話說,我們希望可以獲取一個大文件的某一塊片段,而分塊傳輸是做不到這點的,分塊傳輸只能在開始傳輸?shù)臅r候就把塊分好傳給你,無法確定我需要的某一個范圍的數(shù)據(jù)。
解決這樣的問題就需要用到范圍請求了,范圍請求允許客戶端在請求頭里使用專用字段來表示只獲取整個文件的一部分。
范圍請求并不是Web服務器必備的功能,可以實現(xiàn)也可以不實現(xiàn),所以服務器必須在響應頭里使用字段“Accept-Ranges: bytes”明確的告知客戶端我是支持范圍請求的。如果不支持的話可以用“Accept-Ranges: none”告知客戶端,或者直接就不發(fā)送Accept-Ranges字段。
客戶端使用“Range”作為范圍請求的請求頭格式是“bytes=x-y”,x和y就是以字節(jié)為單位的范圍數(shù)據(jù)了,x必須從0開始,比如0-9是指前10個字節(jié),以此類推。
當服務器收到Range字段后,就會做四件事:
首先服務器會檢查你傳過來的Range范圍是否合法,不合法的范圍服務器會直接甩給你一個416,告訴你請求的數(shù)據(jù)范圍是不合法的。
其次,如果范圍合法,那么服務器則會根據(jù)你的范圍讀取文件的片段,返回206狀態(tài)碼,也就是Partial Content,表示返回了原數(shù)據(jù)的一部分。
服務器在返回部分數(shù)據(jù)的時候會加上一個Content-Range響應頭,告訴客戶端實際的偏移量和資源的總大小,格式是這樣的“bytes x-y/length”。
最后就是發(fā)送數(shù)據(jù)了。
不僅僅是看視頻拖拽進度可以用到范圍請求,下載時候的多端下載和斷點續(xù)傳,實際上也是基于它來實現(xiàn)的,這個我們下一章實踐的時候再說。
九、多段數(shù)據(jù)
基于范圍請求,我們還可以請求不止一個范圍的片段,也就是一次性請求多段數(shù)據(jù)。這種情況需要用到一個特殊的MIME類型:multipart/byterange,表示報文的body是由多段字節(jié)序列組成的,并且還需要一個boundary=xxx來給出段之間的分割標記。就像這樣:
這個boundary=00000000001就是分割標記,開始以--00000000001開始,--00000000001--結束。
嗯……這么說理論肯定有點模糊,我們下一篇動手實踐的時候就可以清楚的看到它的形式是什么樣子的了。
總結
本篇我們主要聊了兩件事,一個是文件、另外一個就是傳輸文件。文件相關的我們聊了文件的類型、語言類型、簡單壓縮等等,傳輸文件則主要有兩個部分,一個是分塊傳輸、一個是分段傳輸。就這點東西,沒了,全是理論。
另外,我還要強調(diào)一下第四部分聊的語言類型和國際化的問題,實際上在HTTP中的國際化,是指你傳輸?shù)奈募?nèi)的數(shù)據(jù)語言,并不是我們在前端單頁應用中使用的國際化插件,這兩者是有差別的?!?/p>