[HTTP 系列] 第 2 篇 —— HTTP 協議那些事

YanceyOfficial發表於2019-05-06

這裡是《寫給前端工程師的 HTTP 系列》,記得有位大佬曾經說過:“大廠前端面試對 HTTP 的要求比 CSS 還要高”,由此可見 HTTP 的重要程度不可小視。文章寫作計劃如下,視情況可能有一定的刪減,本篇是該系列的第 2 篇 —— 《HTTP 協議那些事》。這篇文章會涉及到 HTTP 協議,cookie 和 session,HTTP 首部/方法/狀態碼等。

更多文章可以關注我的 interview 系列

寫作計劃

HTTP 協議

超文字傳輸協議(HyperText Transfer Protocol)是基於 TCP/IP 協議,用於分散式、協作式和超媒體資訊系統的應用層協議。HTTP 是全球資訊網的資料通訊的基礎,它是 無狀態 的協議,預設埠為 80。HTTP 在 TCP 的基礎上,規定了 Request-Response 的模式,這個模式決定了通訊必定由瀏覽器首先發起。

拋去一些複雜的層面,瀏覽器開發者只需要一個 TCP 庫就可以搞定瀏覽器的網路通訊部分。我們可以用 telnet 來做個實驗。

首先連線到 yanceyleo.com 的主機。

telnet yanceyleo.com 80
複製程式碼

此時,三次握手完成,TCP 連線已經建立。輸入下面內容,並 雙擊回車,就可以得到服務端響應的內容。下面的報文中,第一行的開頭 GET 為請求訪問伺服器的型別,稱為 方法 (method);後面的 / 指明瞭請求訪問的資源物件,也叫做請求 URI (request-URI);最後為 HTTP 版本號,用來表示客戶端使用的 HTTP 版本。第二行則是請求的主機名。

GET / HTTP/1.1
Host: yanceyleo.com
複製程式碼

telnet 下的請求和響應

HTTP 是無連線、無狀態協議

HTTP 是無狀態 (stateless) 協議,它不會對請求和響應之間通訊狀態進行儲存,也就是說 HTTP 協議不會對傳送過的請求或響應做持久化處理。使用 HTTP 協議,每當有新的請求傳送時,就會有對應的新響應產生。協議本身並不保留之前一切的請求或響應報文資訊。這是為了更快地處理大量事務,確保協議的可伸縮性。

  • 無連線

    每次連線只處理一個請求,服務端處理完客戶端一次請求,等到客戶端作出回應之後便斷開連線。

  • 無狀態

    是指服務端對於客戶端每次傳送的請求都認為它是一個新的請求,上一次會話和下一次會話沒有聯絡。

cookie

cookie 原理

何為 cookie 呢?我們在上面瞭解到 HTTP 是無狀態的,但隨著 Web 的不斷髮展,這種 無狀態 的特性出現了弊端。當你登入到一家購物網站,在跳轉到該站的其他頁面時也應該繼續保持登入狀態。但是因為 HTTP 是無狀態的,所以必須得在瀏覽器端儲存一些資訊來標識當前使用者,因此 cookie 應運而生,它一種瀏覽器管理狀態的檔案。

cookie 原理

瀏覽器第一次發出請求,伺服器會將 cookie 放入到響應請求中,在瀏覽器第二次發請求的時候,會把 cookie 帶過去,於是服務端就會辨別使用者身份。注意:單個 cookie 儲存的資料不能超過 4K,很多瀏覽器都限制一個站點最多儲存 20 個 cookie。

cookie 在請求頭中有一個 cookie 欄位,在響應頭裡有一個 set-cookie 欄位。

cookie 是不可跨域的

cookie 本身就是用來儲存一些隱私性的欄位,基於安全性的考量,必須要保證它是 不可跨域的。我們可以做個實驗:先開啟 https://google.com,然後在開發者工具中輸入以下程式碼:

document.cookie = 'hello=world;path=/;domain=.baidu.com';

document.cookie = 'world=hello;path=/;domain=.google.com';
複製程式碼

開啟 Application 選項卡,在側邊欄找到 Cookies,可以發現只有 domain 為 .google.com 的被成功新增。

cookie 是不可跨域的

cookie 的屬性

我們通過一個登入的小例子來了解服務端設定 cookie。首先通過 express application generator 生成一個 Express 工程。本示例的原始碼請訪問 express-cookies

接著在 index.html 檔案中輸入以下程式碼,我們建立一個輸入使用者名稱和密碼的介面,在點選按鈕的時候,通過 fetch 將輸入的值傳送給後端。

<fieldset>
  <legend>Login</legend>
  <input id="userName" type="text" placeholder="請輸入使用者名稱" />
  <input id="userPwd" type="password" placeholder="請輸入密碼" />
  <button id="loginBtn">登入</button>
</fieldset>

<p>登入狀態: <span id="result"></span></p>
<script>
  const userName = document.getElementById('userName');
  const userPwd = document.getElementById('userPwd');
  const loginBtn = document.getElementById('loginBtn');
  const result = document.getElementById('result');

  loginBtn.addEventListener('click', function() {
    const data = {
      userName: userName.value,
      userPwd: userPwd.value
    };

    fetch('/login', {
      method: 'POST',
      headers: new Headers({
        'Content-Type': 'application/json'
      }),
      body: JSON.stringify(data)
    })
      .then(res => {
        return res.json();
      })
      .then(json => {
        result.innerHTML = json.msg;
      });
  });
</script>
複製程式碼

當使用者名稱和密碼匹配時 (假設使用者名稱和密碼都是 yancey),返回給客戶端一個 cookie 以及登入成功的 json;否則返回登入失敗的 json。下面是模擬服務端登入的介面。

router.post('/login', (req, res, next) => {
  const body = req.body;
  if (body.userName === 'yancey' && body.userPwd === 'yancey') {
    // 設定 cookie
    res.cookie('yancey', 'success');
    res.json({
      success: true,
      msg: '登入成功'
    });
  } else {
    res.status(401).json({
      success: false,
      msg: '使用者名稱或密碼錯誤'
    });
  }
});
複製程式碼

通過這個例子可以看到,在 express 中,setCookie 的方式為:第一個引數傳遞 name,第二個引數傳遞 value注意瀏覽器會將元字元和語義字元之外的字元進行轉義。開啟 Chrome 的開發者工具,就可以看到該 cookie 被新增到瀏覽器上了。或者你在控制檯輸入 document.cookie,同樣可以看到 cookie 字串。

這只是一個設定 cookie 的簡單例子,cookie 有 7 種屬性可供使用,我們一一來了解。

cookie 的屬性

domain

該屬性給 cookie 設定 域名,預設為當前網站的域名,下面的例子將 domain 設為 yanceyleo.com,由於前端頁面是 127.0.0.1,根據同源策略,該條 cookie 不會生效。

res.cookie('domain', 'domian', { domain: 'yanceyleo.com' });
複製程式碼

expires / maxAge

這兩個屬性都是設定 cookie 的 過期時間。不同的是,expires 接收一個 Date 格式的時間,而 maxAge 接收一個 毫秒時間戳。因為後者更加直觀和簡便,所以建議使用 maxAge

兩個屬性都可以傳遞一個 負值 或者 0,如果瀏覽器已存在同名 cookie,則會清除此 cookie,否則該條 cookie 不會被建立。

下面這個例子是建立一條 cookie,並將該 cookie 的過期時間設為一天後。

res.cookie('expires', 'expires', {
  expires: new Date(Date.now() + 24 * 60 * 60 * 1000)
});
複製程式碼

設定過期時間

接著給該條 cookie 設定一個 “負數”,那麼這條 cookie 就被清除了。

res.cookie('expires', 'expires', {
  expires: new Date(Date.now() - 8 * 60 * 60 * 1000)
});
複製程式碼

maxAge 的用法同理,它直接傳遞一個 過期時間 的毫秒數即可。下面的例子是將該條 cookie 的過期時間設為 7 天后。

res.cookie('maxAge', 'maxAge', {
  maxAge: 7 * 24 * 60 * 60 * 1000
});
複製程式碼

那麼不設定過期時間的 cookie 會怎樣呢?當你關閉該網站的時候,這些沒有被設定過期時間的 cookie 就死翹翹了 (這種情況的 cookie 就好像是 session)。

httpOnly

當該屬性設為 true 時,document.cookie 將無法獲取該條 cookie,但服務端可以照常獲得。該屬性可以有效的避免跨站指令碼攻擊 (XSS)。關於網路安全方面的話題,後面會專門寫一篇文章去講。

res.cookie('httpOnly', 'httpOnly', {
  // 只能被 web server 訪問到,也就是說在瀏覽器輸入 document.cookie 無法取到該條 cookie,目的是防止 xss
  httpOnly: true
});
複製程式碼

path

該屬性給 指定的路徑 新增此 cookie,預設為 /。如下程式碼就是給 users 這個路由設定 cookie (即便在服務端該路徑不存在也會被新增上)。

res.cookie('path', 'path', {
  path: '/users'
});
複製程式碼

path 屬性

secure

只有當連線是 HTTPS 協議,該 cookie 才會被新增。該屬性預設為 fasle。因為我本地的 express 是 HTTP 協議,因此該條 cookie 不會生效。

res.cookie('secure', 'secure', {
  secure: true
});
複製程式碼

signed (防篡改簽名)

該屬性是給瀏覽器傳送一個加密的 cookie,該屬性預設為 false。在 express 中,我們可以使用 cookie-parser 外掛來建立一個加密後的 cookie。服務端通過該 cookie 的內容和簽名來檢驗它是否 被篡改

首先給 cookieParser 傳入一個 secret。

app.use(cookieParser('forcabarca'));
複製程式碼

然後返回一個 sign 後的 cookie。

res.cookie('signed', 'signed', {
  signed: true
});
複製程式碼

sign 屬性

在 express 中,我們可以使用 req.cookies 來獲得 未加密 的 cookie 物件,可以通過 req.signedCookies 來獲得 已加密 的 cookie 物件。

console.log(req.cookies); // { httpOnly: 'httpOnly' }
console.log(req.signedCookies); // { signed: 'signed' }
複製程式碼

document.cookie 字串轉物件的函式

關於 cookie 就說這麼多,最後附贈一個 document.cookie 字串轉物件的函式,如果你有更好的實現方式,請在下面留言。

const formatCookie = cookies => {
  const o = {};
  cookies
    .split(';')
    .forEach(value => (o[value.split('=')[0]] = value.split('=')[1]));
  return o;
};
複製程式碼

session

session 是服務端使用的一種記錄客戶端狀態的機制,與 cookie 不同的是,session 儲存在 服務端。當客戶端初次傳送請求時 (比如登入成功),服務端會將使用者資訊以某種形式儲存在服務端,當再次訪問時只需從該 session 中找到該客戶的狀態即可。

因此,cookie 機制就是通過檢查客戶身上的 “通行證” 來確定客戶身份,而 session 則是通過檢查伺服器上的 “客戶明細表” 來確認客戶身份。session 相當於程式在伺服器上建立的一份客戶檔案,客戶來訪的時候只需要查詢客戶檔案表就可以了。

因為 HTTP 是無狀態的,所以單純的 session 仍不能判斷是否為到底是哪個使用者。因此服務端仍要向客戶端傳送一個 maxAge 為 -1 的 cookie 來作為不同使用者的唯一標識。

當然你也可以不使用 cookie,你可以通過重寫 URL 地址的方式來實現。它的原理是將使用者的 seesion id 寫入到 URL 中,當瀏覽器解析新的 URL 時就可以定位到是哪位使用者。

萬變不離其宗,兩種方式都是要保證使用者資訊以某種形式儲存到客戶端。更先進的 localStorage,sessionStorage,IndexedDB 也是同樣的道理,這裡不去細說。

HTTP 報文

用於 HTTP 協議互動的資訊被稱為 HTTP 報文。客戶端的報文叫做請求報文,服務端的報文叫做響應報文。HTTP 報文字身是有多行資料構成的字串文字。

報文格式

160c90cb78e72587.jpg

上面這張圖清晰地展示了請求報文和響應報文的格式,用文字描述大致如下。

CR (Carriage Return,回車符:16 進位制 0x0d)

LF (Line Feed,換行符:16 進位制 0x0a)

<!--請求報文-->
<method>空格<request-url>空格<version>
<headers>
空行 (CR + LF)
<entity-body>

<!--響應報文-->
<version>空格<status>空格<reason-phrase>
<headers>
空行 (CR + LF)
<entity-body>
複製程式碼

壓縮報文

HTTP 協議中有一種被稱為 內容編碼 的功能,可以有效的壓縮報文的體積。內容編碼指明應用在實體內容上的編碼格式,並保持實體資訊原樣壓縮。內容編碼後的實體由客戶端接收並負責解碼。常見的內容編碼有以下幾種:

  • identity (不做壓縮)

  • compress (UNIX 系統的標準壓縮)

  • gzip (GNU zip, 最常見)

  • deflate (zlib)

  • brotli (Google 出品,必屬精品。比 gzip 的壓縮率還要高 37%+,我的網站已使用 brotli,看下圖)

壓縮報文

分割傳送的分塊傳輸編碼

從 HTTP 請求回來,就產生了流式的資料,後續的 DOM 樹構建、CSS 計算、渲染、合成、繪製,都是儘可能地流式處理前一步的產出:即不需要等到上一步驟完全結束,就開始處理上一步的輸出,這樣我們在瀏覽網頁時,才會看到逐步出現的頁面。

本質上來說,在 HTTP 通訊過程中,請求的編碼實體資源尚未全部傳輸完成之前,瀏覽器無法顯示請求頁面。在傳輸大容量資料時,通過把資料分割成多塊,能讓瀏覽器逐步顯示頁面。這種把實體主體分塊的功能稱為分塊傳輸編碼 (Chunked Transfer Code)。

分塊傳輸編碼會將實體主體分為多個塊,每個塊都會使用十六進位制來標記大小,而實體主體的最後一塊會使用 0 (CR+LF) 來標記。使用分塊傳輸編碼的實體主體會由接收的客戶端負責解碼,恢復到編碼前的實體主體。

HTTP 報文首部

上面的章節我們說到了 HTTP 的報文,它由三部分組成,分別是:報文首部空行報文主體

對於請求報文,它的首部由方法、URL、HTTP 版本、HTTP 首部欄位等部分構成。

Jietu20190501-224131@2x.jpg

對於響應報文,它的首部分別由 HTTP 版本、狀態碼、HTTP 首部欄位等部分構成。

Jietu20190501-224131@2x.jpg

首部欄位型別

  • 通用首部欄位 (General Header Field) 請求報文和響應報文兩方都會使用的首部。

  • 請求首部欄位 (Request Header Field) 從客戶端向服務端傳送請求報文時使用的首部。補充了請求的附加內容、客戶端資訊、響應內容相關優先順序等資訊。

  • 響應首部欄位 (Response Header Field) 從服務端向客戶端返回響應報文時使用的首部。補充了響應的附加內容,也會要求客戶端附加額外的內容資訊。

  • 實體首部欄位 (Entity Header Field) 針對請求報文和響應報文的實體部分使用的首部。補充了資源內容更新時間等與實體有關的資訊。

End-to-end 首部 和 Hop-by-hop 首部

HTTP 首部欄位將定義成快取代理和非快取代理的行為,分成 端到端首部逐條首部

分到 端到端首部 的首部會轉發給請求/響應對應的最終接收目標,且必須儲存在由快取生成的響應中,並且它必須被轉發。

分到 逐跳首部 的首部只對單次轉發有效,會因通過快取或代理而不再轉發。在 HTTP/1.1 之後的版本,如果使用逐跳首部,則需要提供 Connection 首部欄位。其中 Connection、Keep-Alive、Proxy-Authenticate、、Proxy-Authorization、Trailer、TE、Transfer-Encoding、Upgrade 這 8 個為逐跳首部,其餘都為端到端首部。

通用首部欄位

Cache-Control

該欄位用於控制快取的工作機制,它接受多個引數,中間用逗號隔開。

指令 引數 型別 說明
no-cache 請求/響應都有該欄位 若請求中包含該欄位,則表示客戶端不接受快取;若服務端包含該欄位,快取前必須先確認其有效性
no-store 請求/響應都有該欄位 不快取請求或相應的任何內容。no-cache 響應實際上是可以儲存到本地快取區中的,而 no-store 才是本地徹底不快取
max-age 單位為秒,必需 請求/響應都有該欄位 當快取時間小於該值時,客戶端接受快取的資源,否則請求源伺服器,該指令的優先順序高於 Expires
max-state 單位為秒,可省略引數 只有請求擁有該欄位 只要有該欄位,客戶端就可以接受過期的快取
min-fresh 單位為秒,必需 只有請求擁有該欄位 該指令要求快取伺服器返回至少還未過指定時間的快取資源
no-transform 請求/響應都有該欄位 無論在請求還是響應中,都不允許快取改變實體主體的媒體型別
only-if-cached 只有請求擁有該欄位 表示客戶端僅在快取伺服器本地快取目標資源的情況下才會要求去返回
cache-extension - 請求/響應都有該欄位 新指令擴充套件
public 只有響應擁有該欄位 可向任意客戶端提供相應的快取
private 可省略 只有響應擁有該欄位 僅向特定使用者返回響應
must-revalidate 只有響應擁有該欄位 可快取,但必須再向源伺服器進行一次驗證
proxy-revalidate 只有響應擁有該欄位 要求中間快取伺服器對快取的響應有效性再進行確認
s-maxage 單位為秒,必需 只有響應擁有該欄位 與 max-age 相比,該指令僅適用於公共伺服器

Connection

Connection 用於控制不再轉發給代理的首部欄位,還可以管理持久連線。HTTP/1.1 預設是持久連線,當服務端明確表示斷開連線時,則將 Connection 設為 Close

Date

Date 表示建立報文的日期和時間,它的格式如下。

date: Sun, 05 May 2019 02:05:37 GMT
複製程式碼

Trailer

該欄位會事先說明在報文主體後記錄了哪些首部欄位,可應用於 HTTP/1.1 分塊傳輸編碼。

Transfer-Encoding

該欄位規定了傳輸報文主體時採用的編碼方式,HTTP/1.1 的傳輸編碼方式僅對分塊傳輸編碼有效。

Upgrade

該欄位用於檢測 HTTP 協議或者其他協議是否可以使用更高的版本通訊,該欄位要和 Connection 欄位一起使用。下面的例子是詢問是否可以使用 TLS/1.0 協議。對於附有 Upgrade 欄位的請求,服務端可返回 101 的狀態碼。

connection: upgrade
upgrade: TLS/1.0
複製程式碼

Via

該欄位用於追蹤客戶端與伺服器之間請求和響應報文的傳輸路徑。

請求首部欄位

Accept

該欄位通知伺服器,使用者代理能夠處理的媒體型別及媒體型別的相對優先順序。其中用 q 表示權重。下面的例子表示客戶端可以接受純文字型別或者 HTML 型別,並且接收純文字型別的意願 (權重)為 0.3。

Accept: text/plain; q=0.3, text/html
複製程式碼

Accept-Charset

該欄位通知伺服器,使用者代理支援的字符集及字符集的相對優先順序。該欄位應用於內容協商機制的伺服器驅動協商。如果伺服器不能提供該欄位的任何字符集,會報 406 錯誤,因此儘量不去使用該欄位 (我試驗了幾個網站,都沒有此欄位)。下面的例子表示客戶端支援 utf-8 和 iso-8859-1,且優先使用 utf-8。

Accept-Charset: utf-8, iso-8859-1;q=0.5
複製程式碼

Accept-Encoding

該欄位告知服務端,客戶端可使用的頭部壓縮演算法。上面 壓縮報文 已經介紹了幾種壓縮方式,這裡不在贅述。

Accept-Encoding: gzip, deflate, br
複製程式碼

Authorization

該欄位用於告知伺服器,使用者代理的認證資訊。下面是我部落格後臺管理系統的一個場景,在請求一個需要認證的介面時,需要在請求頭上附帶認證資訊。

Authorization: Bearer JWT_TOKEN
複製程式碼

Expect

客戶端使用 Expect 來告知伺服器,期望出現的某種特定行為。當伺服器無法理解客戶端的期望而發生錯誤時,會返回 417 狀態碼。

該欄位跟狀態碼 100 息息相關,等待狀態碼 100 響應的客戶端在發生請求時,需要指定 Expext: 100-continue。該狀態碼的用途主要是允許客戶端傳送帶請求體的請求前,判斷伺服器是否願意接收請求。

Expect: 100-continue
複製程式碼

From

該欄位用來告知伺服器使用使用者代理的使用者的 Email。

Host

當以單臺伺服器分配多個域名的虛擬主機時,Host 欄位就可以用來確定相應的主機。

Host: www.abc.com
複製程式碼

If-Match

形如 If-xxx 的請求欄位都可稱為條件請求。伺服器在收到該類請求後,只有判斷條件為真時才會執行請求。

伺服器會比對 If-Match 的欄位值和資源的 ETag 值,僅當兩者一致時,才會執行請求,否則返回 412 狀態碼。當 If-Match 的欄位值為 * 時,伺服器會忽略 ETag 值,只要資源存在就處理請求。

If-Match: W/"pqxe5g29m4"
複製程式碼

If-None-Match

與 If-Match 相反,伺服器會比對 If-None-Match 的欄位值和資源的 ETag 值,僅當兩者 不一致 時,才會執行請求。在 GET 和 HEAD 方法中使用該欄位會獲取最新資源。

If-Modified-Since

如果在 If-Modified-Since 欄位指定的日期時間後,資源發生了更新,伺服器會接受請求。如果資源沒更新過,則返回 304 狀態碼。

該欄位值和響應首部欄位的 Last-Modifie 欄位做比較,下面的例子中顯然最後修改時間要新於 If-Modified-Since 的時間,因此會響應新的資源。

// 請求首部欄位
If-Modified-Since: Fri, 01 May 2019 11:20:04 GMT

// 響應首部欄位
Last-Modified: Fri, 03 May 2019 11:20:04 GMT
複製程式碼

If-Unmodified-Since

如果在 If-Modified-Since 欄位指定的日期時間後,資源 未發生 更新,伺服器才會接受請求。如果資源在此之後發生了更新,則報 412 錯誤。

If-Range

該欄位值跟 相應頭中的 ETag 或 Date 進行比較,若一致,就作為範圍請求處理,並返回狀態碼 206,否則直接返回全部資源。

Range

對於只需獲取部分資源的範圍請求,包含首部欄位 Range 即可告知伺服器資源的指定範圍。接收到附帶 Range 欄位的請求的伺服器,會在處理請求之後返回狀態碼為 206 的響應。當無法處理該範圍請求時,返回 200 狀態碼及全部資源。

Range: bytes=5001-10000
複製程式碼

Proxy-Authorization

該欄位用於告知代理伺服器,使用者代理的認證資訊。

Referer

告知伺服器請求的 URI 是從哪兒發起的。比如在我的部落格 www.yanceyleo.com 請求了 AliOSS 上的一張圖片,那麼請求 AliOSS 伺服器的那個請求頭就會附上:

Referer: https://www.yanceyleo.com
複製程式碼

當然該單詞正確的拼寫應該是 referrer,但 referer 卻沿用至今。想起一句歌詞:“在漫天風沙裡,望著你遠去,我竟悲傷的不能自己 (已)。”

TE

該欄位會告知服務端,客戶端能夠處理響應的傳輸編碼方式及相對優先順序。它類似於 Accept-Encoding,但用於傳輸編碼。除了指定傳輸編碼,還可以指定伴隨 trailer 欄位的分塊傳輸編碼方式。

TE: gzip, delate;q=0.5

TE: trailers
複製程式碼

User-Agent

這個欄位再不認識直接回爐重造吧,這裡不去贅述,直接看例子。

User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36
複製程式碼

響應首部欄位

Accept-Ranges

該欄位用於告知客戶端,伺服器是否能處理範圍請求,可處理時指定為 bytes,否則為 none

Accept-Ranges: bytes
複製程式碼

Age

該欄位用於告知客戶端,源伺服器在多久前建立了響應,欄位值的單位為秒。若建立該響應的伺服器是快取伺服器,Age 值指的是快取後的響應再次發起認證到認證完成的時間值 (CDN)。

Age: 500
複製程式碼

ETag

ETag 是將資源以字串的形式做唯一性標識,伺服器會為每份資源分配對應的 ETag 值。當資源更新時,ETag 值也需要更新。

ETag 有 強 Etag 值弱 Etag 值 之分。前者是指無論實體發生多麼細微的變化都會改變其值。而弱 ETag 只用於提示資源是否相同。只有資源發生了根本變化,產生差異時才會改變 ETag 值,弱 ETag 欄位值前面會有 W 標識。前者就好比使用了 {deep: true} 一樣。

下面的程式碼是一張圖片的 ETag,顯然一張圖片改變意味著資源的徹底改變,因此使用了強 ETag。

ETag: "F8F155B13C6DA43723EEDE3EDBBB4D28"
複製程式碼

下面的程式碼是請求一個資料介面的 ETag,大多數情況不會發生根本性的改變,因此使用弱 ETag。

etag: W/"300af-7JrdwEcHHeXMqn/UCrMO0zsO0SY"
複製程式碼

Location

Location 欄位可以將響應接收方引導至某個與請求 URI 位置不同的資源,該欄位一般會配合 3xx 的狀態碼使用。

Location: https://yanceyleo.com
複製程式碼

Proxy-Authenticate

該欄位會把由代理伺服器所要求的認證資訊傳送給客戶端。

Retry-After

該欄位告知客戶端應該在多久之後再次傳送請求。當伺服器出錯報 503 時,如果服務端知道什麼時候可以恢復,那麼就應該通過該欄位告知客戶端。該欄位的欄位值可以是具體的日期時間,也可以是建立響應後的秒數。

Retry-After: Sat, 04 May 2019 11:26:52 GMT
複製程式碼

Server

該欄位也是一個常見欄位,用於告知客戶端,Web 伺服器的名稱。比如我使用了 cloudflare 的 CDN,因此伺服器如下所示。

server: cloudflare
複製程式碼

Vary

該欄位可用於對快取進行控制,它的欄位值接收一系列其他首部欄位名。

vary: Accept-Encoding,Cookie
複製程式碼

上面這個例子中,源伺服器向代理伺服器傳送了 vary 欄位,代理伺服器若要進行快取,只能對 Accept-Encoding 和 Cookie 進行快取。

WWW-Authenticate

該欄位告知客戶端適用於訪問請求 URI 所指定資源的認證方案和帶引數提示的質詢。

實體首部欄位

Allow

該欄位會告知客戶端所支援的所有 HTTP 請求方法,當服務端接收到不支援的 HTTP 方法時,會返回 405 狀態碼,並將所有能支援的 HTTP 方法寫入首部欄位。

Allow: GET, PUT
複製程式碼

Content-Encoding

告知客戶端伺服器使用的內容編碼方式。

content-encoding: br
複製程式碼

Content-Language

告知客戶端實體主體使用的自然語言。

content-language: zh-CN
複製程式碼

Content-Length

該欄位表明了實體主體部分的大小,單位是位元組。

Content-Length: 4871261
複製程式碼

Content-MD5

該欄位用於檢查報文主體在傳輸過程中是否保持完整性,以及確認傳輸到達。服務端對報文主體執行 MD5 演算法,獲取一個 128 位的二進位制數,再通過 base64 編碼後將結果寫入 Content-MD5 欄位值。因為 HTTP 首部無法記錄二進位制值,因此需要通過 Base64 進行處理。客戶端在接收到響應後再對報文主體執行一次相同的 MD5 演算法。將計算值於該欄位值比較,即可判斷出報文主體的準確性。

Content-MD5: +PFVsTxtpDcj7t4+27tNKA==
複製程式碼

Content-Range

該欄位告知客戶端作為響應返回的實體的哪個部分符合範圍請求,欄位值以位元組為單位。

Content-Type

非常常見的欄位,用來說明實體主體內物件的媒體型別。

content-type: application/json; charset=utf-8
複製程式碼

Expires

該欄位將資源失敗的日期告訴客戶端,在 Expires 指定的時間之前,響應的副本會一直被儲存。當超過指定的時間後,快取伺服器在請求傳送過來時,轉向源伺服器請求資源。當首部欄位 Cache-Control 有指定的 max-age 時,會優先處理 max-age。

關於快取機制下一章會詳細去講。

Last-Modified

該欄位指明資源的最終修改時間,一般來講,該值就是 Request-URI 指定資源的被修改的時間。

HTTP 方法

方法名 描述
GET GET 請求會顯示請求指定的資源。一般來說 GET 方法應該只用於資料的讀取,而不應當用於會產生副作用的非冪等的操作中。它期望的應該是而且應該是安全的和冪等的。這裡的安全指的是,請求不會影響到資源的狀態。
HEAD HEAD 方法與 GET 方法一樣,都是向伺服器發出指定資源的請求。但是,伺服器在響應 HEAD 請求時不會回傳資源的內容部分,即:響應主體。這樣,我們可以不傳輸全部內容的情況下,就可以獲取伺服器的響應頭資訊。HEAD 方法常被用於客戶端檢視伺服器的效能。
PUT PUT 請求會身向指定資源位置上傳其最新內容,PUT 方法是冪等的方法。通過該方法客戶端可以將指定資源的最新資料傳送給伺服器取代指定的資源的內容。
POST POST 請求會 向指定資源提交資料,請求伺服器進行處理,如:表單資料提交、檔案上傳等,請求資料會被包含在請求體中。POST 方法是非冪等的方法,因為這個請求可能會建立新的資源或/和修改現有資源。
TRACE TRACE 請求伺服器回顯其收到的請求資訊,該方法主要用於 HTTP 請求的測試或診斷。
OPTIONS OPTIONS 請求與 HEAD 類似,一般也是用於客戶端檢視伺服器的效能。 這個方法會請求伺服器返回該資源所支援的所有 HTTP 請求方法,該方法會用'*'來代替資源名稱,向伺服器傳送 OPTIONS 請求,可以測試伺服器功能是否正常。JavaScript 的 XMLHttpRequest 物件進行 CORS 跨域資源共享時,就是使用 OPTIONS 方法傳送嗅探請求,以判斷是否有對指定資源的訪問許可權。
DELETE DELETE 請求用於請求伺服器刪除所請求 URI(統一資源識別符號,Uniform Resource Identifier)所標識的資源。DELETE 請求後指定資源會被刪除,DELETE 方法也是冪等的。
PATCH PATCH 方法出現的較晚,它在 2010 年的 RFC 5789 標準中被定義。PATCH 請求與 PUT 請求類似,同樣用於資源的更新。二者有以下兩點不同:1.PATCH 一般用於資源的部分更新,而 PUT 一般用於資源的整體更新。2.當資源不存在時,PATCH 會建立一個新的資源,而 PUT 只會對已在資源進行更新。
CONNECT CONNECT 方法是 HTTP/1.1 協議預留的,能夠將連線改為管道方式的代理伺服器。通常用於 SSL 加密伺服器的連結與非加密的 HTTP 代理伺服器的通訊。

GET,HEAD,PUT 和 DELETE 是冪等方法,而 POST 不是冪等的。

HTTP 狀態碼

HTTP 狀態碼負責表示客戶端 HTTP 請求的返回結果、標記伺服器端的處理是否正常、通知出現的錯誤等工作。

1xx 資訊類狀態碼

狀態碼 狀態碼英文名稱 描述
100 Continue 伺服器收到請求的初始部分,請客戶端繼續。
101 Switching Protocols 伺服器根據客戶端請求切換協議

1xx 的狀態碼錶示一個臨時的響應,僅由狀態行和可選頭構成,由空行結尾。對該類狀態碼,不需要頭部。該類狀態碼在 HTTP/1.1 引入,因此伺服器禁止向 HTTP1.0 的客戶端響應 1xx 狀態碼。

對於 100 (Continue) 狀態碼,客戶端應該繼續它的請求。這個過渡的響應用於告知客戶端,請求的初始部分已經被伺服器收到,並且沒有被伺服器拒絕。客戶端應該繼續傳送剩餘的請求,如果請求已經完成,就忽略這個響應。伺服器必須在請求完成後傳送一個最終的響應。

100 狀態碼的用途主要是,允許客戶端傳送帶請求體的請求前,判斷伺服器是否願意接收請求 (通過請求頭)。在某些情況下,如果伺服器在不看請求體的情況下就拒絕請求時,客戶端仍然傳送請求體是不恰當的或低效的。

2xx 成功狀態碼

狀態碼 狀態碼英文名稱 描述
200 OK 請求成功,響應主體包含了具體的資料。最常見,一般 GET 和 POST 請求會返回此狀態碼。
201 Created 已建立,一般 PUT 請求會返回此狀態碼。
202 Accepted 伺服器已接收到請求,但還未處理完成。
203 Non-Authoritative Information 非授權資訊。請求成功,但元資訊不在原始伺服器上,而是資源的一個副本。若中間節點上有一份資源副本,但無法或沒有對它發出的與資源有關的元資訊進行驗證,就會出現這種情況。
204 No Content 響應報文中無主體部分。一般 DELETE 請求會返回此狀態碼。
205 Reset Content 負責告知瀏覽器清除當前頁面中所有 HTML 元素。
206 Partial Content 成功執行一個部分或 Range 請求。客戶端可以在首部中指定請求某個範圍內的檔案。該狀態響應頭部必須包含 Content-Range、Date、以及 ETag 或 Content-Location。

206 狀態碼一般是在下載大檔案時會遇到,它表示請求已成功,並且主體包含所請求的資料區間,該資料區間是在請求的 Range 首部指定。下圖中,我的部落格在獲取音訊檔案時返回了 206 狀態碼。

206 狀態碼

3xx 重定向狀態碼

狀態碼 狀態碼英文名稱 描述
300 Multiple Choices 客戶端請求實際指向多個資源的 URL。客戶端可以在響應中找到資源列表。
301 Moved Permanently 請求的 URL 已被移除。響應的 Location 首部包含現在所處的位置。
302 Found 與 301 類似,客戶端本次應使用響應中的臨時 URL,將來的請求任使用以前的 URL。
303 See Other 告知客戶端使用另一個 URL 來獲取資源。其主要目的是,允許 POST 請求的響應將客戶端定向的某一個資源上去。
304 Not Modified 若客戶端發起一個有條件的 GET 請求,而資源未被修改,可以使用該狀態碼說明資源未被修改。
305 Use Proxy 必須通過代理來訪問這一資源,代理有 Location 首部給出。需要知道的是,客戶端接收到這一狀態時,不應該假定所有請求都經過代理。
307 Temporary Redirect 和 302 相同。

4xx 客戶端錯誤狀態碼

狀態碼 狀態碼英文名稱 描述
400 Bad Request 告知客戶端它傳送了一個錯誤的請求。
401 Unauthorized 與適當首部一同返回,告知客戶端在請求之前先進行認證。
403 Forbidden 請求被拒絕。
404 Not Found 伺服器無法找到請求的 URL。
405 Method Not Allowed 客戶端使用不支援的方法請求 URL。應該在首部使用 Allow 告知客戶端正確的方法。
406 Not Acceptable 伺服器端無法提供與 Accept-Charset 以及 Accept-Language 訊息頭指定的值相匹配的響應
407 Proxy Authentication Required 代理伺服器要求客戶端驗證。
408 Request Timeout 客戶端完成請求時間過長,伺服器可以關閉連結。
409 Conflict 伺服器認為該請求可能引起衝突。響應主體中應包含衝突的主體的描述。
410 Gone 與 404 類似,只是伺服器曾經擁有此資源,後來被移除。
411 Length Required 伺服器要求請求報文中包含 Content-Length 首部。
412 Precondition Failed 客戶端發起條件請求,其中有條件失敗。
413 Request Entity Too LargeRequest Entity Too Large 客戶端傳送的主體部分比伺服器能夠活希望處理的要大。
414 Request URI Too Long URL 過長。
415 Unsupported Media Type 伺服器無法理解或無法支援客戶端傳送的內容型別。
416 Requested Range Not Satisfiable 請求範圍無效或無法滿足。
417 Expectation Failed 請求首部包含 Expect 期望,但伺服器無法滿足。
429 Too Many Requests 短時間內傳送了太多請求
431 Request Header Fields Too Large 請求頭太大

5xx 服務端錯誤狀態碼

狀態碼 狀態碼英文名稱 描述
500 Internal Server Error 伺服器遇到一個妨礙它提供服務的錯誤。
501 Not Implemented 客戶端發起的請求超出伺服器能力範圍,如使用了不支援的方法。
502 Bad Gateway 無效閘道器。通常不是這上游伺服器關閉,而是使用了上游伺服器不同意協議交換資料。
503 Service Unavailable 伺服器暫時無法提供服務。若伺服器知道服務什麼時間可以使用,可以在響應頭中加入 Retry-After 首部說明。
504 Gateway Timeout 於 408 類似,只是這裡的響應來自一個閘道器或代理,它們在等待另一個伺服器響應對其請求響應時超時。
505 HTTP Version Not Support 伺服器收到的請求使用了它無法支援的協議版本。

總結

這一篇文章主要探討了 HTTP 協議以及它的 無連線、無狀態 性,從而引出了 cookie 和 session。接著介紹了 HTTP 的頭部、方法、狀態碼。下一篇會著重講解 HTTP 協議的快取,敬請期待。

歡迎關注我的微信公眾號:進擊的前端

進擊的前端

參考

《圖解 HTTP》 -- 上野 宣

express 中 cookie 的使用和 cookie-parser 的解讀

談談 cookie

把 cookie 聊清楚

[讀] 這一次,讓我們再深入一點 - HTTP 報文

HTTP 狀態碼詳解之 100

你所知道的 3xx 狀態碼

相關文章