真正“搞”懂HTTP協議06之body的玩法(理論篇)

Zaking 發表於 2022-12-08

  本來啊,本來,本來我在準備完善這個鴿了四年的系列的時候,是打算按照時間的順序來完成的,好吧。我承認那個時候考慮的稍稍稍稍稍微有些不足,就是我忽略了HTTP協議的“模組性“。因為雖然按照時間順序寫寫流水賬好像是個不錯的選擇,但是寫著寫著發現,其實HTTP的頭欄位,往往是一塊一塊的,這一塊的部分包含了某一系列的欄位作為請求和應答的協商方式。

  那問題來了,按照時間順序會把”模組“打散,思路實在是有些混亂,但是完全按照模組的形式又忽略了HTTP歷程的發展。所以,我思前想後,輾轉反側,決定以模組為核心,輔之以該模組的歷史程式,這樣總可以了吧。

  然後呢,這裡還有個意料之外的事情,就是……,如果完全按照時間順序來寫,就有個很無奈的頭重腳輕的問題,HTTP的發展並不是緩慢的、平均的、順序的發展,0.9及其之前的部分只是個論文,0.9和1.0的部分只是個memo,到了1.1才算是可以廣泛的應用。然後再繼續往後的2.0、3.0醬紫發展。所以,還真不能完全按照時間順序來寫流水賬。嗯……好在我懸崖勒馬,及時意識到了這個問題。知恥而後勇,嗯……就這樣。

  以下是正文。


 

  上一章,我們聊完了HTTP的特點和起始行的部分,並且著重的聊了聊請求方法和狀態碼。這兩個東西十分重要,因為它們往往會配合頭欄位使用,我一再強調,後續的內容在涉及到相關內容的時候。從這一章開始,直到HTTP/2為止,我會帶大家學習並透過Node來實踐HTTP/1的核心頭欄位部分,HTTP的一些能力,其實大部分都是透過頭欄位來擴充套件的。

  那麼這一章,我們就來學一學跟body有關的頭欄位部分。

  我們先來回憶一下,關於body的部分,到目前為止,我們已知的內容有哪些呢?在0.9的時代,可以說是隻有響應返回的body的,而沒有請求的body。到了1.0才有了請求體和響應體,也就是請求和響應才雙雙有了body,到了1.1則擴充套件了一些關於body的欄位。我們下面就來看看關於body的頭欄位的內容及其協商的方式。

一、MIME

  我之前簡單的聊到過這個東西,想必大家有點印象,MIME在HTTP的body體系中發揮了十分重要的作用。我們需要深入的瞭解下。我之前說過,當我們需要傳遞一些資料或者內容的時候,我怎麼才能把資料傳遞給對方並且正確的翻譯這份資料呢?傳遞的事情交給TCP我們不做太多的深究,而翻譯的工作則是由溝通雙方,或者說客戶端和伺服器來做。

  客戶端傳給伺服器一個圖片,伺服器怎麼知道這是個圖片呢?或者反過來,客戶端怎麼知道這是個圖片呢?理論上講,無論用什麼辦法都不行。除非,我把”這是個圖片“告訴你。是不是感覺有點簡單,說白了就是協商。甚至於伺服器收到了客戶端的訊息,知道了這是一個圖片,但是我就是不按照圖片來解析,直接給你報個錯,你也沒辦法。

  但是“標準”的意義就是我們要按照標準來,所以……雖然伺服器可以不按規矩來,但是我們得按照規矩來學。

  我們繼續,哈哈哈,MIME是啥呢?MIME的全稱叫做多用途網際網路郵件擴充套件(Multipurpose Internet Mail Extensions),它本來是用在電子郵件系統裡的,是為了可以讓郵件傳送多型別的資料,在這裡有比較詳細的介紹,大家有興趣可以自己看。當HTTP也需要這個東西的時候,就發現,欸?MIME不錯,我可以直接拿來用,省著我再自己搞一套了,所以HTTP順手牽羊就拿過來一部分用了。

  MIME把資料分為了八大類,格式差不多是這樣的:type/subtype。它的八大型別差不多有這些:

  1. Text:用於標準化地表示的文字訊息,文字訊息可以是多種字符集和或者多種格式的;
  2. Multipart:用於連線訊息體的多個部分構成一個訊息,這些部分可以是不同型別的資料;
  3. Application:用於傳輸應用程式資料或者二進位制資料;
  4. Message:用於包裝一個E-mail訊息;
  5. Image:用於傳輸靜態圖片資料;
  6. Audio:用於傳輸音訊或者音聲資料;
  7. Video:用於傳輸動態影像資料,可以是與音訊編輯在一起的視訊資料格式;
  8. Font:用於傳輸字型檔案;
  9. 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(任意的二進位制資料)
  • 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形式和純文字形式,相同內容使用不同形式表示)
  • application/x-www-form-urlencoded(使用HTTP的POST方法送出的表單)
  • multipart/form-data(同上,但主要用於表單送出時伴隨檔案上傳的場合)

  我列出了大多數的資料型別,以及其子型別,當然,這些東西並不一定要求大家都完全理解,見名知意即可。而我加粗字型的部分,其實就是我們日常工作中最常見的幾種資料型別。

二、資料型別

  在HTTP中,我們可以透過Accept欄位來告知伺服器希望接收什麼型別的資料,伺服器則用Content頭欄位來告知客戶端實際傳送了什麼資料。注意Accept和Content是一個分類,也是我們本章要聊的核心內容。其中包含了不少的頭欄位,我們也會慢慢說。

  我們繼續說回來這個表示資料型別的頭欄位,Accept欄位會表示客戶端可以理解的MIME type,可以用“,”分割,列出多個型別,讓伺服器有更多選擇的可能,比如:

Accept: application/json,text/html,application/xml

  這就是告訴伺服器,我能解析的資料型別有json、html以及xml,可以給我這些型別範圍內的資料。

  相應的,伺服器會使用Content-Type頭欄位告知客戶端實體資料的真實型別:

Content-Type: application/json

  這樣瀏覽器讀取Content-Type就知道是個json檔案,然後透過引擎解析,就完事了。

  很簡單對吧。

  然後……我還是要強調一下,如果伺服器收到了客戶端想要的資料型別,但是我就不按照你想要的給你,咋滴,那其實也一點問題沒有,所以,在早期的RFC1945中,Accept是附加在其他功能中的。直到1.1的時候,才正式加入標準。

三、資料壓縮

  通常情況下,我們在傳輸資料的時候,為了可以更好的節省頻寬,都會對資料進行壓縮後再傳輸。在HTTP中也是如此,那壓縮資料的方式往往有很多種,當然,這個很多就比MIME要少很多了,只有三種:

  1. gzip:熟悉吧,也就是GNU zip壓縮格式,也是網際網路上最最最流行的壓縮格式;
  2. deflate:zlib(deflate)壓縮格式,流行程度僅次於 gzip;
  3. br:一種專門為 HTTP 最佳化的新壓縮演算法(Brotli)。

  那麼客戶端就可以使用Accept-Encoding欄位來標記支援的壓縮格式,也可以透過“,”來分割多個支援的格式,伺服器則會把實際使用的壓縮格式放到Content-Encoding欄位裡。

Accept-Encoding: gzip, deflate, br

Content-Encoding: gzip

  在實際使用中,這兩個欄位是可以省略的,客戶端省略意味著不支援壓縮,伺服器的省略則是告知客戶端傳輸的這份資料沒有被壓縮。

四、語言型別

  有了資料型別和壓縮型別,可以讓機器識別出傳輸的資料是什麼以及如何解壓了。但是全球各地有這麼多的國家和地區,不同的國家和地區都使用不同的語言,甚至是相同國家和地區的人都可能使用不同的語言,那瀏覽器怎麼顯示出每個人都可以理解的語言文字呢?換句話說,就是我如何根據不同的情況來正確的編碼這份資料呢?再換句話說,其實就是國際化的問題。

  我猜已經學到了這裡的你已經知道怎麼解決了,協商唄,欄位唄。哈哈哈,感覺有點無聊。。一點懸念都沒有。

  對於請求頭來說使用的欄位是Accept-Language,對於響應報文中的實體頭欄位則是Content-Language,這裡大家要注意一點,Accept頭欄位是請求頭欄位,而Content則是實體頭欄位,不是響應頭欄位噢。這個大家要注意一下。

  例子如下:

Accept-Language: zh-CN, zh, en

Content-Language: zh-CN

  很簡單,也不復雜,但是還沒完,這些頭欄位對應的值是什麼東東?嗯……這些東西叫做語言型別,就是 人類使用的自然語言,比如英語、漢語、法語等等,而這些自然語言也有其下屬方言,所以跟資料型別類似,也是type-subtype的形式,而與語言型別不同的是。資料型別是用"/"來分割父類與子類,語言型別則是用“-”來分割。

  比如,en表示英語,en-US表示美式英語,en-GB表示英式英語,當然還有更多的語言型別,大家可以自行了解下,這裡多說無益。

  到了這裡,伺服器知道了用什麼型別的語言,但是你要知道計算機的底層本質就是0和1,我要怎麼把0和1翻譯成對應的語言呢?這就要用到字符集了,在計算機發展的早期,十分的混亂,各個國家和地區的人們都自己定義了一套體系,發明了許多編碼來處理各自的文字,比如英語用ASCII,漢語用GBK。這就導致同樣一段文字,用不同的編碼就可能會顯示的一點都不一樣。

  所以後來就出現了Unicode和UTF-8,把世界上所有的語言都容納在一種方案裡。

  在HTTP中的請求頭中,可以透過Accept-Charset來表達客戶端可以接受的編碼型別,但是響應頭裡卻沒有對應的欄位,而是在Content-Type欄位的資料型別後面用“charset=xxx”來表示,這點你要特別注意。

Accept-Charset: gbk, utf-8

Content-Type: text/html; charset=utf-8

  不過在現代的瀏覽器裡都支援多種字符集,所以通常情況下,不會傳送Accept-Charset請求頭,伺服器也不會返回Content-Language,因為使用的語言完全可以透過字符集推斷出來,所以一般在請求頭裡只有Accent-Language,響應頭裡只有Content-Type。

五、質量值

   質量值的英文名叫做quality factory,直譯過來叫做質量因數,其實就是權重的意思啦。它在HTTP中使用q作為一個引數,形式就是“q=value”,這個value可以是0到1之間,包含0和1的兩位小數。與欄位中的值用“;”來分割。

  這裡要強調一點的是,在其它大多數語言中,就比如JavaScript吧,分號“;”的斷句語氣是要強於逗號“,”的,但是在HTTP中則相反。我們看個例子:

Accept: text/html,application/xml;q=0.9,*/*;q=0.8

  這段話是啥意思呢,就是瀏覽器最希望伺服器傳過來的是html檔案,不寫預設權重就是1,其次是xml檔案,權重是0.9,最後就是權重為0.8的任意檔案型別。伺服器收到請求後,就會根據這段內容,來優先返回HTML。

六、Vary

  這個東西有點怪怪的,我們來學學。它的意思是,我返回給你的響應報文,參考了哪些頭欄位。也就是說,客戶端與瀏覽器在協商確定響應報文該如何返回的過程,其實並不透明,你不知道是咋協商的,或者伺服器根本就不管你協商不協商都是有可能的。

  但是友好一點的伺服器會在響應頭裡多加一個Vary欄位,記錄伺服器在內容協商時參考的請求頭欄位,給出一點資訊。

Vary: Accept-Encoding,User-Agent,Accept

  上面的例子表示伺服器參考了Accept-Encoding,User-Agent,Accept這三個欄位後,返回了響應報文。

  Vary 欄位可以認為是響應報文的一個特殊的“版本標記”。每當 Accept 等請求頭變化時,Vary 也會隨著響應報文一起變化。也就是說,同一個 URI 可能會有多個不同的“版本”,主要用在傳輸鏈路中間的代理伺服器實現快取服務,這個之後講“HTTP 快取”時還會再提到。

七、分塊傳輸

  我們前六個小節,聊了聊資料是如何在HTTP中協商才可以讓客戶端與伺服器雙方知道怎麼處理該資料。並且如果資料體積過大,我們還可以透過協商壓縮方式來給傳輸的資料進行壓縮傳輸。看起來,好像一切都挺美好的,但是如果我要傳輸的檔案體積特別大呢?比如一個影片……小的有幾百兆,大的有幾個G,並且針對影片的壓縮效率是很低的,那你怎麼傳輸呢?

  嗯……標題就是答案。我們沒辦法把一個大體積的資料整體變小,那麼我們只能把這個特別大的資料進行切分,切分成一小塊一小塊的,伺服器把這些小塊的資料傳輸給瀏覽器,瀏覽器收到後再按照一定的規則組裝復原。

  這種思路在HTTP中就叫做chunked,也就是分塊傳輸編碼,在響應報文裡可以使用“Transfer-Encoding: chunked”來表示,意思就是響應報文中的body不是一次性傳送的,而是分成了許多的塊逐次傳送的。

  大家要注意的一點是,一個響應報文的長度要麼已知,要麼未知,不可能即知又不知,什麼意思呢?就是Transfer-Encoding: chunked和Content-length是互斥的,不能同時出現在響應頭裡。

八、範圍請求

  有了分塊傳輸,我們可以把一份體積龐大的資料逐一傳送,解決大檔案在傳輸過程中的卡死問題。我們還是拿影片來舉例,你在騰訊影片或者愛奇藝上看電視劇,看的正開心呢,突然彈出來一個影片內廣告,或者你不想看電視劇的開頭和結尾,你會透過拖動進度條來跳過這部分內容,那這中實現就需要用到範圍請求。

  換句話說,我們希望可以獲取一個大檔案的某一塊片段,而分塊傳輸是做不到這點的,分塊傳輸只能在開始傳輸的時候就把塊分好傳給你,無法確定我需要的某一個範圍的資料。

  解決這樣的問題就需要用到範圍請求了,範圍請求允許客戶端在請求頭裡使用專用欄位來表示只獲取整個檔案的一部分。

  範圍請求並不是Web伺服器必備的功能,可以實現也可以不實現,所以伺服器必須在響應頭裡使用欄位“Accept-Ranges: bytes”明確的告知客戶端我是支援範圍請求的。如果不支援的話可以用“Accept-Ranges: none”告知客戶端,或者直接就不傳送Accept-Ranges欄位。

  客戶端使用“Range”作為範圍請求的請求頭格式是“bytes=x-y”,x和y就是以位元組為單位的範圍資料了,x必須從0開始,比如0-9是指前10個位元組,以此類推。

  當伺服器收到Range欄位後,就會做四件事:

  1. 首先伺服器會檢查你傳過來的Range範圍是否合法,不合法的範圍伺服器會直接甩給你一個416,告訴你請求的資料範圍是不合法的。
  2. 其次,如果範圍合法,那麼伺服器則會根據你的範圍讀取檔案的片段,返回206狀態碼,也就是Partial Content,表示返回了原資料的一部分。
  3. 伺服器在返回部分資料的時候會加上一個Content-Range響應頭,告訴客戶端實際的偏移量和資源的總大小,格式是這樣的“bytes x-y/length”。
  4. 最後就是傳送資料了。

  不僅僅是看影片拖拽進度可以用到範圍請求,下載時候的多端下載和斷點續傳,實際上也是基於它來實現的,這個我們下一章實踐的時候再說。

九、多段資料

  基於範圍請求,我們還可以請求不止一個範圍的片段,也就是一次性請求多段資料。這種情況需要用到一個特殊的MIME型別:multipart/byterange,表示報文的body是由多段位元組序列組成的,並且還需要一個boundary=xxx來給出段之間的分割標記。就像這樣:

Content-Type: multipart/byteranges; boundary=00000000001

  這個boundary=00000000001就是分割標記,開始以--00000000001開始,--00000000001--結束。

  嗯……這麼說理論肯定有點模糊,我們下一篇動手實踐的時候就可以清楚的看到它的形式是什麼樣子的了。

總結

  本篇我們主要聊了兩件事,一個是檔案、另外一個就是傳輸檔案。檔案相關的我們聊了檔案的型別、語言型別、簡單壓縮等等,傳輸檔案則主要有兩個部分,一個是分塊傳輸、一個是分段傳輸。就這點東西,沒了,全是理論。下一篇,我們就本篇的內容,來把我們學過的這些理論全都實踐一遍,加深印象。

  另外,我還要強調一下第四部分聊的語言型別和國際化的問題,實際上在HTTP中的國際化,是指你傳輸的檔案內的資料語言,並不是我們在前端單頁應用中使用的國際化外掛,這兩者是有差別的。