HTTP學習筆記
※,平時記錄
eTLD+1:實際指的就是一級域名。參考此文。
- a.com中com是頂級域名,a是一級域名。
- b.ac.cn中,ac.cn是eTLD(effective Top-Level Domain,有效頂級域名,即應該被視為頂級域名),b是一級域名。
教程:geektime 透視HTTP協議【此教程時間:2019年】
https://github.com/chronolaw/http_study
※,01、HTTP的前世今生
HTTP 協議始于三十年前蒂姆·伯納斯 - 李的一篇論文(1989年)
http/0.9: 20 世紀 90年代初期的互聯網世界非常簡陋,計算機處理能力低,這一時期的 HTTP 被定義為 0.9版,結構比較簡單,為了便于服務器和客戶端處理,它也采用了純文本格式。蒂姆·伯納斯 -李最初設想的系統里的文檔都是只讀的,所以只允許用“GET”動作從服務器上獲取 HTML文檔,并且在響應請求之后立即關閉連接,功能非常有限
HTTP/1.0: 1996 年,HTTP/1.0版本在 正式發布。HTTP/1.0并不是一個“標準”,只是記錄已有實踐和模式的一份參考文檔,不具有實際的約束力,相當于一個“備忘錄”。RFC編號:1945.
HTTP/1.1: 1999 年,HTTP/1.1 發布了 RFC 文檔,編號為RFC2616。它是一個“正式的標準”,而不是一份可有可無的“參考文檔”。是目前互聯網上使用最廣泛的協議,功能也非常完善。HTTP/1.1主要的變更點有:
- 增加了 PUT、DELETE 等新的方法;
- 增加了緩存管理和控制;
- 明確了連接管理,允許持久連接;
- 允許響應數據分塊(chunked),利于傳輸大文件;
- 強制要求 Host 頭,讓互聯網主機托管成為可能
2014年:由于 HTTP/1.1 太過龐大和復雜,所以在 2014年又做了一次修訂,原來的一個大文檔被拆分成了六份較小的文檔,編號為RFC7230-RFC7235,優化了一些細節,但此外沒有任何實質性的改動。
HTTP/2: 2015年:HTTP/1.1發布之后,整個互聯網世界呈現出了爆發式的增長,度過了十多年的“快樂時光”,更涌現出了Facebook、Twitter、淘寶、京東等互聯網新貴。這期間也出現了一些對 HTTP不滿的意見,主要就是連接慢,無法跟上迅猛發展的互聯網。第二次的“瀏覽器大戰”后(chrome與IE),互聯網標準化組織以Google 的SPDY協議為基礎開始制定新版本的 HTTP協議,最終在 2015 年發布了HTTP/2,RFC 編號 RFC7540。
HTTP/2的制定充分考慮了現今互聯網的現狀:寬帶、移動、不安全,在高度兼容 HTTP/1.1的同時在性能改善方面做了很大努力,主要的特點有:
- 二進制協議,不再是純文本;
- 可發起多個請求(多路復用能力),廢棄了 1.1 里的管道;
- 使用專用算法壓縮頭部,減少數據傳輸量;
- 允許服務器主動向客戶端推送數據;
- 增強了安全性,“事實上”要求加密通信。
雖然 HTTP/2 到今天已經四歲,也衍生出了 gRPC 等新協議,但由于 HTTP/1.1實在是太過經典和強勢,目前它的普及率還比較低,大多數網站使用的仍然還是 20 年前的 HTTP/1.1。
HTTP/3: 2018: 在 HTTP/2 還處于草案之時,Google又發明了一個新的協議,叫做QUIC。2018 年,互聯網標準化組織 IETF提議將“HTTP over QUIC”更名為“HTTP/3”并獲得批準,HTTP/3正式進入了標準化制訂階段。
※,與HTTP相關的概念/協議
代理(Proxy)是 HTTP協議中請求方和應答方中間的一個環節,作為“中轉站”,既可以轉發客戶端的請求,也可以轉發服務器的應答。代理有很多的種類,常見的有:
- 匿名代理:完全“隱匿”了被代理的機器,外界看到的只是代理服務器;
- 透明代理:顧名思義,它在傳輸過程中是“透明開放”的,外界既知道代理,也知道客戶端;
- 正向代理:靠近客戶端,代表客戶端向服務器發送請求;
- 反向代理:靠近服務器端,代表服務器響應客戶端的請求;
上一講提到的CDN,實際上就是一種代理,它代替源站服務器響應客戶端的請求,通常扮演著透明代理和反向代理的角色。由于代理在傳輸過程中插入了一個“中間層”,所以可以在這個環節做很多有意思的事情,比如:
- 負載均衡:把訪問請求均勻分散到多臺機器,
- 實現訪問集群化;
- 內容緩存:暫存上下行的數據,減輕后端的壓力;
- 安全防護:隱匿 IP, 使用 WAF等工具抵御網絡攻擊,保護被代理的機器;
- 數據處理:提供壓縮、加密等額外的功能。
- 關于 HTTP 的代理還有一個特殊的“代理協議”(proxy protocol),它由知名的代理軟件 HAProxy 制訂,但并不是RFC 標準,我也會在之后的課程里專門講解.
MAC 層的傳輸單位是幀(frame),IP層的傳輸單位是包(packet),TCP層的傳輸單位是段(segment),HTTP的傳輸單位則是消息或報文(message)。但這些名詞并沒有什么本質的區分,可以統稱為數據包。
※,DNS
★,DNS的核心系統是一個三層的樹狀、分布式服務,基本對應域名的結構:
- 根域名服務器(Root DNS Server):管理頂級域名服務器,返回“com”“net”“cn”等頂級域名服務器的 IP 地址;【根域名服務器告訴請求方com等這種頂級域名在哪個服務器上】
- 頂級域名服務器(Top-level DNS Server):管理各自域名下的權威域名服務器,比如 com 頂級域名服務器可以返回 apple.com 域名服務器的 IP地址;
- 權威域名服務器(Authoritative DNS Server):管理自己域名下主機的 IP 地址,比如 apple.com權威域名服務器可以返回 www.apple.com 的 IP 地址。
★,在核心 DNS 系統之外,還有兩種手段用來減輕域名解析的壓力
- ★,許多大公司、網絡運行商都會建立自己的 DNS服務器,作為用戶 DNS 查詢的代理,代替用戶訪問核心 DNS系統。這些“野生”服務器被稱為“非權威域名服務器”,可以緩存之前的查詢結果,如果已經有了記錄,就無需再向根服務器發起查詢,直接返回對應的 IP 地址。比較知名的 DNS
-
- Google 的“8.8.8.8”
- Microsoft 的“4.2.2.1”
- CloudFlare的“1.1.1.1”等等。
- ★,操作系統里也會對 DNS 解析結果做緩存。另外,操作系統里還有一個特殊的“主機映射”文件,通常是一個可編輯的文本,在 Linux 里是“/etc/hosts”,在Windows里是C:\WINDOWS\system32\drivers\etc\hosts”,
如果操作系統在緩存里找不到 DNS 記錄,就會找這個文件。
★,可以使用 bind9等開源軟件搭建一個在內部使用的DNS,作為名字服務器。這樣我們開發的各種內部服務就都用域名來標記。
★,基于域名實現負載均衡:域名解析可以返回多個 IP地址,所以一個域名可以對應多臺主機。有兩種方式實現負載均衡,兩種方式可以混用
- 客戶端收到多個 IP地址后,就可以自己使用輪詢算法依次向服務器發起請求,實現負載均衡。
- 域名解析可以配置內部的策略,返回離客戶端最近的主機,或者返回當前服務質量最好的主機,這樣在 DNS 端把請求分發到不同的服務器,實現負載均衡。
★,
※,自己動手,搭建HTTP實驗環境
★,“最小化”環境用到的應用軟件:Wireshark、Chrome/Firefox、Telnet、OpenResty
Telnet使用方法:
Telnet是一個經典的虛擬終端,基于 TCP協議遠程登錄主機,我們可以使用它來模擬瀏覽器的行為,連接服務器后手動發送 HTTP請求,把瀏覽器的干擾也徹底排除,能夠從最原始的層面去研究 HTTP 協議。telnet使用方法(比較原始):
- `telnet localhost 80` //連接web服務器
- 連接上之后按組合鍵·ctrl+]·
- 然后按回車鍵,此時就進入了編輯模式
- 在編輯模式中可以手動輸入HTTP請求(包括請求行、請求頭、請求體等),也可以使用鼠標右鍵粘貼文本 // 注:親測可用,如果不可用,可能是粘貼的文本中含有一些特殊字符!!!
- 這里注意!!!:如果輸入了隨意的字符(非HTTP請求格式),Telnet會直接報錯400 Bad Request,然后斷開連接(遺失對主機的連接)。POST請求中的Content-Length如果設置的值比實際輸入的body短,那么就會出現這種情況。
- 然后按兩下回車就會發送數據,也就是模擬了一次HTTP請求!
Telnet示例:
-
-- 正常GET請求 GET /09-1 HTTP/1.1 Host: www.chrono.com -- 格式異常的GET請求,到的會是一個“400 BadRequest”,表示請求報文格式有誤,服務器無法正確處理 GET /09-1 HTTP/1.1 Host : www.chrono.com -- POST請求。 POST /10-2 HTTP/1.1 Host: www.chrono.com Content-Length: 3 abc // 注意:發送POST請求時,Content-Length(即空行后的body長度,如abc長度為3)一定要設置正確。如果不正確會出現兩種情況: // 1. Content-Length設置的值A小于實際輸入的body字符串的長度B:此時Telnet只會讀取B中前A個字符,剩下的字符就相當于下一次Telnet的輸入, // 當Telnet中發現輸入的是隨意字符時(非HTTP請求格式)就會報錯400 Bad Request,然后斷開與服務器的連接。 // 2. Content-Length設置的值A大于實際輸入的body字符串的長度B:此時Telnet會繼續等待輸入,直至輸入的字符串的實際長度等于A
★,Telnet也可以用nc命令或socat命令代替!參考另一篇博文。
※,HTTP協議報文解析
★,HTTP 報文結構就像是“大頭兒子”,由“起始行 + 頭部 + 空行 + 實體”組成,簡單地說就是“header+body”;HTTP 報文可以沒有 body,但必須要有 header,而且header后也必須要有空行(就像人的脖子),形象地說就是“大頭”必須要帶著“脖子”。

- 請求頭:由“請求行 + 頭部字段”構成,響應頭由“狀態行 + 頭部字段”構成;
- 請求行有三部分:請求方法,請求目標和版本號;
- 狀態行也有三部分:版本號,狀態碼和原因字符串;
- 頭部字段是 key-value的形式,用“:”分隔,不區分大小寫,順序任意,除了規定的標準頭,也可以任意添加自定義字段,實現功能擴展
★,頭字段格式需要注意下面幾點:
- 字段名不區分大小寫,例如“Host”也可以寫成“host”,但首字母大寫的可讀性更好;
- 字段名里不允許出現空格,可以使用連字符“-”,但不能使用下劃線“_”。例如,“test-name”是合法的字段名,而“test name”“test_name”是不正確的字段名;
- 字段名后面必須緊接著“:”,不能有空格,而“:”后的字段值前可以有多個空格;
- 字段的順序是沒有意義的,可以任意排列不影響語義;
- 字段原則上不能重復,除非這個字段本身的語義允許,例如Set-Cookie。
★,一次HTTP請求抓包分析

- 前三個包為TCP三次握手
- 第四個包(No.7)為瀏覽器向服務端發送的HTTP請求(此次HTTP請求中包含了TCP傳輸,即基于TCP協議)
- 第五個包(No.8)為服務端在TCP協議層面向瀏覽器發送的ACK包,告訴瀏覽器:"剛才的報文我已經收到了",不過這個TCP包HTTP協議是看不到的
- 第六個包(No.9)為服務端發送給瀏覽器的HTTP包,底層走的還是TCP協議。
- 第七個包(No.10)為瀏覽器回復給服務器的一個TCP的ACK確認:"你的響應報文收到了,多謝!"
- TCP關閉連接的“四次揮手”在抓包里沒有出現,這是因為 HTTP/1.1總是默認啟用keepalive長連接機制,默認不會立即關閉連接。

★,【擴展】TCP三次握手和四次揮手以及相關的狀態:

四次揮手:四次揮手即終止TCP連接,就是指斷開一個TCP連接時,需要客戶端和服務端總共發送4個包以確認連接的斷開。在socket編程中,這一過程由客戶端或服務端任一方執行close來觸發。由于TCP連接是全雙工的,因此,每個方向都必須要單獨進行關閉,這一原則是當一方完成數據發送任務后,發送一個FIN來終止這一方向的連接,收到一個FIN只是意味著這一方向上沒有數據流動了,即不會再收到數據了,但是在這個TCP連接上仍然能夠發送數據,直到這一方向也發送了FIN。首先進行關閉的一方將執行主動關閉,而另一方則執行被動關閉。

為什么連接的時候是三次握手,關閉的時候卻是四次握手?
建立連接時因為當Server端收到Client端的SYN連接請求報文后,可以直接發送SYN+ACK報文。其中ACK報文是用來應答的,SYN報文是用來同步的。所以建立連接只需要三次握手。
由于TCP協議是一種面向連接的、可靠的、基于字節流的運輸層通信協議,TCP是全雙工模式。這就意味著,關閉連接時,當Client端發出FIN報文段時,只是表示Client端告訴Server端數據已經發送完畢了。當Server端收到FIN報文并返回ACK報文段,表示它已經知道Client端沒有數據發送了,但是Server端還是可以發送數據到Client端的,所以Server很可能并不會立即關閉SOCKET,直到Server端把數據也發送完畢。當Server端也發送了FIN報文段時,這個時候就表示Server端也沒有數據要發送了,就會告訴Client端,我也沒有數據要發送了,之后彼此就會愉快的中斷這次TCP連接。
★,常用頭字段:
協議規定了非常多的頭部字段,實現各種各樣的功能,但基本上可以分為四大類:
- 通用字段:在請求頭和響應頭里都可以出現;
- 請求字段:僅能出現在請求頭里,進一步說明請求信息或者額外的附加條件;
- 響應字段:僅能出現在響應頭里,補充說明響應報文的信息;
- 實體字段:它實際上屬于通用字段,但專門描述 body的額外信息。
Host字段:
- 屬于請求字段,只能出現在請求頭里.
- HTTP/1.0不支持Host請求頭(HTTP/1.0的所有請求頭都是可選的)。而在HTTP/1.1中,Host請求頭部必須存在(HTTP/1.1中唯一要求必須存在的請求頭),否則會返回400 Bad Request。
- Host字段告訴服務器這個請求應該由哪個主機來處理,當一臺計算機上托管了多個虛擬主機的時候,服務器端就需要用 Host字段來選擇使用哪個虛擬主機。
User-Agent:是請求字段,只出現在請求頭里。
Date字段:是一個通用字段,但通常出現在響應頭里,表示HTTP報文創建的時間,客戶端可以使用這個時間再搭配其他字段決定緩存策略。
Server字段:是響應字段,只能出現在響應頭里。它告訴客戶端當前正在提供 Web服務的軟件名稱和版本號,Server字段也不是必須要出現的,因為這會把服務器的一部分信息暴露給外界,如果這個版本恰好存在 bug,那么黑客就有可能利用 bug攻陷服務器。所以,有的網站響應頭里要么沒有這個字段,要么就給出一個完全無關的描述信息。比如 GitHub,它的 Server 字段里就看不出是使用了 Apache還是 Nginx,只是顯示為“GitHub.com”。
Content-Length字段:屬于實體字段(通用字段)。表示報文里body的長度(以字節為單位的十進制數),也就是請求頭或響應頭空行后面數據的長度。服務器看到這個字段,就知道了后續有多少數據,可以直接接收。如果沒有這個字段,那么body就是不定長的,需要使用chunked方(Transfer-Encoding:chunked)式分段傳輸。
- Content-Length如果存在并且有效地話,則必須和消息內容的傳輸長度完全一致,否則就會導致異常。
- 這個大小是包含了所有內容編碼的, 比如,對文本文件進行了
gzip壓縮的話,Content-Length首部指的就是壓縮后的大小而不是原始大小。 - GET請求報文一般不攜帶body實體,所以不會有Content-Length,但是GET請求的響應報文中包含實體,需要有Content-Length(非分段傳輸場景)。
- POST請求報文和響應報文都需要有Content-Length(非分段傳輸場景)。
★,理解請求頭里的請求方法:
請求頭的實際含義是:戶端發出了一個“動作指令”,要求服務器端對 URI 定位的資源執行這個動作。服務器掌控著所有資源,也就有絕對的決策權力。它收到HTTP請求報文后,看到里面的請求方法,可以執行也可以拒絕,或者改變動作的含義。比如,你發起了一個 GET請求,想獲取“/orders”這個文件,但這個文件保密級別比較高,不是誰都能看的,服務器就可以有如下的幾種響應方式:
- 假裝這個文件不存在,直接返回一個 404 Not found 報文;
- 稍微友好一點,明確告訴你有這個文件,但不允許訪問,返回一個 403 Forbidden;
- 再寬松一些,返回 405 Method Not Allowed,然后用 Allow頭告訴你可以用 HEAD 方法獲取文件的元信息。【注:405表示請求方法不被允許!】
目前 HTTP/1.1 規定了八種方法,單詞都必須是大寫的形式:
- GET:獲取資源,可以理解為讀取或者下載數據;
- HEAD:獲取資源的元信息;
- POST:向資源提交數據,相當于寫入或上傳數據;
- PUT:類似 POST;
- DELETE:刪除資源;
- CONNECT:建立特殊的連接隧道;
- OPTIONS:列出可對資源實行的方法;
- TRACE:追蹤請求 - 響應的傳輸路徑。
GET:
- GET 方法雖然基本動作比較簡單,但搭配 URI和其他頭字段就能實現對資源更精細的操作。例如,在 URI后使用“#”,就可以在獲取頁面后直接定位到某個標簽所在的位置;使用If-Modified-Since字段就變成了“有條件的請求”,僅當資源被修改時才會執行獲取動作;使用 Range字段就是“范圍請求”,只獲取資源的一部分數據。
HEAD:
- HEAD 方法可以看做是 GET方法的一個“簡化版”或者“輕量版”。因為它的響應頭與 GET完全相同,所以可以用在很多并不真正需要資源的場合。比如想要檢查一個文件是否存在,再比如,要檢查文件是否有最新版本,
同樣也應該用HEAD,服務器會在響應頭里把文件的修改時間傳回來
POST/PUT:
PUT 的作用與 POST 類似,也可以向服務器提交數據,但與POST 存在微妙的不同,通常 POST表示的是“新建”“create”的含義,而 PUT則是“修改”“update”的含義。在實際應用中,PUT 用到的比較少。而且,因為它與 POST的語義、功能太過近似,有的服務器甚至就直接禁止使用 PUT方法,只用 POST 方法上傳數據。
DELETE:
DELETE方法指示服務器刪除資源,因為這個動作危險性太大,所以通常服務器不會執行真正的刪除操作,而是對資源做一個刪除標記。當然,更多的時候服務器就直接不處理 DELETE 請求
CONNECT:
CONNECT是一個比較特殊的方法,要求服務器為客戶端和另一臺遠程服務器建立一條特殊的連接隧道,這時 Web 服務器在中間充當了代理的角色
OPTIONS:
OPTIONS方法要求服務器列出可對資源實行的操作方法,在響應頭的Allow字段里返回。它的功能很有限,用處也不大,有的服務器(例如 Nginx)干脆就沒有實現對它的支持,但nginx可以使用配置指令、自定義模塊或Lua腳本實現。
TRACE:
TRACE方法多用于對 HTTP鏈路的測試或診斷,可以顯示出請求 -響應的傳輸路徑。它的本意是好的,但存在漏洞,會泄漏網站的信息,所以 Web 服務器通常也是禁止使用。
請求方法的擴展方法:
雖然 HTTP/1.1里規定了八種請求方法,但它并沒有限制我們只能用這八種方法,這也體現了 HTTP協議良好的擴展性,我們可以任意添加請求動作,只要請求方和響應方都能理解就行。
例如著名的愚人節玩笑 RFC2324,它定義了協議HTCPCP,即“超文本咖啡壺控制協議”,為 HTTP協議增加了用來煮咖啡的 BREW 方法,要求添牛奶的 WHEN方法。此外,還有一些得到了實際應用的請求方法(WebDAV),例如 MKCOL、COPY、MOVE、LOCK、UNLOCK、PATCH等。如果有合適的場景,你也可以把它們應用到自己的系統里,比如用 LOCK 方法鎖定資源暫時不允許修改,或者使用PATCH方法給資源打個小補丁,部分更新數據。但因為這些方法是非標準的,所以需要為客戶端和服務器編寫額外的代碼才能添加支持。當然了,你也完全可以根據實際需求,自己發明新的方法,比如“PULL”拉取某些資源到本地,“PURGE”清理某個目錄下的所有緩存數據。
請求方法的安全與冪等:
“安全”與“冪等”是描述請求方法的兩個重要屬性,具有理論指導意義,可以幫助我們設計系統。
謂的“安全”是指請求方法不會“破壞”服務器上的資源,即不會對服務器上的資源造成實質的修改。按照這個定義,只有 GET 和 HEAD方法是“安全”的,因為它們是“只讀”操作,只要服務器不故意曲解請求方法的處理方式,無論 GET 和HEAD 操作多少次,服務器上的數據都是“安全的”。而 POST/PUT/DELETE操作會修改服務器上的資源,增加或刪除數據,所以是“不安全”的。
“冪等”實際上是一個數學用語,被借用到了 HTTP協議里,意思是多次執行相同的操作,結果也都是相同的,即多次“冪”后結果“相等”。GET 和 HEAD 既是安全的也是冪等的,DELETE可以多次刪除同一個資源,效果都是“資源不存在”,所以也是冪等的。POST 和 PUT 的冪等性質就略費解一點。按照 RFC 里的語義,POST是“新增或提交數據”,多次提交數據會創建多個資源,所以不是冪等的;而 PUT是“替換或更新數據”,多次更新一個資源,資源還是會第一次更新的狀態,所以是冪等的。我對你的建議是,你可以對比一下 SQL 來加深理解:把POST 理解成 INSERT,把 PUT 理解成UPDATE,這樣就很清楚了。多次 INSERT會添加多條記錄,而多次 UPDATE只操作一條記錄,而且效果相同。
※,11 URI/URL
URI的完整形式:其中`host:port`部分稱為authority,path必須是以反斜杠`/`開頭。

示例:
·file:///D:/http_study/www/· //這里file是scheme,·://·是分隔符,屬于固定格式,·/D:/http_study/www/·是path,中間的authority(host:path)被省略了,這是file類型URI的特例,它允許省略主機名,默認是本機localhost
★,URI的編碼
剛才我們看到了,在 URI 里只能使用 ASCII 碼,但如果要在URI 里使用英語以外的漢語、日語等其他語言該怎么辦呢?還有,某些特殊的 URI,會在 path、query里出現“@&?"等起界定符作用的字符,會導致 URI解析錯誤,這時又該怎么辦呢?所以URI 引入了編碼機制,對于 ASCII碼以外的字符集和特殊字符做一個特殊的操作,把它們轉換成與 URI 語義不沖突的形式。這在 RFC規范里稱為“escape”和“unescape”,俗稱“轉義”。URI 轉義的規則有點“簡單粗暴”,直接把非 ASCII碼或特殊字符轉換成十六進制字節值,然后前面再加上一個“%”。例如,空格被轉義成“%20”,“?”被轉義成“%3F”。而中文、日文等則通常使用 UTF-8編碼后再轉義,例如“銀河”會被轉義成“%E9%93%B6%E6%B2%B3”。
※,12 響應狀態碼
狀態行的結構,有三部分
?
★,RFC標準將狀態碼分成了五類:
- 1××:提示信息,表示目前是協議處理的中間狀態,還需要后續的操作;
- 101 Switching Protocols”。它的意思是客戶端使用 Upgrade 頭字段,要求在 HTTP協議的基礎上改成其他的協議繼續通信,比如WebSocket。而如果服務器也同意變更協議,就會發送狀態碼101,但這之后的數據傳輸就不會再使用 HTTP 了
- 2××:成功,報文已經收到并被正確處理;
- “200 OK”是最常見的成功狀態碼,表示一切正常。服務器如客戶端所期望的那樣返回了處理結果,如果是非HEAD 請求,通常在響應頭后都會有 body 數據
- “204 No Content”是另一個很常見的成功狀態碼,它的含義與“200OK”基本相同,但響應頭后沒有 body 數據。
- “206 Partial Content”是 HTTP分塊下載或斷點續傳的基礎,在客戶端發送“范圍請求”、要求獲取資源的部分數據時出現,它與 200一樣,也是服務器成功處理了請求,但 body里的數據不是資源的全部,而是其中的一部分。狀態碼 206 通常還會伴隨著頭字段“Content-Range”,表示響應報文里 body數據的具體范圍,供客戶端確認,例如“Content-Range: bytes 0-99/2000”,意思是此次獲取的是總計 2000 個字節的前 100
個字節。
- 3××:重定向,資源位置發生變動,需要客戶端重新發送請求;
- “301 Moved Permanently”俗稱“永久重定向”,含義是此次請求的資源已經不存在了,需要改用改用新的 URI 再次訪問。
- “302 Found”,曾經的描述短語是“Moved Temporarily”,俗稱“臨時重定向”,意思是請求的資源還在,但需要暫時用另一個 URI 來訪問。
- 301 和 302 都會在響應頭里使用字段Location指明后續要跳轉的URI。301和302還有兩個等價的狀態碼:“308 Permanent Redirect”和“307 Temporary Redirect”,但這兩個狀態碼不允許后續的請求更改請求方法。
- “304 Not Modified” 是一個比較有意思的狀態碼,它用于If-Modified-Since等條件請求,表示資源未修改,用于緩存控制。它不具有通常的跳轉含義,但可以理解成“重定向已到緩存的文件”(即“緩存重定向”)。
- 4××:客戶端錯誤,請求報文有誤,服務器無法處理;
- “400 Bad Request”是一個通用的錯誤碼,表示請求報文有錯誤,但具體是數據格式錯誤、缺少請求頭還是 URI超長它沒有明確說,只是一個籠統的錯誤,客戶端看到 400只會是“一頭霧水”“不知所措”。所以,在開發 Web應用時應當盡量避免給客戶端返回400,而是要用其他更有明確含義的狀態碼。
- “403 Forbidden”實際上不是客戶端的請求出錯,而是表示服務器禁止訪問資源。原因可能多種多樣,例如信息敏感、法律禁止等
- “404 Not Found”可能是我們最常看見也是最不愿意看到的一個狀態碼,它的原意是資源在本服務器上未找到,所以無法提供給客戶端。但現在已經被“用濫了”,只要服務器“不高興”就可以給出個404,而我們也無從得知后面到底是真的未找到,還是有什么別的原因
- 405 Method NotAllowed:不允許使用某些方法操作資源,例如不允許 POST只能 GET;
- 406 Not Acceptable:資源無法滿足客戶端請求的條件,例如請求中文但只有英文;
- 408 Request Timeout:請求超時,服務器等待了過長的時間;
- 409 Conflict:多個請求發生了沖突,可以理解為多線程并發時的競態;
- 413 Request Entity Too Large:請求報文里的 body 太大;
- 414 Request-URI Too Long:請求行里的 URI 太大;
- 429 Too Many Requests:客戶端發送了太多的請求,通常是由于服務器的限連策略;
- 431 Request Header Fields Too Large:請求頭某個字段或總體太大;
- 5××:表示客戶端請求報文正確,但服務器在處理時內部發生了錯誤,無法返回應有的響應數據,是服務器端的“錯誤碼”。
- “500 Internal Server Error”與 400類似,也是一個通用的錯誤碼,服務器究竟發生了什么錯誤我們是不知道的。不過對于服務器來說這應該算是好事,通常不應該把服務器內部的詳細信息,例如出錯的函數調用棧告訴外界。雖然不利于調試,但能夠防止黑客的窺探或者分析。
- “501 Not Implemented”表示客戶端請求的功能還不支持
- “502 Bad Gateway”通常是服務器作為網關或者代理時返回的錯誤碼,表示服務器自身工作正常,訪問后端服務器【自注:作為代理的服務器需要訪問的上游服務器】時發生了錯誤,但具體的錯誤原因也是不知道的。
- “503 Service Unavailable”表示服務器當前很忙,暫時無法響應服務,我們上網時有時候遇到的“網絡服務正忙,請稍后重試”的提示信息就是狀態碼 503。503是一個“臨時”的狀態,很可能過幾秒鐘后服務器就不那么忙了,可以繼續提供服務,所以 503 響應報文里通常還會有一個“Retry-After”字段,指示客戶端可以在多久以后再次嘗試發送請求
目前 RFC 標準里總共有 41個狀態碼,但狀態碼的定義是開放的,允許自行擴展。所以Apache、Nginx 等 Web服務器都定義了一些專有的狀態碼。如果你自己開發 Web應用,也完全可以在不沖突的前提下定義新的代碼。
★,
※,13 HTTP有哪些特點
- 靈活可擴展:可以任意添加頭字段實現任意功能
- 可靠傳輸:HTTP 是可靠傳輸協議,基于 TCP/IP協議“盡量”保證數據的送達;我們必須正確地理解“可靠”的含義,HTTP 并不能 100%保證數據一定能夠發送到另一端,在網絡繁忙、連接質量差等惡劣的環境下,也有可能收發失敗。“可靠”只是向使用者提供了一個“承諾”,會在下層用多種手段“盡量”保證數據的完整送達。
- 如果要100%保證數據收發成功就不能使用HTTP或者TCP協議了,而是要用各種消息中間件(MQ),如RabbitMQ、RocketMQ、kafka等。
- 【參考文章:為什么消息中間件不直接使用HTTP協議】
-
這個問題問得很準確,并不是說所有消息中間件一定不使用 http 協議,而是“不直接”使用 http 協議。以 RocketMQ 為例,使用的是 grpc 協議,本質是 protobuf(編碼協議) + http2.0(傳輸協議),底層其實使用了 http 協議。當然還有一些消息中間件,是完全沒有使用 http 協議,比如 kafka,直接使用了 TCP 協議。消息中間件關注的一個核心點是吞吐量,所以對性能的要求比較高,而 http 協議,特別是 http1.x 版的協議,不具備多路復用的能力(http 2.0 具備),且消息頭和消息體都比較大(http2.0 會做頭部壓縮,protobuf 則直接對消息做了編碼壓縮)。此外,在消息傳輸過程中,我們更看重數據的壓縮率,網絡連接的復用情況,以及特定的優化手段。而 http 協議作為應用層協議,包含很多 MQ 場景可能并不需要的功能,但這些功能(比如額外的頭部信息)可能會帶來額外的開銷;且作為應用層協議,http 缺乏一定的靈活性(越底層越靈活)。綜上,http 協議不太適合用來直接作為消息中間件的通信協議。
-
- 應用層協議:HTTP 是應用層協議,比 FTP、SSH等更通用功能更多,能夠傳輸任意數據
- 請求-應答:HTTP 使用了請求 - 應答模式,客戶端主動發起請求,服務器被動回復請求;
- 無狀態:
- TCP協議有狀態,ESTABLISHED,CLOSED等狀態。
- udp協議無連接無狀態,順序發包亂序收包,數據包發出去后就不管了,收到后也不會順序整理。
- HTTP是有連接無狀態,順序發包順序收包,按照收發的順序管理報文。HTTP的無狀態即每個請求都是互相獨立、毫無關聯的,協議不要求客戶端或服務器記錄請求相關的信息。
- 無狀態”對于 HTTP來說既是優點也是缺點。“無狀態”有什么好處呢?因為服務器沒有“記憶能力”,所以就不需要額外的資源來記錄狀態信息,不僅實現上會簡單一些,而且還能減輕服務器的負擔,能夠把更多的 CPU 和內存用來對外提供服務。而且,“無狀態”也表示服務器都是相同的,沒有“狀態”的差異,所以可以很容易地組成集群,讓負載均衡把請求轉發到任意一臺服務器,不會因為狀態不一致導致處理出錯,使用“堆機器”的“笨辦法”輕松實現高并發高可用。
那么,“無狀態”又有什么壞處呢?
既然服務器沒有“記憶能力”,它就無法支持需要連續多個步驟的“事務”操作。例如電商購物,首先要登錄,然后添加購物車,再下單、結算、支付,這一系列操作都需要知道用戶的身份才行,但“無狀態”服務器是不知道這些請求是相互關聯的,每次都得問一遍身份信息,不僅麻煩,而且還增加了不必要的數據傳輸量。
所以,HTTP協議最好是既“無狀態”又“有狀態”,不過還真有“魚和熊掌”兩者兼得這樣的好事,這就是“小甜餅”Cookie 技術
★,
※,15 | 海納百川:HTTP的實體數據
在 TCP/IP協議棧里,傳輸數據基本上都是“header+body”的格式。但TCP、UDP 因為是傳輸層的協議,它們不會關心 body數據是什么(但是關心header里的數據,TCP、udp協議里的header已經被規定了固定的格式,接收到之后按照規定好的格式解析即可。HTTP的body沒有規定固定的格式,所以需要通過頭字段讓 客戶端和服務器進行“內容協商”),只要把數據發送到對方就算是完成了任務。而 HTTP協議則不同,它是應用層的協議,數據到達之后工作只能說是完成了一半,還必須要告訴上層應用這是什么數據才行。
★,數據類型與編碼:解決計算機理解 body數據的問題
- 客戶端用 ·Accept·頭告訴服務器希望接收什么樣的數據,服務器用·Content-Type·頭告訴客戶端實際發送了什么樣的數據。
- Accept: text/html,application/xml,image/webp,image/png
- Content-Type: text/html
- ·Accept-Encoding·字段標記的是客戶端支持的壓縮格式,服務器實際使用的壓縮格式放在響應頭字段·Content-Encoding·里。這兩個字段是可以省略的,如果請求報文里沒有Accept-Encoding字段,就表示客戶端不支持壓縮數據;如果響應報文里沒有Content-Encoding 字段,就表示響應數據沒有被壓縮。
- Accept-Encoding: gzip, deflate, br
- Content-Encoding: gzip
★,語言類型與編碼:解決“國際化”問題
·Accept-Language·字段標記了客戶端可理解的自然語言,也允許用“,”;服務器應該在響應報文里用頭字段·Content-Language·告訴客戶端實體數據使用的實際語言類型。
- Accept-Language: zh-CN, zh, en
- Content-Language: zh-CN
編碼字符集在 HTTP 里使用的請求頭字段是Accept-Charset,但響應頭里卻沒有對應的 Content-Charset,而是在Content-Type字段的數據類型后面用“charset=xxx”來表示。
- Accept-Charset: gbk, utf-8
- Content-Type: text/html; charset=utf-8
現在的瀏覽器都支持多種字符集,通常不會發送Accept-Charset,而服務器也不會發送Content-Language,因為使用的語言完全可以由字符集推斷出來,所以在請求頭里一般只會有 Accept-Language字段,響應頭里只會有 Content-Type 字段。
★,內容協商的質量值
在 HTTP 協議里用 Accept、Accept-Encoding、Accept-Language等請求頭字段進行內容協商的時候,還可以用一種特殊的“q”參數表示權重來設定優先級,這里的“q”是“qualityfactor”的意思。權重的最大值是 1,最小值是 0.01,默認值是 1,如果值是 0就表示拒絕。具體的形式是在數據類型或語言代碼后面加一個“;”,然后是“q=value”。這里要提醒的是“;”的用法,大多數編程語言里“;”的斷句語氣要強于“,”,而在 HTTP的內容協商里卻恰好反了過來,“;”的意義是小于“,”的。例如下面的 Accept 字段:Accept: text/html,application/xml;q=0.9,*/*;q=0.8它表示瀏覽器最希望使用的是 HTML 文件,權重是1,其次是 XML 文件,權重是0.9,最后是任意數據類型,權重是0.8。
★,內容協商的結果
有的時候,服務器會在響應頭里多加一個Vary字段,記錄服務器在內容協商時參考的請求頭字段,給出一點信息,例如:Vary: Accept-Encoding,User-Agent,Accept這個 Vary 字段表示服務器依據了Accept-Encoding、User-Agent 和 Accept這三個頭字段,然后決定了發回的響應報文。
★,總結

※,16 |把大象裝進冰箱:HTTP傳輸大文件的方法
★,數據壓縮
★,分塊傳輸: 【注:傳輸的是整個大文件,區分與多段數據的傳輸格式】
分塊傳輸在響應報文里用頭字段“Transfer-Encoding:chunked”來表示,意思是報文里的 body部分不是一次性發過來的,而是分成了許多的塊(chunk)逐個發送。
分塊傳輸時 body數據的長度是未知的,無法在頭字段“Content-Length”里給出確切的長度,所以也只能用 chunked 方式分塊發送。“Transfer-Encoding: chunked”和“Content-Length”這兩個字段是互斥的,也就是說響應報文里這兩個字段不能同時出現,一個響應報文的傳輸要么是長度已知,要么是長度未知(hunked),這一點你一定要記住。
分塊傳輸的編碼規則:采用了明文的方式,很類似響應頭。每個分塊包含兩個部分,長度頭和數據塊;長度頭是以CRLF(回車換行,即\r\n)結尾的一行明文,用 16進制數字表示長度;數據塊緊跟在長度頭后,最后也用 CRLF結尾,但數據不包含 CRLF;最后用一個長度為 0 的塊表示結束,即“0\r\n\r\n”,如下圖所示:
問題:
- Q: 分塊傳輸數據的時候,如果數據里含有回車換行(\r\n)是否會影響分塊的處理呢?
- A: 分塊傳輸中數據里含有回車換行(\r\n)不影響分塊處理,因為分塊前有數據長度說明
★,范圍請求:分塊傳輸是把整個文件分成若干小塊進行傳輸,如果客戶端想獲取一個大文件其中的片段數據,分塊傳輸并沒有這個能力。HTTP 協議為了滿足這樣的需求,提出了“范圍請求”(range requests)的概念,允許客戶端在請求頭里使用專用字段來表示只獲取文件的一部分。
范圍請求不是 Web服務器必備的功能,可以實現也可以不實現,所以服務器必須在響應頭里使用字段“·Accept-Ranges: bytes·”明確告知客戶端:“我是支持范圍請求的”。
如果不支持的話該怎么辦呢?服務器可以發送“Accept-Ranges:none”,或者干脆不發送“Accept-Ranges”字段,這樣客戶端就認為服務器沒有實現范圍請求功能,只能老老實實地收發整塊文件了。
- 請求頭·Range·是 HTTP 范圍請求的專用字段,格式是“bytes=x-y”,其中的 x 和 y 是以字節為單位的數據范圍,從0計數。如:·Range: bytes=0-31·
服務器收到 Range 字段后,需要做四件事。
- 第一,它必須檢查范圍是否合法,比如文件只有 100個字節,但請求“200-300”,這就是范圍越界了。服務器就會返回狀態碼416,意思是“你的范圍請求有誤,我無法處理,請再檢查一下”。
- 第二,如果范圍正確,服務器就可以根據 Range頭計算偏移量,讀取文件的片段了,返回狀態碼“206 Partial Content”,和 200 的意思差不多,但表示 body只是原數據的一部分。
- 第三,服務器要添加一個響應頭字段·Content-Range·,告訴片段的實際偏移量和資源的總大小,格式是“bytes x-y/length”(如:·Content-Range: bytes 0-31/96·),與 Range頭區別在沒有“=”,范圍后多了總長度。例如,對于“0-10”的范圍請求,值就是“bytes 0-10/100”。
- 第四:最后剩下的就是發送數據了,直接把片段用 TCP發給客戶端,一個范圍請求就算是處理完了
有了范圍請求之后,HTTP處理大文件就更加輕松了,看視頻時可以根據時間點計算出文件的Range,不用下載整個文件,直接精確獲取片段所在的數據內容。不僅看視頻的拖拽進度需要范圍請求,常用的下載工具里的多段下載、斷點續傳也是基于它實現的,要點是:
先發個HEAD,看服務器是否支持范圍請求,同時獲取文件的大小;開 N 個線程,每個線程使用 Range字段劃分出各自負責下載的片段,發請求傳輸數據;下載意外中斷也不怕,不必重頭再來一遍,只要根據上次的下載記錄,用 Range請求剩下的那一部分就可以了。
- Q: 如果對一個被 gzip 的文件執行范圍請求,比如“Range: bytes=10-19”,那么這個范圍是應用于原文件還是壓縮后的文件呢?
- A: range是針對原文件的。
★,多段數據:范圍請求一次只獲取一個片段,其實它還支持在Range 頭里使用多個“x-y”,一次性獲取多個片段數據。這種情況需要使用一種特殊的 MIME 類型:“
multipart/byteranges”,表示報文的 body是由多段字節序列組成的,并且還要用一個參數“boundary=xxx”給出段之間的分隔標記。多段數據的傳輸格式如下圖所示:

每一個分段必須以“--boundary”開始(前面加兩個“-”),之后要用“Content-Type”和“Content-Range”標記這段數據的類型和所在范圍,然后就像普通的響應頭一樣以回車換行結束,再加上分段數據,最后用一個“- -boundary--”(前后各有兩個“-”)表示所有的分段結束。
示例:
例如,我們在實驗環境里用 Telnet 發出有兩個范圍的請求:
GET /16-2 HTTP/1.1
Host: www.chrono.com
Range: bytes=0-9, 20-29
得到的就會是下面這樣:
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=
00000000001
Content-Length: 189
Connection: keep-alive
Accept-Ranges: bytes
--00000000001
Content-Type: text/plain
Content-Range: bytes 0-10/96
hello world
--00000000001
Content-Type: text/plain
Content-Range: bytes 20-29/96
ext json d
--00000000001--
報文里的“--00000000001”就是多段的分隔符,使用它客戶端就可以很容易地區分出多段 Range 數據。
※,17 | 排隊也要講效率:HTTP的連接管理
★,短連接
HTTP協議最初(0.9/1.0)是個非常簡單的協議,通信過程也采用了簡單的“請求 - 應答”方式。它底層的數據傳輸基于TCP/IP,每次發送請求前需要先與服務器建立連接,
收到響應報文后會立即關閉連接。因為客戶端與服務器的整個連接過程很短暫,不會與服務器保持長時間的連接狀態,所以就被稱為“短連接”(short-lived connections)。早期的 HTTP 協議也被稱為是“無連接”的協議。
短連接的缺點相當嚴重,因為在 TCP協議里,建立連接和關閉連接都是非常“昂貴”的操作。TCP建立連接要有“三次握手”,發送 3 個數據包,需要 1 個RTT;關閉連接是“四次揮手”,4 個數據包需要 2 個 RTT。而 HTTP 的一次簡單“請求 - 響應”通常只需要 4個包,如果不算服務器內部的處理時間,最多是 2 個RTT。這么算下來,浪費的時間就是“3÷5=60%”,有三分之二的時間被浪費掉了,傳輸效率低得驚人.
★,長連接
長連接對性能的改善效果非常顯著,所以在 HTTP/1.1中的連接都會默認啟用長連接。不需要用什么特殊的頭字段指定,只要向服務器發送了第一次請求,后續的請求都會重復利用第一次打開的 TCP連接,也就是長連接,在這個連接上收發數據。當然,我們也可以在請求頭里明確地要求使用長連接機制,使用的字段是Connection,值是“keep-alive”。不過不管客戶端是否顯式要求長連接,如果服務器支持長連接,它總會在響應報文里放一個“`Connection: keep-alive`”字段,告訴客戶端:“我是支持長連接的,接下來就用這個TCP 一直收發數據吧”。
- Connection還有一個取值:·Connection: Upgrade·,配合狀態碼101表示協議升級,例如從HTTP協議切換到WebSocket。
長連接也有缺點:因為 TCP連接長時間不關閉,服務器必須在內存里保存它的狀態,這就占用了服務器的資源。如果有大量的空閑長連接只連不發,就會很快耗盡服務器的資源,導致服務器無法為真正有需要的用戶提供服務。所以,長連接也需要在恰當的時間關閉,不能永遠保持與服務器的連接,這在客戶端或者服務器都可以做到。
- 在客戶端,可以在請求頭里加上“Connection: close”字段,告訴服務器:“這次通信后就關閉連接”。服務器看到這個字段,就知道客戶端要主動關閉連接,于是在響應報文里也加上·Connection: close·這個字段,發送之后就調用 SocketAPI 關閉 TCP 連接。
- 服務器端通常不會主動關閉連接,但也可以使用一些策略。拿Nginx 來舉例,它有兩種方式:
- 用“keepalive_timeout”指令,設置長連接的超時時間,如果在一段時間內連接上沒有任何數據收發就主動斷開連接,避免空閑連接占用系統資源。
- 使用“keepalive_requests”指令,設置長連接上可發送的最大請求次數。比如設置成1000,那么當 Nginx 在這個連接上處理了 1000個請求后,也會主動斷開連接。
- 另外:客戶端和服務器都可以在報文里附加通用頭字段“Keep-Alive:timeout=value”,限定長連接的超時時間。但這個字段的約束力并不強,通信的雙方可能并不會遵守,所以不太常見。
★,隊頭阻塞(Head-of-line blocking,也叫“隊首阻塞”):一個HTTP的TCP連接中,多個請求組成一個“串行”隊列,這就可能會導致隊頭阻塞問題。
“隊頭阻塞”與短連接和長連接無關,而是由 HTTP基本的“請求 - 應答”模型所導致的。因為 HTTP規定報文必須是“一發一收”,這就形成了一個先進先出的“串行”隊列。隊列里的請求沒有輕重緩急的優先級,只有入隊的先后順序,排在最前面的請求被最優先處理。如果隊首的請求因為處理的太慢耽誤了時間,那么隊列里后面的所有請求也不得不跟著一起等待,結果就是其他的請求承擔了不應有的時間成本。

隊頭阻塞優化方案:
因為“請求 - 應答”模型不能變,所以“隊頭阻塞”問題在HTTP/1.1 里無法解決,只能緩解。方法有二:
- “并發連接”(concurrent connect):,也就是同時對一個域名發起多個長連接,用數量來解決質量的問題。
- 但這種方式也存在缺陷。如果每個客戶端都想自己快,建立很多個連接,用戶數×并發數就會是個天文數字。服務器的資源根本就扛不住,或者被服務器認為是惡意攻擊,反而會造成“拒絕服務”。所以,HTTP協議建議客戶端使用并發,但不能“濫用”并發。RFC2616里明確限制每個客戶端最多并發 2個連接。不過實踐證明這個數字實在是太小了,眾多瀏覽器都“無視”標準,把這個上限提高到了 6~8。后來修訂的 RFC7230也就“順水推舟”,取消了這個“2”的限制。
- 利用HTTP的長連接特性對服務器發起大量的請求,導致服務器最終耗盡資源“拒絕服務”,這就是常說的DDos。
- “域名分片”(domain shard):,還是用數量來解決質量的思路。HTTP協議和瀏覽器不是限制并發連接數量嗎?好,那我就多開幾個域名,比如shard1.chrono.com、shard2.chrono.com,而這些域名都指向同一臺服務器www.chrono.com,這樣實際長連接的數量就又上去了,真是“美滋滋”。不過實在是有點“上有政策,下有對策”的味道。
★,
※,18 | 四通八達:HTTP的重定向和跳轉
★,用于標識重定向的“Location”字段屬于響應字段,必須出現在響應報文里。但只有配合301/302 狀態碼才有意義,它標記了服務器要求重定向的URI。
★,重定向報文里還可以用Refresh字段實現延遲重定向,如·Refresh: 5;url=xxx·告訴瀏覽器5秒鐘后再跳轉。
★,與跳轉有關的還有一個·Referer·和·Referrer-Policy·(注意前者是個拼寫錯誤,但已經將錯就錯!),表示瀏覽器跳轉的來源(即引用地址),用于統計分析和防盜鏈。
★,如果重定向的策略設置欠考慮,可能會出現“A=>B=>C=>A”的無限循環,不停地在這個鏈路里轉圈圈。HTTP協議特別規定,瀏覽器必須具有檢測“循環跳轉”的能力,在發現這種情況時應當停止發送請求并給出錯誤提示。
★,
※,19 |讓我知道你是誰:HTTP的Cookie機制
★,Cookie 的工作過程
Cookie是服務器委托瀏覽器存儲的一些數據,讓服務器有了“記憶能力”;
客戶端和服務器之間進行cookie的傳遞需要用到兩個字段:響應頭字段·Set-Cookie·和請求頭字段·Cookie·。cookie是由瀏覽器負責存儲的,而不是操作系統。所以,它是“瀏覽器綁定”的,只能在本瀏覽器內生效。

★,Cookie 的屬性
Cookie就是服務器委托瀏覽器存儲在客戶端里的一些數據,而這些數據通常都會記錄用戶的關鍵識別信息。所以,就需要在“key=value”外再用一些手段來保護,防止外泄或竊取,這些手段就是 Cookie 的屬性。
一個cookie的實例如下:·Set-Cookie: favorite=hamburger; Max-Age=10; Expires=Wed, 21-Feb-24 09:55:57 GMT; Domain=localhost; Path=/; HttpOnly; SameSite=Strict·,cookie屬性介紹如下:
- Cookie 的生存周期:也就是它的有效期,讓它只能在一段時間內可用,就像是食品的“保鮮期”,一旦超過這個期限瀏覽器就認為是Cookie 失效,在存儲里刪除,也不會發送給服務器。Cookie 的有效期可以使用 Expires 和 Max-Age兩個屬性來設置。Expires 和 Max-Age可以同時出現,兩者的失效時間可以一致,也可以不一致,但瀏覽器會優先采用 Max-Age 計算失效期。
- ·Expires·俗稱“過期時間”,用的是絕對時間點,可以理解為“截止日期”(deadline)。
- ·Max-Age·用的是相對時間,單位是秒,瀏覽器用收到報文的時間點再加上Max-Age,就可以得到失效的絕對時間(Cookie 的 max-age 是從瀏覽器拿到響應報文時開始計算的)。
- 如果不指定Expires或Max-Age 屬性,那么Cookie 僅在瀏覽器運行時有效,一旦瀏覽器關閉就會失效,這被稱為會話 Cookie (sessioncookie)或內存 Cookie(in-memory cookie)在 Chrome 里過期時間會顯示為“Session”或N/A
- Cookie 的作用域:讓瀏覽器僅發送給特定的服務器和URI,避免被其他網站盜用。·Domain·和·Path·指定了 Cookie所屬的域名和路徑,瀏覽器在發送 Cookie 前會從 URI中提取出 host 和 path 部分,對比 Cookie
的屬性。如果不滿足條件,就不會在請求頭里發送 Cookie。現實中為了省事,通常 Path就用一個“/”或者直接省略,表示域名下的任意路徑都允許使用Cookie,讓服務器自己去挑。 - Cookie 的安全性:盡量不要讓服務器以外的人看到。在 JS 腳本里可以用document.cookie 來讀寫 Cookie數據,這就帶來了安全隱患,有可能會導致“跨站腳本”(XSS)攻擊竊取數據。
- 屬性·HttpOnly· 會告訴瀏覽器,此 Cookie 只能通過瀏覽器HTTP 協議傳輸,禁止其他方式訪問,瀏覽器的 JS引擎就會禁用 document.cookie 等一切相關的API,腳本攻擊也就無從談起了。
- ·SameSite·可以防范“跨站請求偽造”(XSRF)攻擊,設置成“SameSite=Strict”可以嚴格限定 Cookie不能隨著跳轉鏈接跨站發送,而“SameSite=Lax”則略寬松一點,允許 GET/HEAD 等安全方法,但禁止 POST 跨站發送。
- 還有一個屬性叫·Secure·,表示這個 Cookie 僅能用 HTTPS協議加密傳輸,明文的 HTTP 協議會禁止發送。但 Cookie本身不是加密的,瀏覽器里還是以明文的形式存在。Chrome 開發者工具是查看 Cookie的有力工具
★,Cookie 的應用
- 身份識別:實現有狀態的會話“事務”(如登錄、購物、付款三個活動構成的一個“事務”)。
- 廣告跟蹤:你上網的時候肯定看過很多的廣告圖片,這些圖片背后都是廣告商網站(例如Google),它會“偷偷地”給你貼上 Cookie小紙條,這樣你上其他的網站,別的廣告就能用 Cookie讀出你的身份,然后做行為分析,再推給你廣告。這種 Cookie 不是由訪問的主站存儲的,所以又叫“第三方Cookie”(third-party cookie)
★,雖然現在已經出現了多種 Local Web Storage技術,能夠比 Cookie 存儲更多的數據,但 Cookie仍然是最通用、兼容性最強的客戶端數據存儲手段。
★,因為 Cookie 并不屬于 HTTP標準(RFC6265,而不是RFC2616/7230),所以語法上與其他字段不太一致,使用的分隔符是“;”,與 Accept等字段的“,”不同,小心不要弄錯了。
★,小貼士:
- Cookie 這個詞來源于計算機編程里的術語“Magic Cookie”,意思是不透明的數據,并不是“小甜餅”的含義 (雖然字面意如此)
- 早期 Cookie 直接就是磁盤上的一些小文本文件,現在基本上都是以數據庫記錄的形式存放的 (通常使用的是 Sqlite)。瀏覽器對 Cookie的數量和大小也都有限制,不允許無限存儲一般總大小不能超過4K
※,20 | 生鮮速遞:HTTP的緩存控制
緩存(Cache)是計算機領域里的一個重要概念,是優化系統性能的利器。HTTP傳輸的每一個環節基本上都會有緩存,非常復雜。基于“請求 -應答”模式的特點,可以大致分為客戶端緩存和服務器端緩存,因為服務器端緩存經常與代理服務“混搭”在一起,所以今天先講客戶端——也就是瀏覽器的緩存。
·Cache-Control: max-age=0· //Cache 的 max-age 是從響應報文的生成時間(Date 頭字段)開始計算而不是瀏覽器接受報文的時間(注意與cookie的區別)
服務器可以發“Cache-Control”頭,瀏覽器也可以發“Cache-Control”,也就是說請求 -應答的雙方都可以用這個字段進行緩存控制,互相協商緩存的使用策略。
當點擊瀏覽器“刷新”按鈕的時候,瀏覽器會在請求頭里加一個“Cache-Control: max-age=0”。瀏覽器就不會使用緩存,而是向服務器發請求。服務器看到max-age=0,也就會用一個最新生成的報文回應瀏覽器。
★,除了“Cache-Control”,服務器也可以用“Expires”字段來標記資源的有效期,它的形式和 Cookie 的差不多,同樣屬于“過時”的屬性,優先級低于“Cache-Control”。
★,還有一個歷史遺留字段“Pragma: no-cache”,它相當于“Cache-Control: no-cache”,除非為了兼容 HTTP/1.0 否則不建議使用。
★,
※,21 | 良心中間商:HTTP的代理服務
客戶端---代理服務器---源服務器
★,代理的作用
你也許聽過這樣一句至理名言:“計算機科學領域里的任何問題,都可以通過引入一個中間層來解決”(在這句話后面還可以再加上一句“如果一個中間層解決不了問題,那就再加一個中間層”)。
- 負載均衡:
- 健康檢查:使用“心跳”等機制監控后端服務器,發現有故障就及時“踢出”集群,保證服務高可用;
- 安全防護:保護被代理的后端服務器,限制 IP地址或流量,抵御網絡攻擊和過載;
- 加密卸載:對外網使用 SSL/TLS加密通信認證,而在安全的內網不加密,消除加解密成本;
- 數據過濾:攔截上下行的數據,任意指定策略修改請求或者響應;
- 內容緩存:暫存、復用服務器響應
★,代理相關頭字段
`Via` : Via是一個通用字段,請求頭或響應頭里都可以出現。每當報文經過一個代理節點,代理服務器就會把自身的信息追加到字段的末尾。
- “Via”是HTTP 協議里規定的標準頭字段,但有的服務器返回的響應報文里會使用“X-Via”含義是相同的
·X-Forwarded-For·的字面意思是“為誰而轉發”,形式上和“Via”差不多,也是每經過一個代理節點就會在字段里追加一個信息。但“Via”追加的是代理主機名(或者域名),而“X-Forwarded-For”追加的是請求方的 IP 地址。所以,在字段里最左邊的 IP地址就客戶端的地址。有了“X-Forwarded-For”等頭字段,源服務器就可以拿到準確的客戶端信息了。
- 因為 HTTP 是明文傳輸,請求頭很容易被竄改所以“X-Forwarded-For”也不是完全可信的
·X-Real-IP·是另一種獲取客戶端真實 IP的手段,它的作用很簡單,就是記錄客戶端 IP地址,沒有中間的代理信息,相當于是“X-Forwarded-For”的簡化版。如果客戶端和源服務器之間只有一個代理,那么這兩個字段的值就是相同的。
·X-Forwarded-Host· 和 ·X-Forwarded-Proto·,它們的作用與“X-Real-IP”類似,只記錄客戶端的信息,分別是客戶端請求的原始域名和原始協議名。
★,代理協議:
專門的“代理協議”可以在不改動原始報文的情況下傳遞客戶端的真實 IP。,它由知名的代理軟件 HAProxy所定義,也是一個“事實標準”,被廣泛采用(注意并不是RFC)。
★,tips
知名的代理軟件有 HAProxy、Squid、Varnish等,而 Nginx 雖然是 Web 服務器,但也可以作為代理服務器,而且功能毫不遜色
★,
※,22 | 冷鏈周轉:HTTP的緩存代理
HTTP傳輸鏈路上,不只是客戶端有緩存,服務器上的緩存也是非常有價值的,可以讓請求不必走完整個后續處理流程,“就近”獲得響應結果。
HTTP的服務器緩存功能主要由代理服務器來實現(即緩存代理),而源服務器系統內部雖然也經常有各種緩存(如Memcache、Redis、Varnish 等),但與 HTTP沒有太多關系。
★,源服務器的緩存控制
服務器端的“Cache-Control”屬性:max-age、no_store、no_cache 和 must-revalidate,這 4 種緩存屬性可以約束客戶端,也可以約束代理。但客戶端和代理是不一樣的,客戶端的緩存只是用戶自己使用,而代理的緩存可能會為非常多的客戶端提供服務。所以,需要對它的緩存再多一些限制條件(也就是會多一些屬性)
區分客戶端上的緩存和代理上的緩存,可以使用兩個新屬性“private”和“public”:“private”表示緩存只能在客戶端保存,是用戶“私有”的,不能放在代理上與別人共享。而“public”的意思就是緩存完全開放,誰都可以存,誰都可以用。

★,客戶端的緩存控制
客戶端在 HTTP緩存體系里要面對的是代理和源服務器(之前在沒有代理的情形下是只面對源服務器),也必須區別對待,
★,清理緩存
清理緩存的方法有很多,比較常用的一種做法是使用自定義請求方法“PURGE”,發給代理服務器,要求刪除 URI 對應的緩存數據。
★,小結:
計算機領域里最常用的性能優化手段是“時空轉換”,也就是“時間換空間”或者“空間換時間”,HTTP 緩存屬于后者;緩存代理是增加了緩存功能的代理服務,緩存源服務器的數據,分發給下游的客戶端;
“Cache-Control”字段也可以控制緩存代理,常用的有“private”“s-maxage”“no-transform”等,同樣必須配合“Last-modified”“ETag”等字段才能使用;
★,
※,23 |HTTPS是什么?SSL/TLS又是什么?
之前講了HTTP的一些缺點,其中的“無狀態”在加入 Cookie后得到了解決,而另兩個缺點——“明文”和“不安全”僅憑 HTTP。自身是無力解決的,需要引入新的 HTTPS 協議
★,通信安全必須同時具備機密性、完整性,身份認證和不可否認這四個特性;
★,什么是 HTTPS
HTTPS的全稱是“HTTP over SSL/TLS”,也就是運行在 SSL/TLS 協議上的 HTTP。注意它的名字,這里是 SSL/TLS,而不是TCP/IP,它是一個負責加密通信的安全協議,建立在 TCP/IP之上,所以也是個可靠的傳輸協議,可以被用作 HTTP的下層。因為 HTTPS相當于“HTTP+SSL/TLS+TCP/IP”。
HTTPS抓包實際就是篩選出tls協議內容(tls && tcp.port==xxxx)
HTTPS 其實是一個“非常簡單”的協議,RFC文檔很小,只有短短的 7 頁,里面規定了新的協議名“https”,默認端口號 443,至于其他的什么請求-應答模式、報文結構、請求方法、URI、頭字段、連接管理等等都完全沿用 HTTP,沒有任何新的東西。也就是說,除了協議名“http”和端口號 80 這兩點不同,HTTPS協議在語法、語義上和 HTTP完全一樣,優缺點也“照單全收”(當然要除去“明文”和“不安全”)。
然沒有新東西,HTTPS憑什么就能做到機密性、完整性這些安全特性呢?秘密就在于 HTTPS 名字里的“S”,它把 HTTP下層的傳輸協議由 TCP/IP 換成了 SSL/TLS,由“HTTP over TCP/IP”變成了“HTTP over SSL/TLS”,讓 HTTP運行在了安全的 SSL/TLS 協議上,收發報文不再使用 Socket API,而是調用專門的安全接口。

★,TLS: TLS1.0 實際上就是 SSLv3.1
TLS由記錄協議、握手協議、警告協議、變更密碼規范協議、擴展協議等幾個子協議組成,綜合使用了對稱加密、非對稱加密、身份認證等許多密碼學前沿技術。瀏覽器和服務器在使用 TLS建立連接時需要選擇一組恰當的加密算法來實現安全通信,這些算法的組合被稱為“密碼套件”(ciphersuite,也叫加密套件)。
TLS的密碼套件命名非常規范,格式很固定。基本的形式是“密鑰交換算法 + 簽名算法 + 對稱加密算法 + 摘要算法”,比如`ECDHE-RSA-AES256-GCM-SHA384`密碼套件的意思就是:“握手時使用 ECDHE 算法進行密鑰交換,用 RSA簽名和身份認證,握手后的通信使用 AES對稱算法,密鑰長度 256 位,分組模式是 GCM,摘要算法SHA384 用于消息認證和產生隨機數。”
除了 HTTP,SSL/TLS 也可以承載其他的應用協議,例如 FTP=>FTPS,LDAP=>LDAPS 等
★,OpenSSL
說到 TLS,就不能不談到OpenSSL,它是一個著名的開源密碼學程序庫和工具包,幾乎支持所有公開的加密算法和協議,已經成為了事實上的標準,許多應用軟件都會使用它作為底層庫來實現 TLS功能,包括常用的 Web 服務器 Apache、Nginx 等。
OpenSSL里的密碼套件定義與 TLS略有不同TLS里的形式是“TLS ECDHE_RSAWITH AES_256_GCM_SHA384”,加了前綴“TLS”,并用“WITH”分開了握手和通信的算法。
另一個比較著名的開源密碼庫是 NSS(Network Security Services),由 Mozilla 開發。
★,
※,24 |固若金湯的根本(上):對稱加密與非對稱加密
所有的加密算法都是公開的,任何人都可以去分析研究,而算法使用的“密鑰”則必須保密。那么,這個關鍵的“密鑰”又是什么呢?由于 HTTPS、TLS都運行在計算機上,所以“密鑰”就是一長串的數字,但約定俗成的度量單位是“位”(bit),而不是“字節”(byte)。比如,說密鑰長度是 128,就是 16字節的二進制串,密鑰長度 1024,就是 128字節的二進制串。按照密鑰的使用方式,加密可以分為兩大類:對稱加密和非對稱加密。
★,對稱加密
對稱加密只使用一個密鑰,運算速度快,密鑰必須保密。
TLS 里有非常多的對稱加密算法可供選擇,比如RC4、DES、3DES、AES、ChaCha20等,但前三種算法都被認為是不安全的,通常都禁止使用,目前常用的只有 AES 和 ChaCha20。·AES· 的意思是“高級加密標準”(Advanced Encryption Standard),密鑰長度可以是 128、192 或 256。它是 DES算法的替代者,安全強度很高,性能也很好,而且有的硬件還會做特殊優化,所以非常流行,是應用最廣泛的對稱加密算法。
加密分組模式:對稱算法還有一個“分組模式”的概念,它可以讓算法用固定長度的密鑰加密任意長度的明文。最新的分組模式被稱為AEAD(Authenticated Encryption with ssociated Data),在加密的同時增加了認證的功能,常用的是GCM、CCM 和 Poly1305。
★,非對稱加密
對稱加密看上去好像完美地實現了機密性,但其中有一個很大的問題:如何把密鑰安全地傳遞給對方,術語叫“密鑰交換”。因為在對稱加密算法中只要持有密鑰就可以解密。
如果你和網站約定的密鑰在傳遞途中被黑客竊取,那他就可以在之后隨意解密收發的數據,通信過程也就沒有機密性可言了。
只用對稱加密算法,是絕對無法解決密鑰交換的問題的。所以,就出現了非對稱加密(也叫公鑰加密算法)。它有兩個密鑰,一個叫“公鑰”(public key),一個叫“私鑰
”(private key)。兩個密鑰是不同的,“不對稱”,公鑰可以公開給任何人使用,而私鑰必須嚴格保密。公鑰和私鑰有個特別的“單向”性,雖然都可以用來加密解密,但公鑰加密后只能用私鑰解密,反過來,私鑰加密后也只能用公鑰解密。非對稱加密可以解決“密鑰交換”的問題。網站秘密保管私鑰,在網上任意分發公鑰,你想要登錄網站只要用公鑰加密就行了,密文只能由私鑰持有者才能解密。而黑客因為沒有私鑰,所以就無法破解密文。
非對稱加密算法的設計要比對稱算法難得多,在 TLS里只有很少的幾種,比如 DH、DSA、·RSA·、·ECC· 等。RSA可能是其中最著名的一個,幾乎可以說是非對稱加密的代名詞。ECC(Elliptic Curve Cryptography)是非對稱加密里的“后起之秀。比起 RSA,ECC 在安全強度和性能上都有明顯的優勢。160位的 ECC 相當于 1024 位的 RSA,而 224 位 ECC則相當于 2048 位的RSA。
ECC 雖然定義了公鑰和私鑰,但不能直接實現密鑰交換和身份認證,需要搭配 DH、DSA等算法,形成專門的 ECDHE、ECDSA。RSA 比較特殊,本身既支持密鑰交換也支持身份認證。
★,混合加密:
然非對稱加密沒有“密鑰交換”的問題,但因為它們都是基于復雜的數學難題,運算速度很慢,即使是ECC 也要比 AES差上好幾個數量級。如果僅用非對稱加密,雖然保證了安全,但通信速度有如烏龜、蝸牛,實用性就變成了零。
TLS 里使用的混合加密方式,其實說穿了也很簡單:在通信剛開始的時候使用非對稱算法,比如RSA、ECDHE,首先解決密鑰交換的問題。然后用隨機數產生對稱加密算法使用的“會話密鑰”(session key),再用公鑰加密。因為會話密鑰很短,通常只有 16字節或 32 字節,所以慢一點也無所謂。對方拿到密文后用私鑰解密,取出會話密鑰。這樣,
雙方就實現了對稱密鑰的安全交換,后續就不再使用非對稱加密,全都使用對稱加密。
這樣混合加密就解決了對稱加密算法的密鑰交換問題,而且安全和性能兼顧,完美地實現了機密性。不過這只是“萬里長征的第一步”,后面還有完整性、身份認證、不可否認等特性沒有實現,所以現在的通信還不是絕對安全。
★,
※,25 |固若金湯的根本(下):數字簽名與證書
上一講中我們學習了對稱加密和非對稱加密,以及兩者結合起來的混合加密,實現了機密性。但僅有機密性,離安全還差的很遠。黑客雖然拿不到會話密鑰,無法破解密文,但可以通過竊聽收集到足夠多的密文,再嘗試著修改、重組后發給網站。因為沒有完整性保證,服務器只能“照單全收”,然后他就可以通過服務器的響應獲取進一步的線索,最終就會破解出明文。另外,黑客也可以偽造身份發布公鑰。如果你拿到了假的公鑰,混合加密就完全失效了。你以為自己是在和“某寶”通信,實際上網線的另一端卻是黑客,行卡號、密碼等敏感信息就在“安全”的通信過程中被竊取了。所以,在機密性的基礎上還必須加上完整性、身份認證等特性,才能實現真正的安全。
★,摘要算法
實現完整性的手段主要是摘要算法(Digest Algorithm),也就是常說的散列函數、哈希函數(Hash Function)。可以把摘要算法近似地理解成一種特殊的壓縮算法,它能夠把任意長度的數據“壓縮”成固定長度、而且獨一無二的“摘要”字符串。
摘要算法實際上是把數據從一個“大空間”映射到了“小空間”,所以就存在“沖突”(collision,也叫碰撞)的可能性,就如同現實中的指紋一樣,可能會有兩份不同的原文對應相同的摘要。好的摘要算法必須能夠“抵抗沖突”,讓這種可能性盡量地小。
MD5(Message-Digest5)、SHA-1(Secure Hash Algorithm1),它們就是最常用的兩個摘要算法,能夠生成 16 字節和20字節長度的數字摘要。但這兩個算法的安全強度比較低,不夠安全,在 TLS 里已經被禁止使用了。目前 TLS 推薦使用的是 SHA-1 的后繼者:SHA-2。SHA-2 實際上是一系列摘要算法的統稱,總共有 6種,常用的有 SHA224、SHA256、SHA384,分別能夠生成 28字節、32 字節、48 字節的摘要。
摘要算法除了用于 TLS 安全通信,還有很多其他的用途,比如散列表、數據校驗、大文件比較等。
★,完整性
摘要算法保證了“數字摘要”和原文是完全等價的。所以,我們只要在原文后附上它的摘要,就能夠保證數據的完整性。比如,你發了條消息:“轉賬 1000 元”,然后再加上一個SHA-2的摘要。網站收到后也計算一下消息的摘要,把這兩份“指紋”做個對比,如果一致,就說明消息是完整可信的,沒有被修改。如果黑客在中間哪怕改動了一個標點符號,摘要也會完全不同,網站計算比對就會發現消息被竄改,是不可信的。不過摘要算法不具有機密性,如果明文傳輸,那么黑客可以修改消息后把摘要也一起改了,網站還是鑒別不出完整性。所以,真正的完整性必須要建立在機密性之上,在混合加密系統里用會話密鑰加密消息和摘要,這樣黑客無法得知明文,也就沒有辦法動手腳了。這有個術語,叫哈希消息認證碼(HMAC)。
★,數字簽名
加密算法結合摘要算法,我們的通信過程可以說是比較安全了。但這里還有漏洞,就是通信的兩個端點(endpoint)。就像一開始所說的,黑客可以偽裝成網站來竊取信息。而反過來,他也可以偽裝成你,向網站發送支付、轉賬等消息,網站沒有辦法確認你的身份,錢可能就這么被偷走了。
非對稱加密里的“私鑰”只能由本人持有,能夠在數字世界里證明你的身份。使用私鑰再加上摘要算法,就能夠實現“數字簽名”,同時實現“身份認證”和“不可否認”。數字簽名的原理其實很簡單,就是把公鑰私鑰的用法反過來,之前是公鑰加密、私鑰解密,現在是私鑰加密、公鑰解密。但又因為非對稱加密效率太低,所以私鑰只加密原文的摘要,這樣運算量就小的多,而且得到的數字簽名也很小,方便保管和傳輸。簽名和公鑰一樣完全公開,任何人都可以獲取。但這個簽名只有用私鑰對應的公鑰才能解開,拿到摘后,再比對原文驗證完整性,就可以像簽署文件一樣證明消息確實是你發的。
剛才的這兩個行為也有專用術語,叫做“簽名”和“驗簽”。只要你和網站互相交換公鑰,就可以用“簽名”和“驗簽”來確認消息的真實性,因為私鑰保密,黑客不能偽造簽,就能夠保證通信雙方的身份。比如,你用自己的私鑰簽名一個消息“我是小明”。網站收到后用你的公鑰驗簽,確認身份沒問題,于是也用它的私鑰簽名消息“我是某寶”。你收到后再用它的公鑰驗一下,也沒問題,這樣你和網站就都知道對方不是假冒的,后面就可以用混合加密進行安全通信了。
★,數字證書和 CA
綜合使用對稱加密、非對稱加密和摘要算法,我們已經實現了安全的四大特性,是不是已經完美了呢?不是的,這里還有一個“公鑰的信任”問題。因為誰都可以發布公鑰,我們還缺少防止黑客偽造公鑰的手段,也就是說,怎么來判斷這個公鑰就是你或者某寶的公鑰呢?
我們可以用類似密鑰交換的方法來解決公鑰認證問題,用別的私鑰來給公鑰簽名,顯然,這又會陷入“無窮遞歸”。但這次實在是“沒招”了,要終結這個“死循環”,就必須入“外力”,找一個公認的可信第三方,讓它作為“信任的起點,遞歸的終點”,構建起公鑰的信任鏈。這個“第三方”就是我們常說的CA(Certificate Authority,證書認證機構)。它就像網絡世界里的公安局、教育部、公證中心,具有極高的可信度,由它來給各個公鑰簽名,用自身的信譽來保證公鑰無法偽造,是可信的。
有了證書體系,操作系統和瀏覽器都內置了各大 CA的根證書,上網的時候只要服務器發過來它的證書,就可以驗證證書里的簽名,順著證書鏈(CertificateChain)一層層地驗證,直到找到根證書,就能夠確定證書是可信的,從而里面的公鑰也是可信的。
證書的格式遵循X509 v3 標準,有兩種編碼方式,一種是二進制的 DER,另一種是 ASCII 碼的 PEM。
★,
★,
★,
※,26 | 信任始于握手:TLS1.2連接過程解析
HTTPS 協議會先與服務器執行 TCP 握手,然后執行 TLS握手,才能建立安全連接;TLS握手的目標是安全地交換對稱密鑰,需要三個隨機數,第三個隨機數“Pre-Master”必須加密傳輸,絕對不能讓黑客破解;“Hello”消息交換隨機數,“Key Exchange”消息交換“Pre-Master”;“Change Cipher Spec”之前傳輸的都是明文,之后都是對稱密鑰加密的密文。
★,TLS的握手過程

★,雙向認證
“單向認證”握手過程,只認證了服務器的身份,而沒有認證客戶端的身份。這是因為通常單向認證通過后已經建立了安全通信,用賬號、密碼等簡單的手段就能夠確認用戶的真實身份。但為了防止賬號、密碼被盜,有的時候(比如網上銀行)還會使用 U 盾給用戶頒發客戶端證書,實現“雙向認證”,這樣會更加安全。雙向認證的流程也沒有太多變化,只是在“Server HelloDone”之后,“Client Key Exchange”之前,客戶端要發送“Client Certificate”消息,服務器收到后也把證書鏈走一遍,驗證客戶端的身份。
★,
★,
※,27 | 更好更快的握手:TLS1.3特性解析
★,
※,28 | 連接太慢該怎么辦:HTTPS的優化
★,
※,29 | 我應該遷移到HTTPS嗎
“遷移到HTTPS”已經不是“要不要做”的問題,而是“要怎么做”的問題了.
★,如何遷移至HTTPS
- 申請證書:
- 要把網站從 HTTP 切換到HTTPS,首先要做的就是為網站申請一張證書。大型網站出于信譽、公司形象的考慮,通常會選擇向傳統的CA 申請證書,例如DigiCert、GlobalSign,而中小型網站完全可以選擇使用“Let’s Encrypt”這樣的免費證書,效果也完全不輸于那些收費的證書。“Let’s Encrypt”一直在推動證書的自動化部署,為此還實現了專門的 ACME協議(RFC8555)。有很多的客戶端軟件可以完成申請、驗證、下載、更新的“一條龍”操作,比如 Certbot、acme.sh等等,都可以在“Let’s Encrypt”網站上找到,用法很簡單,相關的文檔也很詳細,幾分鐘就能完成申請。
- =============注意事項==============
- 第一,申請證書時應當同時申請 RSA 和 ECDSA兩種證書,在 Nginx里配置成雙證書驗證,這樣服務器可以自動選擇快速的橢圓曲線證書,同時也兼容只支持 RSA 的客戶端。
- 第二,如果申請 RSA 證書,私鑰至少要 2048位,摘要算法應該選用 SHA-2,例如 SHA256、SHA384 等。
- 第三,出于安全的考慮,“Let’s Encrypt”證書的有效期很短,只有 90天,時間一到就會過期失效,所以必須要定期更新。你可以在crontab 里加個每周或每月任務,發送更新請求,不過很多ACME 客戶端會自動添加這樣的定期任務,完全不用你操心。
- 配置 HTTPS
- 搞定了證書,接下來就是配置 Web 服務器,在 443端口上開啟 HTTPS 服務了。這在 Nginx上非常簡單,只要在“listen”指令后面加上參數“ssl”,再配上剛才的證書文件就可以實現最基本的 HTTPS。
listen 443 ssl; ssl_certificate xxx_rsa.crt; #rsa2048 cert ssl_certificate_key xxx_rsa.key; #rsa2048 private key ssl_certificate xxx_ecc.crt; #ecdsa cert ssl_certificate_key xxx_ecc.key; #ecdsa private key # 為了提高 HTTPS 的安全系數和性能,你還可以強制 Nginx只支持 TLS1.2 以上的協議,打開“Session Ticket”會話復用: ssl_protocols TLSv1.2 TLSv1.3; ssl_session_timeout 5m; ssl_session_tickets on; ssl_session_ticket_key ticket.key;
- 搞定了證書,接下來就是配置 Web 服務器,在 443端口上開啟 HTTPS 服務了。這在 Nginx上非常簡單,只要在“listen”指令后面加上參數“ssl”,再配上剛才的證書文件就可以實現最基本的 HTTPS。
★,服務器名稱指示
配置 HTTPS 服務時還有一個“虛擬主機”的問題需要解決。在 HTTP 協議里,多個域名可以同時在一個 IP地址上運行,這就是“虛擬主機”,Web服務器會使用請求頭里的 Host 字段(參見第 9 講)來選擇。但在 HTTPS 里,因為請求頭只有在 TLS握手之后才能發送,而在握手時就必須選擇“虛擬主機”對應的證書,TLS 無法得知域名的信息,就只能用 IP地址來區分。所以,最早的時候每個 HTTPS域名必須使用獨立的 IP 地址,非常不方便。
那么怎么解決這個問題呢?
這還是得用到 TLS 的“擴展”,給協議加個SNI(Server Name Indication)的“補充條款”。它的作用和 Host字段差不多,客戶端會在“ClientHello”時帶上域名信息,這樣 服務器就可以根據名字而不是 IP地址來選擇證書。
Extension: server_name (len=19)
Server Name Indication extension
Server Name Type: host_name (0)
Server Name: www.chrono.com
Nginx 很早就基于 SNI 特性支持了 HTTPS 的虛擬主機(proxy_pass配置中·proxy_ssl_server_name·和·proxy_ssl_name·指令),但在OpenResty 里可還以編寫 Lua 腳本,利用 Redis、MySQL等數據庫更靈活快速地加載證書。
★,重定向跳轉: 將HTTP請求跳轉到HTTPS
return 301 https://$host$request_uri; # 永久重定向
rewrite ^ https://$host$request_uri permanent;
重定向有兩個問題。一個是重定向增加了網絡成本,多出了一次請求;另一個是存在安全隱患,重定向的響應可能會被“中間人”竄改,實現“會話劫持”,跳轉到惡意網站。有一種“HSTS”(HTTP 嚴格傳輸安全,HTTP Strict Transport Security)的技術可以消除這種安全隱患。
★,
※,30 | 時代之風(上):HTTP/2特性概覽
HTTP有兩個主要的缺點:安全不足和性能不高。通過引入 SSL/TLS在安全上達到了“極致”,但在性能提升方面卻是乏善可陳,只優化了握手加密的環節,對于整體的數據傳輸沒有提出更好的改進方案,還只能依賴于“長連接”這種“落后”的技術。在 HTTPS 逐漸成熟之后,HTTP就向著性能方面開始“發力”,走出了另一條進化的道路。google
率先發明了 SPDY 協議,并應用于自家的瀏覽器Chrome,打響了 HTTP 性能優化的“第一槍”。隨后互聯網標準化組織 IETF 以 SPDY為基礎,綜合其他多方的意見,終于推出了 HTTP/1的繼任者,也就是今天的主角“HTTP/2”,在性能方面有了一個大的飛躍。
由于 HTTPS 已經在安全方面做的非常好了,所以 HTTP/2的唯一目標就是改進性能。
★,兼容 HTTP/1
★,頭部壓縮
★,二進制格式
HTTP/2把原來的“Header+Body”的消息“打散”為數個小片的二進制“幀”(Frame),用“HEADERS”幀存放頭數據、“DATA”幀存放實體數據。這種做法有點像是“Chunked”分塊編碼的方式(參見第 16 講),也是“化整為零”的思路,但 HTTP/2數據分幀后“Header+Body”的報文結構就完全消失了,協議看到的只是一個個的“碎片”。下圖中的HEADERS Frame可以有多個。

★,虛擬的“流”
消息的“碎片”到達目的地后應該怎么組裝起來呢?
HTTP/2 為此定義了一個“流”(Stream)的概念,它是二進制幀的雙向傳輸序列,同一個消息往返的幀會分配一個唯一的流
ID。你可以想象把它成是一個虛擬的“數據流”,在里面流動的是一串有先后順序的數據幀,這些數據幀按照次序組裝起來就是 HTTP/1里的請求報文和響應報文。
因為“流”是虛擬的,實際上并不存在,所以 HTTP/2就可以在一個 TCP 連接上用“流”同時發送多個“碎片化”的消息,這就是常說的“多路復用”(Multiplexing)——多個往返通信都復用一個連接來處理(注:HTTP/1.1中的長連接也可以將多個往返通信使用一個連接來處理,區別在于HTTP/1.1中的多個通信是串行的,HTTP/2中的多個往返通信是并行的)。在“流”的層面上看,消息是一些有序的“幀”序列,而在“連接”的層面上看,消息卻是亂序收發的“幀”。多個請求 /響應之間沒有了順序關系,不需要排隊等待,也就不會再出現“隊頭阻塞”問題,降低了延遲,大幅度提高了連接的利用率。

為了更好地利用連接,加大吞吐量,HTTP/2還添加了一些控制幀來管理虛擬的“流”,實現了優先級和流量控制,這些特性也和 TCP協議非常相似。
HTTP/2 還在一定程度上改變了傳統的“請求 -應答”工作模式,服務器不再是完全被動地響應請求,也可以新建“流”主動向客戶端發送消息。比如,在瀏覽器剛請求 HTML 的時候就提前把可能會用到的JS、CSS 文件發給客戶端,減少等待的延遲,這被稱為“服務器推送”(Server Push,也叫 Cache Push)。
★,強化安全
出于兼容的考慮,HTTP/2 延續了 HTTP/1的“明文”特點,可以像以前一樣使用明文傳輸數據,不強制使用加密通信,不過格式還是二進制,只是不需要解密。但由于 HTTPS 已經是大勢所趨,而且主流的瀏覽器Chrome、Firefox 等都公開宣布只支持加密的HTTP/2,所以“事實上”的 HTTP/2是加密的。也就是說,互聯網上通常所能見到的 HTTP/2都是使用“https”協議名,跑在 TLS 上面。為了區分“加密”和“明文”這兩個不同的版本,HTTP/2協議定義了兩個字符串標識符:“·h2·”表示加密的 HTTP/2,“·h2c·”表示明文的 HTTP/2,多出的那個字母“c”的意思是“clear text”。
★,協議棧:HTTP/2是建立在“HPack”“Stream”“TLS1.2”基礎之上的,比HTTP/1、HTTPS 復雜了一些。

★,
※,31 | 時代之風(下):HTTP/2內核剖析
★,連接前言(connection preface)
由于 HTTP/2“事實上”是基于TLS,所以在正式收發數據之前,會有 TCP 握手和 TLS握手,TLS 握手成功之后,客戶端必須要發送一個“連接前言”(connection preface),用來確認建立 HTTP/2 連接。這個“連接前言”是標準的 HTTP/1 請求報文,使用純文本的ASCII碼格式,請求方法是特別注冊的一個關鍵字“PRI”,全文只有
24 個字節:`PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n`在 Wireshark 里,HTTP/2 的“連接前言”被稱為“Magic”,意思就是“不可知的魔法”。所以,就不要問“為什么會是這樣”了,只要服務器收到這個“有魔力的字符串”,就知道客戶端在 TLS 上想要的是 HTTP/2協議,而不是其他別的協議,后面就會都使用 HTTP/2的數據格式。
★,頭部壓縮
★,二進制幀

★,流與多路復用
流與多路復用是HTTP/2 最核心的部分。
再重復一遍:流是二進制幀的雙向傳輸序列。
要搞明白流,關鍵是要理解幀頭里的流 ID。在 HTTP/2連接上,雖然幀是亂序收發的,但只要它們都擁有相同的流ID,就都屬于一個流,而且在這個流里幀不是無序的,而是有著嚴格的先后順序。
比如在這次的 Wireshark抓包里,就有“0、1、3”一共三個流,實際上就是分配了三個流ID 號,把這些幀按編號分組,再排一下隊,就成了流。
在概念上,一個 HTTP/2 的流就等同于一個 HTTP/1里的“請求 - 應答”。在 HTTP/1 里一個“請求 -響應”報文來回是一次 HTTP 通信,在 HTTP/2里一個流也承載了相同的功能。
你還可以對照著 TCP 來理解。TCP 運行在 IP 之上,其實從MAC 層、IP 層的角度來看,TCP的“連接”概念也是“虛擬”的。但從功能上看,無論是 HTTP/2的流,還是 TCP
的連接,都是實際存在的,所以你以后大可不必再糾結于流的“虛擬”性,把它當做是一個真實存在的實體來理解就好。
HTTP/2 的流有哪些特點呢?我給你簡單列了一下:
- 流是可并發的,一個 HTTP/2連接上可以同時發出多個流傳輸數據,也就是并發多請求,實現“多路復用”;
- 客戶端和服務器都可以創建流,雙方互不干擾;
- 流是雙向的,一個流里面客戶端和服務器都可以發送或接收數據幀,也就是一個“請求 - 應答”來回;
- 流之間沒有固定關系,彼此獨立,但流內部的幀是有嚴格順序的;
- 流可以設置優先級,讓服務器優先處理,比如先傳HTML/CSS,后傳圖片,優化用戶體驗;
- 流 ID 不能重用,只能順序遞增,客戶端發起的 ID是奇數,服務器端發起的 ID 是偶數;
- 【注意“發起”的含義:客戶端發起的流ID是奇數,針對此次請求服務端響應的流ID也是奇數】
- 在流上發送“RST_STREAM”幀可以隨時終止流,取消接收或發送;
- 第 0號流比較特殊,不能關閉,也不能發送數據幀,只能發送控制幀,用于流量控制。
下圖顯示了連接中無序的幀是如何依據流 ID 重組成流的。

從這些特性中,我們還可以推理出一些深層次的知識點。
比如說,HTTP/2在一個連接上使用多個流收發數據,那么它本身默認就會是長連接,所以永遠不需要“Connection”頭字段(keepalive 或 close)。
又比如,下載大文件的時候想取消接收,在 HTTP/1里只能斷開 TCP 連接重新“三次握手”,成本很高,而在HTTP/2里就可以簡單地發送一個“RST_STREAM”中斷流,
而長連接會繼續保持。
再比如,因為客戶端和服務器兩端都可以創建流,而流 ID有奇數偶數和上限的區分,所以大多數的流 ID都會是奇數(客戶端發起請求占了絕大部分),而且客戶端在一個連接里最多只能發出2^30,也就是 10 億個請求。所以就要問了:ID用完了該怎么辦呢?這個時候可以再發一個控制幀“GOAWAY”,真正關閉 TCP 連接。
★,流狀態轉換
★,
※,32 | 未來之路:HTTP/3展望

★,QUIC 選定了 UDP,在它之上把 TCP的那一套連接管理、擁塞窗口、流量控制等“搬”了過來,“去其糟粕,取其精華”,打造出了一個全新的可靠傳輸協議,可以認為是“新時代的 TCP”。
★,
※,33 | 我應該遷移到HTTP/2嗎?
★,主要看流量情況。tmall.com,qq.com等已經使用了HTTP/2。
※,34 | Nginx:高性能的Web服務器
本講結合 HTTP 協議來講Nginx,帶你窺視一下 HTTP 處理的內幕,看看 Web服務器的工作原理。
★,進程池
nginx 是個“輕量級”的 Web 服務器,那么這個所謂的“輕量級”是什么意思呢?“輕量級”是相對于“重量級”而言的。“重量級”就是指服務器進程很“重”,占用很多資源,當處理 HTTP請求時會消耗大量的 CPU和內存,受到這些資源的限制很難提高性能。而 Nginx 作為“輕量級”的服務器,它的CPU、內存占用都非常少,同樣的資源配置下就能夠為更多的用戶提供服務,其奧秘在于它獨特的工作模式.

在 Nginx 之前,Web服務器的工作模式大多是“Per-Process”或者“Per-Thread”,對每一個請求使用單獨的進程或者線程處理。這就存在創建進程或線程的成本,還會有進程、線程“上下文切換”的額外開銷。如果請求數量很多,CPU就會在多個進程、線程之間切換時“疲于奔命”,平白地浪費了計算時間。Nginx則完全不同,“一反慣例”地沒有使用多線程,而是使用了“`進程池 + 單線程`”的工作模式。【總結:Nginx采用“master/workers”進程池架構,不使用多線程,消除了進程、線程切換的成本】
Nginx 在啟動的時候會預先創建好固定數量的 worker進程,在之后的運行過程中不會再 fork出新進程,這就是進程池,而且可以自動把進程“綁定”到獨立的 CPU上,這樣就完全消除了進程創建和切換的成本,能夠充分利用多核 CPU 的計算能力。在進程池之上,還有一個“master”進程,專門用來管理進程池。它的作用有點像是 supervisor(一個用 Python編寫的進程管理工具),用來監控進程,自動恢復發生異常的worker,保持進程池的穩定和服務能力。不過 master 進程完全是 Nginx 自行用 C語言實現的,這就擺脫了外部的依賴,簡化了 Nginx的部署和配置。
Nginx 自1.7.11 開始引入了“多線程”,但只是作為輔助手段,卸載阻塞的磁盤I/O 操作,主要的HTTP請求處理使用的還是單線程里的epoll。
★,I/O 多路復用
使用多線程能夠很容易實現并發處理。但多線程也有一些缺點,除了剛才說到的“上下文切換”成本,還有編程模型復雜、數據競爭、同步等問題,寫出正確、快速的多線程程序并不是一件容易的事情。所以 Nginx就選擇了單線程的方式,帶來的好處就是開發簡單,沒有互斥鎖的成本,減少系統消耗。那么,疑問也就產生了:為什么單線程的
Nginx,處理能力卻能夠超越其他多線程的服務器呢?
這要歸功于 Nginx 利用了 Linux 內核里的一件“神兵利器”,I/O 多路復用接口,“大名鼎鼎”的 epoll。Web 服務器從根本上來說是“I/O 密集型”而不是“CPU密集型”,處理能力的關鍵在于網絡收發而不是 CPU計算(這里暫時不考慮 HTTPS 的加解密),而網絡 I/O會因為各式各樣的原因不得不等待,比如數據還沒到達、對端沒有響應、緩沖區滿發不出去等等。這種情形就有點像是 HTTP里的“隊頭阻塞”。對于一般的單線程來說 CPU就會“停下來”,造成浪費。多線程的解決思路有點類似“并發連接”,雖然有的線程可能阻塞,但由于多個線程并行,總體上看阻塞的情況就不會太嚴重了。Nginx 里使用的 epoll,就好像是 HTTP/2里的“多路復用”技術,它把多個 HTTP請求處理打散成碎片,都“復用”到一個單線程里,不按照先來后到的順序處理,而是只當連接上真正可讀、可寫的時候才處理,如果可能發生阻塞就立刻切換出去,處理其他的請求。通過這種方式,Nginx 就完全消除了 I/O 阻塞,把 CPU利用得“滿滿當當”,又因為網絡收發并不會消耗太多 CPU計算能力,也不需要切換進程、線程,所以整體的 CPU負載是相當低的。這里我畫了一張 Nginx“I/O多路復用”的示意圖,你可以看到,它的形式與 HTTP/2的流非常相似,每個請求處理單獨來看是分散、阻塞的,但因為都復用到了一個線程里,所以資源的利用率非常高。

epoll還有一個特點,大量的連接管理工作都是在操作系統內核里做的,這就減輕了應用程序的負擔,所以 Nginx可以為每個連接只分配很小的內存維護狀態,即使有幾萬、
幾十萬的并發連接也只會消耗幾百 M 內存,而其他的 Web服務器這個時候早就“Memory not enough”了。
★,多階段處理
有了“進程池”和“I/O 多路復用”,Nginx 是如何處理 HTTP請求的呢?
Nginx 在內部也采用的是“化整為零”的思路,把整個 Web服務器分解成了多個“功能模塊”,就好像是樂高積木,可以在配置文件里任意拼接搭建,從而實現了高度的靈活性和擴展性。Nginx 的 HTTP 處理有四大類模塊:
- handler 模塊:直接處理 HTTP 請求;
- filter 模塊:不直接處理請求,而是加工過濾響應報文;
- upstream模塊:實現反向代理功能,轉發請求到其他服務器;
- balance 模塊:實現反向代理時的負載均衡算法。
因為 upstream 模塊和 balance 模塊實現的是代理功能,Nginx作為“中間人”,運行機制比較復雜,所以我今天只講 handler模塊和 filter 模塊。
nginx 里的 handler 模塊和 filter模塊是按照“職責鏈”模式設計和組織的,HTTP請求報文就是“原材料”,各種模塊就是工廠里的工人,走完模塊構成的“流水線”,出來的就是處理完成的響應報文。下面的這張圖顯示了 Nginx 的“流水線”,在 Nginx里的術語叫“階段式處理”(Phases),一共有 11個階段,每個階段里又有許多各司其職的模塊。

注:
- 此“流水線”圖沒有畫出 flter 模塊所在的位置,它其實是在 CONTENT 階段的末尾專門“過”響應數據。
- Nginx的“PRECONTENT”階段在1.13.3 之前叫“TRY_FILES”,僅供Nginx 內部使用,用戶不可介入。
簡單列舉幾個模塊:
- charset 模塊實現了字符集編碼轉換;(第 15 講)
- chunked 模塊實現了響應數據的分塊傳輸;(第 16 講)
- range 模塊實現了范圍請求,只返回數據的一部分;(第16 講)
- rewrite模塊實現了重定向和跳轉,還可以使用內置變量自定義跳轉的 URI;(第 18 講)
- not_modified模塊檢查頭字段“if-Modified-Since”和“If-None-Match”,處理條件請求;(第 20 講)
- realip模塊處理“X-Real-IP”“X-Forwarded-For”等字段,獲取客戶端的真實 IP 地址;(第 21 講)
- ssl 模塊實現了 SSL/TLS協議支持,讀取磁盤上的證書和私鑰,實現 TLS 握手和SNI、ALPN 等擴展功能;(安全篇)
- http_v2 模塊實現了完整的 HTTP/2 協議。(飛翔篇)
在這張圖里,你還可以看到 limit_conn、limit_req、access、log等其他模塊,它們實現的是限流限速、訪問控制、日志等功能,不在 HTTP 協議規定之內,但對于運行在現實世界的 Web服務器卻是必備的。
★,tips
如何讓 Web 服務器能夠高效地處理 10K 以上的并發請求(Concurrent 10K),這就是著名的“C10K 問題”,當然它早已經被 epoll/kqueue 等解決了,現在的新問題是“C10M”
※,35 | OpenResty:更靈活的Web服務器
★,OpenResty 是什么
nginx依賴于磁盤上的靜態配置文件,修改后必須重啟才能生效,缺乏靈活性;特別是對于擁有成千上萬臺服務器的網站來說,僅僅增加或者刪除一行配置就要分發、重啟所有的機器,對運維是一個非常大的挑戰,要耗費很多的時間和精力,成本很高,很不靈活,難以“隨需應變”。
那么,有沒有這樣的一個 Web 服務器,它有 Nginx的優點卻沒有 Nginx的缺點,既輕量級、高性能,又靈活、可動態配置呢?這就是我今天要說的 OpenResty,它是一個“更好更靈活的Nginx”。OpenResty 基于Nginx,打包了很多有用的模塊和庫,是一個高性能的 Web開發平臺;
OpenResty 的核心是 Nginx,但它又超越了Nginx,關鍵就在于其中的 ngx_lua 模塊,把小巧靈活的 Lua語言嵌入了 Nginx,可以用腳本的方式操作 Nginx內部的進程、多路復用、階段式處理等各種構件。OpenResty 還把 Lua 自身的協程與 Nginx的事件機制完美結合在一起,優雅地實現了許多其他語言所沒有的“同步非阻塞”編程范式,能夠輕松開發出高性能的 Web 應用。目前 OpenResty有兩個分支,分別是開源、免費的“OpenResty”和閉源、商業產品的“OpenResty+"。
★,動態的 Lua
★,高效率的 Lua
OpenResty 能夠高效運行的一大“秘技”是它的“同步非阻塞”編程范式,如果你要開發 OpenResty應用就必須時刻銘記于心。
“同步非阻塞”本質上還是一種“多路復用”,我拿上一講的Nginx epoll 來對比解釋一下。
epoll 是操作系統級別的“多路復用”,運行在內核空間。而OpenResty 的“同步非阻塞”則是基于 Lua 內建的“協程”,是應用程序級別的“多路復用”,運行在用戶空間,
所以它的資源消耗要更少。
OpenResty 里每一段 Lua 程序都由協程來調度運行。和 Linux的 epoll一樣,每當可能發生阻塞的時候“協程”就會立刻切換出去,執行其他的程序。這樣單個處理流程是“阻塞”的,但整個OpenResty 卻是“非阻塞的”,多個程序都“復用”在一個 Lua虛擬機里運行。

下面的代碼是一個簡單的例子,讀取 POST 發送的 body數據,然后再發回客戶端:
ngx.req.read_body() -- 同步非阻塞 (1)
local data = ngx.req.get_body_data()
if data then
ngx.print("body: ", data) -- 同步非阻塞 (2)
end
代碼中的“ngx.req.read_body”和“ngx.print”分別是數據的收發動作,只有收到數據才能發送數據,所以是“同步”的。但即使因為網絡原因沒收到或者發不出去,OpenResty也不會在這里阻塞“干等著”,而是做個“記號”,把等待的這段CPU時間用來處理其他的請求,等網絡可讀或者可寫時再“回來”接著運行。
【擴展:同步、異步 、阻塞、非阻塞】
參考此文。
同步與異步關注的是消息通信機制(synchronous communication/ asynchronous communication)。
- 同步異步是對于被調用方而言。
- 同步:調用方發起調用后,被調用方必須計算出真實結果之后才返回。
- 異步:調用方發起調用后,被調用方立即返回給調用方一個結果,真實的結果慢慢計算,算出真實結果后通過“狀態”,“通知”,“回調”這三種方式告訴調用方。
阻塞和非阻塞關注的是程序在等待調用結果(消息,返回值)時的狀態。
- 阻塞和非阻塞是對于調用方而言。
- 阻塞:調用法發起調用后,不管被調用方是同步返回結果還是異步返回結果,調用方都一直掛起等待被調用方返回真實的結果。
- 異步阻塞:盡管被調用方立即返回給調用方一個結果,但是調用方還是一直等待被調用方返回真實的結果。這種情況很少使用
- 非阻塞:調用法發起調用后,不管被調用方是同步返回結果還是異步返回結果,調用方都不會將自己掛起等待,而是去做其他的事情。
- 同步非阻塞:調用法發起調用后,被調用方采取同步方式,在被調用方計算真實結果的過程中,被調用方可以去做其他事情,但是會周期性的向被調用方發起輪詢查詢真實結果是否已經計算出來。即:同步非阻塞時調用方獲取真實結果的方式一般是采用輪詢。
★,
※,36 | WAF:保護我們的網絡服務
★,Web 服務遇到的威脅
- “DDoS”攻擊(distributed denial-of-service attack),有時候也叫“洪水攻擊”。
- “CC 攻擊”(Challenge Collapsar) 是“DDoS”的一種,它使用代理服務器發動攻擊
- SQL 注入 (SQL inject)
- HTTP 頭注入:“User-Agent”“X-Forwarded-For”等字段里加入了惡意數據或代碼,服務端程序如果解析不當,就會執行預設的惡意代碼
- 跨站腳本”(XSS)攻擊:利用 Cookie的攻擊手段,屬于“JS 代碼注入”,利用 JavaScript 腳本獲取未設防的 Cookie。
★,網絡應用防火墻
傳統的“防火墻”工作在三層或者四層,隔離了外網和內網,使用預設的規則,只允許某些特定 IP地址和端口號的數據包通過,拒絕不符合條件的數據流入或流出內網,實質上是一種網絡數據過濾設備。
網絡應用防火墻 (Web Application Firewall)了,簡稱為“WAF",也是一種“防火墻”,但它工作在七層,看到的不僅是 IP地址和端口號,還能看到整個 HTTP報文,所以就能夠對報文內容做更深入細致的審核,使用更復雜的條件、規則來過濾數據。說白了,WAF 就是一種“HTTP 入侵檢測和防御系統”。
通常一款產品能夠稱為 WAF,要具備下面的一些功能:
- IP黑名單和白名單,拒絕黑名單上地址的訪問,或者只允許白名單上的用戶訪問;
- URI 黑名單和白名單,與 IP黑白名單類似,允許或禁止對某些 URI 的訪問;
- 防護 DDoS 攻擊,對特定的 IP 地址限連限速;
- 過濾請求報文,防御“代碼注入”攻擊;
- 過濾響應報文,防御敏感信息外泄;
- 審計日志,記錄所有檢測到的入侵操作。
好像很高深,但如果你理解了它的工作原理,其實也不難。如果你比較熟悉Apache、Nginx、OpenResty,可以自己改改配置文件,寫點 JS或者 Lua 代碼,就能夠實現基本的 WAF 功能。
比如說,在 Nginx 里實現 IP地址黑名單,可以利用“map”指令,從變量 $remote_addr 獲取IP 地址,在黑名單上就映射為值 1,然后在“if”指令里判斷:
map $remote_addr $blocked {
default 0;
"1.2.3.4" 1;
"5.6.7.8" 1;
}
if ($blocked) {
return 403 "you are blocked.";
}
Nginx的配置文件只能靜態加載,改名單必須重啟,比較麻煩。如果換成 OpenResty 就會非常方便,在 access階段進行判斷,IP 地址列表可以使用 cosocket 連接外部的Redis、MySQL 等數據庫,實現動態更新:
local ip_addr = ngx.var.remote_addr
local rds = redis:new()
if rds:get(ip_addr) == 1 then
ngx.exit(403)
end
雖然自己實現難度不是很大,但是網絡安全領域必須時刻記得“木桶效應”(也叫“短板效應”),使用 WAF 最好“不要重新發明輪子”,而是使用現有的、比較成熟的、經過實際考驗的 WAF產品。
★,全面的 WAF 解決方案
這里要“隆重”介紹一下 WAF 領域里的最頂級產品了:ModSecurity,它可以說是 WAF 界“事實上的標準”。
ModSecurity可以以模塊的形式集成到NGINX中。
在Nginx 上還可以使用另一個 WAF 模塊Naxsi,雖然它的功能也很強大,但與ModSecurity 并不兼容。
OpenResty 生態系統里也已經有了多個比較成熟的純 Lua WAF,可以在 GitHub 上搜索關鍵詞“ngx lua waf”,而商業版的 OpenResty+則基于ModSecurity 的核心規則集,使用 Lua重新實現了一套自有引擎
★,
※,37 | CDN:加速我們的網絡服務
協議方面,HTTPS 強化通信鏈路安全、HTTP/2優化傳輸效率;應用方面,Nginx/OpenResty提升網站服務能力,WAF抵御網站入侵攻擊,在應用領域,還缺一個在外部加速 HTTP協議的服務:CDN(Content Delivery Network 或 Content Distribution Network),中文名叫“內容分發網絡”。它是專門為解決“長距離”上網絡訪問速度慢而誕生的一種網絡應用服務。
由于客觀地理距離的存在,直連網站訪問速度會很慢,所以就出現了 CDN;CDN構建了全國、全球級別的專網,讓用戶就近訪問專網里的邊緣節點,降低了傳輸延遲,實現了網站加速;
CDN 的最核心原則是“就近訪問”,如果用戶能夠在本地幾十公里的距離之內獲取到數據,那么時延就基本上變成 0 了。
★,CDN 的負載均衡
CDN有兩個關鍵組成部分:全局負載均衡和緩存系統,對應的是 DNS(第 6 講)和緩存代理(第21 講、第 22 講)技術。
- CDN 里除了核心的負載均和緩存系統,還有其他的輔助系統,比如管理、監控、日志、統計、計費等。
全局負載均衡(Global Sever Load Balance)一般簡稱為GSLB,它是 CDN的“大腦”,主要的職責是當用戶接入網絡的時候在 CDN專網中挑選出一個“最佳”節點提供服務,解決的是用戶如何找到“最近的”邊緣節點,對整個 CDN網絡進行“負載均衡”。
GSLB 最常見的實現方式是“DNS 負載均衡”,這個在第 6講里也說過,不過 GSLB 的方式要略微復雜一些。原來沒有 CDN 的時候,權威 DNS返回的是網站自己服務器的實際 IP 地址,瀏覽器收到 DNS解析結果后直連網站。但加入 CDN 后就不一樣了,權威 DNS 返回的不是 IP地址,而是一個 CNAME( Canonical Name )別名記錄,指向的就是 CDN 的 GSLB。它有點像是 HTTP/2里“Alt-Svc”的意思,告訴外面:“我這里暫時沒法給你真正的地址,你去另外一個地方再查查看吧。”因為沒拿到 IP 地址,于是本地 DNS 就會向 GSLB再發起請求,這樣就進入了 CDN的全局負載均衡系統,開始“智能調度”,主要的依據有這么幾個:
看用戶的 IP地址,查表得知地理位置,找相對最近的邊緣節點;
看用戶所在的運營商網絡,找相同網絡的邊緣節點;
檢查邊緣節點的負載情況,找負載較輕的節點;
其他,比如節點的“健康狀況”、服務能力、帶寬、響應時間等。GSLB把這些因素綜合起來,用一個復雜的算法,最后找出一臺“最合適”的邊緣節點,把這個節點的 IP地址返回給用戶,用戶就可以“就近”訪問 CDN的緩存代理了。
CDN 的緩存代理:緩存系統是 CDN 的另一個關鍵組成部分,相當于 CDN的“心臟”。如果緩存系統的服務能力不夠,不能很好地滿足用戶的需求,那 GSLB調度算法再優秀也沒有用。
目前國內的CDN廠商內部都是基于開源軟件定制的。最常用的是專門的緩存代理軟件 Squid、Varnish,還有新興的ATS(Apache Traffic Server),而 Nginx 和 OpenResty 作為Web服務器領域的“多面手”,憑借著強大的反向代理能力和模塊化、易于擴展的優點,也在 CDN 里占據了不少的份額。
★,小結
CDN 發展到現在已經有二十來年的歷史了,早期的 CDN功能比較簡單,只能加速靜態資源。隨著這些年 Web2.0、HTTPS、視頻、直播等新技術、新業務的崛起,它也在不斷進步,增加了很多的新功能,比如 SSL加速、內容優化(數據壓縮、圖片格式轉換、視頻轉碼)、資源防盜鏈、WAF 安全防護等等。現在,再說 CDN是“搬運工”已經不太準確了,它更像是一個“無微不至”的“網站保姆”,讓網站只安心生產優質的內容,其他的“雜事”都由它去代勞。
目前應用最廣泛的 DNS 軟件是開源的 BIND9(Berkeley Internet Name Domain),而OpenResty 則使用stream_lua 實現了純 Lua的 DNS 服務。
CDN大廠 CloudFlare 的系統就都是由Nginx/OpenResty 驅動的,而 OpenResty 公司的主要商業產品“OpenResty Edge”也是CDN。
當前的 CDN 也有了“云化”的趨勢,很多云廠商都把 CDN 作為一項“標配”服務。
★,
※,38 | WebSocket:沙盒里的TCP
★,websocket
- webSocket”是一種基于 TCP的輕量級網絡通信協議,在地位上是與 HTTP“平級”的。
- WebSocket 與 HTTP/2 一樣,都是為了解決 HTTP某方面的缺陷而誕生的。HTTP/2 針對的是“隊頭阻塞”,而WebSocket 針對的是“請求 - 應答”通信模式。
- WebSocket 是一個“全雙工”的通信協議,相當于對 TCP做了一層“薄薄的包裝”,讓它運行在瀏覽器環境里;WebSocket 使用兼容 HTTP 的 URI來發現服務,但定義了新的協議名“ws”和“wss”,端口號也沿用了 80 和 443;
- 瀏覽器是一個“沙盒”環境,有很多的限制,不允許建立 TCP連接收發數據,而有了WebSocket,我們就可以在瀏覽器里與服務器直接建立“TCP連接”,獲得更多的自由。不過自由也是有代價的,WebSocket雖然是在應用層,但使用方式卻與“TCPSocket”差不多,過于“原始”,用戶必須自己管理連接、緩存、狀態,開發上比 HTTP 復雜的多,所以是否要在項目中引入
WebSocket 必須慎重考慮
★,websocket的握手
websocket利用了 HTTP身的“協議升級”特性,“偽裝”成HTTP,這樣就能繞過瀏覽器沙盒、網絡防火墻等等限制,
WebSocket 的握手是一個標準的 HTTP GET請求,但要帶上兩個協議升級的專用頭字段:“Connection: Upgrade”,表示要求協議“升級”;“Upgrade: websocket”,表示要“升級”成 WebSocket 協議。另外,為了防止普通的 HTTP 消息被“意外”識別成WebSocket,握手消息還增加了兩個額外的認證用頭字段(所謂的“挑戰”,Challenge):Sec-WebSocket-Key:一個 Base64 編碼的 16字節隨機數,作為簡單的認證密鑰;Sec-WebSocket-Version:協議的版本號,當前必須是 13。
服務器收到 HTTP請求報文,看到上面的四個字段,就知道這不是一個普通的GET 請求,而是 WebSocket 的升級請求,于是就不走普通的HTTP 處理流程,而是構造一個特殊的“101 Switching
Protocols”響應報文,通知客戶端,接下來就不用 HTTP了,全改用 WebSocket 協議通信。
WebSocket的握手響應報文也是有特殊格式的,要用字段“Sec-WebSocket-Accept”驗證客戶端請求報文,同樣也是為了防止誤連接.
※,39 | HTTP性能優化面面觀(上)
性能優化是一個復雜的概念,在 HTTP里可以分解為服務器性能優化、客戶端性能優化和傳輸鏈路優化
★,HTTP服務器性能
衡量服務器性能的主要指標有三個:吞吐量(requests per second)、并發數(concurrency)和響應時間(time per request)。
- 吞吐量就是我們常說的 RPS,每秒的請求次數,也有叫TPS、QPS,它是服務器最基本的性能指標,RPS越高就說明服務器的性能越好。
- 并發數反映的是服務器的負載能力,也就是服務器能夠同時支持的客戶端數量,當然也是越多越好,能夠服務更多的用戶。
- 響應時間反映的是服務器的處理能力,也就是快慢程度,響應時間越短,單位時間內服務器就能夠給越多的用戶提供服務,提高吞吐量和并發數。
- 除了上面的三個基本性能指標,服務器還要考慮CPU、內存、硬盤和網卡等系統資源的占用程度,利用率過高或者過低都可能有問題。
在 Linux 上,最常用的性能測試工具可能就是 ab(Apache Bench)了,比如,下面的命令指定了并發數 100,總共發送10000 個請求:·ab -c 100 -n 10000 'http://www.xxx.com'·
系統資源監控方面,Linux 自帶的工具也非常多,常用的有uptime、top、vmstat、netstat、sar等等,可能你比我還要熟悉,我就列幾個簡單的例子吧:
top # 查看 CPU 和內存占用情況
vmstat 2 # 每 2 秒檢查一次系統狀態
sar -n DEV 2 # 看所有網卡的流量,定時 2 秒檢查
理解了這些性能指標,我們就知道了服務器的性能優化方向:合理利用系統資源,提高服務器的吞吐量和并發數,降低響應時間。
更高級的服務器性能測試工具有 LoadRunner、JMeter 等,很多云服務商也會提供專業的測試平臺。
★,HTTP 客戶端性能
HTTP客戶端基本的性能指標就是“延遲”(latency)。?影響因素有地理距離、帶寬、DNS 查詢、TCP 握手等;
之前講 HTTPS 時介紹過一個專門的網站“SSLLabs”(https://www.ssllabs.com/),而對于HTTP 性能優化,也有一個專門的測試網站“WebPageTest”(https://www.webpagetest.org/)。它的特點是在世界各地建立了很多的測試點,可以任意選擇地理位置、機型、操作系統和瀏覽器發起測試,非常方便,用法也很簡單。網站測試的最終結果是一個直觀的“瀑布圖”(WaterfallChart),清晰地列出了頁面中所有資源加載的先后順序和時間消耗。
★,
※,40 | HTTP性能優化面面觀(下)
上一講里我說到了,在整個 HTTP系統里有三個可優化的環節,分別是服務器、客戶端和傳輸鏈路(“第一公里”和“中間一公里”)。但因為我們是無法完全控制客戶端的,所以實際上的優化工作通常是在服務器端。這里又可以細分為后端和前端,后端是指網站的后臺服務,而前端就是HTML、CSS、圖片等展現在客戶端的代碼和數據。
★,花錢的
投資購買現成的硬件最簡單的優化方式。
花錢購買外部的軟件或者服務。也是一種行之有效的優化方式,最“物有所值”的應該算是 CDN了。CDN專注于網絡內容交付,幫助網站解決“中間一公里”的問題,還有很多其他非常專業的優化功能。把網站交給 CDN運營,就好像是“讓網站坐上了噴氣飛機”,能夠直達用戶,幾乎不需要費什么力氣就能夠達成很好的優化效果。
★,網站內部、“不花錢”的軟件優化
這方面的 HTTP 性能優化概括為三個關鍵詞:開源、節流、緩存。
開源:是指抓“源頭”,開發網站服務器自身的潛力,在現有條件不變的情況下盡量挖掘出更多的服務能力
- 選用高性能的 Web服務器,最佳選擇當然就是 Nginx/OpenResty了,盡量不要選擇基于 Java、Python、Ruby的其他服務器,它們用來做后面的業務邏輯服務器更好。利用Nginx 強大的反向代理能力實現“動靜分離”,動態頁面交給Tomcat、Django、Rails,圖片、樣式表等靜態資源交給 Nginx。Nginx 或者 OpenResty自身也有很多配置參數可以用來進一步調優,舉幾個例子,比如說禁用負載均衡鎖、增大連接池,綁定 CPU等等。特別要說的是,對于 HTTP 協議一定要啟用長連接。另外,在現代操作系統上都已經支持 TCP 的新特性“TCP Fast Open”(Win10、iOS9、Linux 4.1),它的效果類似 TLS的“False Start”,可以在初次握手的時候就傳輸數據,也就是o-RTT,所以我們應該盡可能在操作系統和 Nginx里開啟這個特性,減少外網和內網里的握手延遲。下面給出一個簡短的 Nginx配置示例,啟用了長連接等優化參數,實現了動靜分離:
server { listen 80 deferred reuseport backlog=4096 fastopen=1024; keepalive_timeout 60; keepalive_requests 10000; location ~* \.(png)$ { root /var/images/png/; } location ~* \.(php)$ { proxy_pass http://php_back_end; } }
節流:“節流”是指減少客戶端和服務器之間收發的數據量,在有限的帶寬里傳輸更多的內容。“節流”最基本的做法就是使用 HTTP協議內置的“數據壓縮”編碼,不僅可以選擇標準的gzip,還可以積極嘗試新的壓縮算法br,它有更好的壓縮效果。不過在數據壓縮的時候應當注意選擇適當的壓縮率,不要追求最高壓縮比,否則會耗費服務器的計算資源,增加響應時間,降低服務能力,反而會“得不償失”。
gzip 和 br 是通用的壓縮算法,對于 HTTP協議傳輸的各種格式數據,我們還可以有針對性地采用特殊的壓縮方式。
- HTML/CSS/JS屬于純文本,就可以采用特殊的“壓縮”,去掉源碼里多余的空格、換行、注釋等元素。這樣“壓縮”之后的文本雖然看起來很混亂,對“人類”不友好,但計算機仍然能夠毫無障礙地閱讀
- 圖片在 HTTP傳輸里占有非常高的比例,雖然它本身已經被壓縮過了,不能被 gzip、br處理,但仍然有優化的空間。比如說,去除圖片里的拍攝時間、地點、機型等元數據,適當降低分辨率,縮小尺寸。圖片的格式也很關鍵,盡量選擇高壓縮率的格式,有損格式應該用 JPEG,無損格式應該用 Webp 格式。對于小文本或者小圖片,還有一種叫做“資源合并”(Concatenation)的優化方式,就是把許多小資源合并成一個大資源,用一個請求全下載到客戶端,然后客戶端再用 JS、CSS切分后使用,好處是節省了請求次數,但缺點是處理比較麻煩。
壓縮之外,“節流”還有兩個優化點,就是域名和重定向。
- 應當適當“收縮”域名,限制在兩三個左右,減少解析完整域名所需的時間,讓客戶端盡快從系統緩存里獲取解析結果。
- 重定向引發的客戶端延遲也很高,它不僅增加了一次請求往返,還有可能導致新域名的 DNS 解析,是 HTTP前端性能優化的“大忌”。除非必要,應當盡量不使用重定向,或者使用 Web 服務器的“內部重定向”。
緩存:緩存是無論何時都不能忘記的性能優化利器,應該總使用Etag 或 Last-modified 字段標記資源;
HTTP/2: 在“開源”“節流”和“緩存”這三大策略之外,HTTP性能優化還有一個選擇,那就是把協議由 HTTP/1 升級到HTTP/2。通過“飛翔篇”的學習,你已經知道了 HTTP/2的很多優點,它消除了應用層的隊頭阻塞,擁有頭部壓縮、二進制幀、多路復用、流量控制、服務器推送等許多新特性,大幅度提升了 HTTP 的傳輸效率。實際上這些特性也是在“開源”和“節流”這兩點上做文章,但因為這些都已經內置在了協議內,所以只要換上HTTP/2,網站就能夠立刻獲得顯著的性能提升。不過你要注意,一些在 HTTP/1 里的優化手段到了 HTTP/2里會有“反效果”。對于 HTTP/2 來說,一個域名使用一個 TCP連接才能夠獲得最佳性能,如果開多個域名,就會浪費帶寬和服務器資源,也會降低 HTTP/2的效率,所以“域名收縮”在 HTTP/2 里是必須要做的。“資源合并”在 HTTP/1 里減少了多次請求的成本,但在HTTP/2里因為有頭部壓縮和多路復用,傳輸小文件的成本很低,所以合并就失去了意義。而且“資源合并”還有一個缺點,就是降低了緩存的可用性,只要一個小文件更新,整個緩存就完全失效,必須重新下載。所以在現在的大帶寬和 CDN應用場景下,應當盡量少用資源合并(JS、CSS圖片合并,數據內嵌),讓資源的粒度盡可能地小,才能更好地發揮緩存的作用。
★,
★,
※,1
★,
★,
★,
浙公網安備 33010602011771號