1.HTTP
當我們瀏覽網頁時,地址欄中使用最多的多是https://開頭的url,它與我們所學的http協議有什么區別?
http協議又叫超文本傳輸協議,它是應用層中使用最多的協議,
http與我們常說的socket有什么區別嗎?
我們使用的網絡可以分為(會話層和表示層可以忽略),每一層使用下一層的功能,并為上一層提供接口,我們經常聽說的http協議就是應用層的協議,其中應用層協議包括ftp等等,而應用層還需要使用傳輸層的協議,http使用的就是tcp協議,http3計劃使用udp協議。不過不管是tcp還是udp都是使用網絡層的ip協議。不過傳輸層及以下層都是在操作系統內核中的,不利于我們使用,所以在用戶態設計了socket接口來幫助我們使用tcp或者udp的網絡協議。而http協議則是已經定義好解析標準的能讓我們直接使用的協議。
1.1 http1.0
http1.0是個短連接,即每發一次請求,就建立一次連接,一次響應后就釋放連接,這種方式簡化了http的請求響應,但是卻造成了重大的浪費:不能并行請求,每次請求都需要建立連接和釋放連接,導致每次請求都需要三次握手和四次揮手。
1.2 http1.1
http1.1為了優化http1.0加入了很多東西。
①keep-alive(長連接):長連接,每個連接完成一次請求后先不關閉,在一定時間內可以發送多次請求。通過設置請求頭Connection: keep-alive來實現
http1.1的長連接減少了三次握手和四次揮手的次數,不過需要前一次請求的響應返回后才能發送下一次請求,但是有個問題,在不釋放連接的情況下如何才能判斷請求已經結束,例如瀏覽器怎么知道數據傳輸完畢了?http1.10的短連接和http1.1的長連接
- 通過 Content-Length 的長度信息,判斷響應數據傳輸完畢。但是如果是大文件或者動態生成的數據呢?這些數據一開始可能不知道它們的長度,這時候就需要用到Transfer-Encoding: chunked請求頭,這個請求頭標識需要響應使用數據分塊傳輸,并且每個塊會有長度和數據,如果接收到長度為0的塊標識數據接收完畢。http長連接? --? http請求與連接的關系
②pipelining(管道化技術):雖然長連接減少了握手消耗,但是每次都要前一次請求完成之后才能發送下一次請求,管道化技術允許在一個請求沒有響應之前發送多次請求。不過管道話技術要求響應必須按照請求發送的順序返回,否則無法解析。存在隊頭阻塞。實際使用不多。
③增加并發連接數量與多域名:http1為了實現多并發,增加http連接的數量。
2.HTTPS
上面所說的http協議都是明文傳輸的,而且不會對應用數據和主機進行驗證,這樣即使數據被竊取和篡改我們也不得而知,所以為了保證http的安全,在它的基礎上加了TSL/SSL安全協議。形成了我們所說的https協議。
先盜一張https驗證的過程圖
微信圖片_20210802184236.jpg
信息的安全傳輸解決三個問題,保密性,完整性與端點鑒別。
(a,b為傳輸方,c為攻擊方)
1.保密性
http之所以不安全最大的原因就是使用明文傳輸,所以加密成了最大的問題,目前分為兩大類加密算法(md5由于不能解密,不能算作加密算法)
①對稱加密:DES(分組加密算法),AES(可以看作是DES的升級版)
②非對稱加密:RSA、DSA
對稱密鑰加密過程
由于目前公鑰加密算法的開銷比較大,所以一般采用對稱加密來保密信息。但是對稱加密過程,密鑰如何傳輸?這時可以使用公私鑰來進行傳輸,因為公私鑰使用公鑰加密,私鑰解密,所以只要是對方公鑰加密的數據只有對方的私鑰才能解密(私鑰不公開,保證了傳輸過程中,即使數據泄露也沒辦法解密)對應圖中④的過程:
note:不過又出現了一個問題,如何保證a得到的是b的公鑰,而不是別人偽造的,這就需要下面的端點鑒別。
2.完整性
為了防止http數據在傳輸的過程中被篡改,完整性也是必須要考慮的問題,如何保證信息的完整性。
①發送方使用自己的私鑰加密信息,接收方接收到后只能使用發送方的公鑰解密
②使用信息摘要算法md5,sha生成散列加在正文數據的后面
使用MAC鑒別碼保證報文完整
第一種方法由于中間人沒有發送方的私鑰所以只能解密發送傳輸的數據,但是不能修改數據,因為一旦中間人修改數據,接收方利用發送方的公鑰解密得到的數據就是錯誤的(信息中有專門的比特位用于驗證信息的正確性),但是向前面說的公私鑰加密開銷比較大,所以不建議采用。
信息摘要算法是利用散列函數將信息映射成一段固定長的數據,由于該算法具有不可逆性而且不同的數據散列得到的數據基本不會相同。所以接收方得到數據后,將正文數據散列得到的數據于散列值比對,如果不同,則正文數據遭篡改。
3.端點鑒別
保證了數據的完整性和保密性后,最終的問題就是如何確定發送方的問題。如何不進行端點鑒別,就可能會出現中間人攻擊的風險,如圖中間人攻擊端點A和端點B通信,但是中間人C攔截了請求并冒充B和A通信(中間人C將自己的公鑰發給A,讓A誤以為這是B的公鑰),然后C作為客戶端向端點B發請求(將A發送的報文解密后,用BC之間的共享密鑰加密),這樣A誤以為是在和B進行https加密通信,但是其實中間人已經知曉所有的報文。那么如何知曉這是公鑰是不是B的?
這就需要用到證書,證書是CA機構用自己的私鑰加密后的得到的東西,用來證明公鑰持有者的身份,證書包括以下東西:
- 持有者信息;
- 證書認證機構(CA)的信息;
- CA 對這份文件的數字簽名及使用的算法;
- 證書有效期以及一些額外信息;
CA是權威的受信任的證書頒發機構,CA的公鑰會內嵌在瀏覽器中。由于CA使用自己的私鑰加密證書,所以可以保證證書不可以偽造。服務器在發送公鑰的時候還要發送自己的證書,客戶端解密該證書的時候將證書中的公鑰與服務器發來的公鑰比較,如果兩者相同,則直接說明服務器發來的公鑰是可靠的;如果不相同說明服務器端身份有問題。例如上圖,中間人C將自己的公鑰和證書發給端點A,A解密證書,發現證書中的網站信息與B不同,說明訪問的端點不是B。
證書分為根證書和中間證書,我們我大部分的證書都是由中間CA機構頒發的,中間CA機構是受根CA機構信任的能夠頒發CA證書的機構,這樣就形成了一個證書鏈:客戶端信任操作系統瀏覽器->瀏覽器信任根CA機構->根CA機構信任中間CA機構->中間CA機構信任中間證書。所以瀏覽器對于收到的多級證書,需要從站點證書開始逐級驗證,直至出現操作系統或瀏覽器內置的受信任 CA 根證書。
證書鏈
2.1TLS的流程
https是在http的基礎上添加tls協議保證安全性的,那么tls的過程又是如何呢?SSL/TLS工作原理以及幾幅圖,拿下 HTTPS
tls握手的過程:這里以比較熟知的RSA密鑰交換算法為例說明,下面會說明DHE密鑰交換算法,RSA算法主要分為以下幾步:tls握手的過程
①Client Hello:握手第一步是客戶端向服務端發送 Client Hello 消息,這個消息里包含了一個客戶端生成的隨機數 Random1、客戶端支持的加密套件(Support Ciphers)和 SSL Version 等信息。
②Server Hello:第二步是服務端向客戶端發送 Server Hello 消息,這個消息會從 Client Hello 傳過來的 Support Ciphers 里確定一份加密套件,這個套件決定了后續加密和生成摘要時具體使用哪些算法,另外還會生成一份隨機數 Random2。注意,至此客戶端和服務端都擁有了兩個隨機數(Random1+ Random2),這兩個隨機數會在后續生成對稱秘鑰時用到。
③Certificate:這一步是服務端將自己的證書下發給客戶端,讓客戶端驗證自己的身份,客戶端驗證通過后取出證書中的公鑰。Server Key Exchange:如果是DH算法,這里發送服務器使用的DH參數。RSA算法不需要這一步。Server Hello Done:通知客戶端 Server Hello 過程結束。
④Certificate Verify:客戶端收到服務端傳來的證書后,先從 CA 驗證該證書的合法性,驗證通過后取出證書中的服務端公鑰,再生成一個隨機數 Random3,再用服務端公鑰非對稱加密 Random3生成 PreMaster Key。
⑤Client Key Exchange:上面客戶端根據服務器傳來的公鑰生成了 PreMaster Key,Client Key Exchange 就是將這個 key 傳給服務端,服務端再用自己的私鑰解出這個 PreMaster Key得到客戶端生成的 Random3。至此,客戶端和服務端都擁有 Random1+ Random2+ Random3,兩邊再根據同樣的算法就可以生成一份秘鑰,握手結束后的應用層數據都是使用這個秘鑰進行對稱加密。為什么要使用三個隨機數呢?這是因為 SSL/TLS 握手過程的數據都是明文傳輸的,并且多個隨機數種子來生成秘鑰不容易被暴力破解出來。
⑥Change Cipher Spec(Client):這一步是客戶端通知服務端后面再發送的消息都會使用前面協商出來的秘鑰加密了,是一條事件消息。Encrypted Handshake Message(Client Finish):客戶端將前面的握手消息生成摘要再用協商好的秘鑰加密,這是客戶端發出的第一條加密消息。服務端接收后會用秘鑰解密,能解出來說明前面協商出來的秘鑰是一致的。
⑦Change Cipher Spec(Server)和Encrypted Handshake Message(Server)與客戶端的意思基本相同,使用前面的加密數據和加密之前的握手消息。
2.2https的優化
①會話復用tls
https為了安全引入了tls,但是一次連接無形中又增加了tls握手的部分,而且要命的是每次都需要將這個過程走一遍。為了復用TLS過程,我們引入了session id與sesson ticket機制 Session會話恢復SessionID&SessionTicket
session id
session id是指一次tls握手完成后(第一次握手攜帶空的session id,服務器端就知道首次連接),服務器端生成session id返回,客戶端存下,而且客戶端會將生成的密鑰存放在tls層中,接著下次連接時客戶端攜帶此session id發起請求,服務器端收到后與內存中的session id比較,如果相同則使用上次的密鑰。發送一個Change Cipher Spec報文后,就發送加密后的驗證信息Finished。session ticket
session ticket與session id的過程差不多,但是session ticket為了解決有多臺服務器的問題(服務器負載均衡導致兩次請求到不同機器),所以session ticket是首次tls握手完成后服務端將加密協議,算法,參數等加密生成會話票據,服務器不存儲,傳給客戶端存儲,等客戶端下次tls握手時,服務器驗證票據并根據票據生成加密密鑰,同時生成新的NewSessionTicket防止過期。
②前向安全算法
普通的密鑰交換算法(rsa)需要使用公私鑰傳輸加密密鑰,如果私鑰一旦泄露,那么中間人就可以通過私鑰解密出加密密鑰當前會話以及后續會話就不安全了。為了安全性,設計出了ECDHE(DHE)算法TLS/SSL 協議中的RSA、ECDHE、ECDH流程與區別。
RSA算法DHE算法流程
上面那張圖是RSA算法交換加密密鑰的過程,下面的圖則是ECDHE算法交換密鑰的過程,ECDHE與DHE的過程相同,只是加密算法不同而已,下面以DHE算法流程為例說明:
(1):客戶端發送client hello申請DHE加密。
(2):服務器允許DHE算法并生成加密使用的參數p,q,然后服務器生成隨機數Xb作為自己的臨時私鑰,接著計算Pb = q^Xb mod p,最后發送sever hello,和server key exchange(包括Pb,p, q等加密參數)至客戶端,Xb僅自己保存。
(3):客戶端收到Pb后,生成自己的臨時私鑰Xa,接著計算Pa = q^Xa mod p,然后將Pa發送給服務器端,客戶端發送client key exchange (包括Pa)至服務器,
(4):客戶端計算Sa=Pb ^Xa mod p;服務器收到Pa后計算Sb= Pa^Xb mod p,DHE與ECDHE算法使用離散對數的概念保證了Sa與Sb的相同和高度保密性,所以密鑰交換成功,最后雙方通過S加解密數據。算法的加密過程可以查看DH密鑰交換算法,DHE與DH不同的是DHE使用臨時的私鑰,這樣可以保證即使一次通信密鑰被破解,但是其他的通信仍是安全的。
③TFO(tcp fast open)TFO詳解
傳統的http通信需要先進性3次握手后才能發送數據,但是為了提高速度,一般在第三次握手時會攜帶應用數據,即使服務器沒有收到,客戶端也可以在超時后重發,但是每次http通信還是需要浪費1RTT的時間?左邊是tcp三次握手發送數據,右邊是TFO應用流程
如圖為了彌補這種浪費,人們設計出了TFO通信方式,在第一次通信時按照正常的3次握手來完成,同時第一次通信完成后服務器端會發送cookie給客戶端,接下來的tcp通信客戶端會發送cookie與應用數據,這個過程就和上面的tsl復用的思路一樣。
- 如果cookie驗證成功,則將應用數據上傳給應用程序,并且服務器端會發送客戶端syn和應用數據確認的ack;
- 如果cookie驗證不成功,則服務器會將數據丟棄,然后只發送syn確認的ack,客戶端通過服務器端的ack就可以知道cookie是否驗證成功,如果不成功,則客戶端第三次握手的時候再發送一遍應用數據,這就又回到了沒有TFO的過程。
④hsts(HTTP Strict-Transport-Security)HSTS詳解-CSDN博客
在訪問網站的時候我們經常會直接敲域名www.baidu.com,而不會添加http://或者https://,而瀏覽器為了兼容http會先使用http詢問是否能夠進行https請求,如果瀏覽器只支持https會返回3XX狀態碼表示路徑已經改變。然后瀏覽器才會使用新的https://開頭的域名訪問server,但是這樣每次都會多發一次請求,而且還會造成中間人攻擊
hsts就是為了解決一個問題,當訪問過一次server之后,server會返回http->可能發生的中間人攻擊
Strict-Transport-Security: max-age=31536000; includeSubDomains
,這個響應行表示如果瀏覽器接收到使用 HTTP 加載資源的請求,則必須嘗試使用 HTTPS 請求替代。 如果 HTTPS 不可用,則必須直接終止連接;max-age是強制執行的時間,這里是一年,只要一年時間內瀏覽器再次訪問這個域名,就可以再次強制執行一年;includeSubDomains表示當前域名及其子域名均開啟HSTS保護;而且對于tls握手過程中有問題的證書也會禁止訪問,hsts不僅提升了訪問速度,而且增強了安全性。
TLS1.3
TLS1.3相對于之前的版本有了更加安全和高效的密鑰交換流程,以下是截取tls1.3相比之前協議的改變
接下來具體說一下tls的哪些改變,詳細的過程可以參考tls詳解:tls1.3相比以前版本的改變
①1RTT的tls連接時間左邊是tls1.3的一般流程,右邊tls1.3的會話復用流程tls1.3還廢除了rsa算法,因為它不支持前向安全,所以這里使用DHE算法作為講解。wires hark抓包的new session ticket
- 客戶端發送client hello,支持的加密套件,(該作用和之前一樣)、supproted_versions 拓展(包含自己支持的TLS協議版本號)、supproted_groups 拓展(表示自己支持的橢圓曲線類型)、key_share拓展(包含客戶端利用supprot_groups中各橢圓曲線算法生成的public key)
wireshark捕獲的clienthello包- server 發送Server Hello,攜帶如下幾個重要信息supproted_versions 拓展(包含自己從client的supproted_versions中選擇的TLS協議版本號)、key_share拓展(包含自己選中的橢圓曲線,以及自己計算出來的公鑰,接著發送Change Ciper Spec和Finished(與tls1.2相同)
wireshark捕獲的server hello包tls1.3相比1.2的DHE算法減少了一個RTT,原因在于tls再client hello階段就計算出了所有橢圓曲線算法的公鑰供server選擇。
②0RTT的會話復用
與上圖tls1.2的會話復用相同,tls1.3的會話復用使用了相同的sessionid和ticket機制,也是1RTT的時間。但是tls1.3的會話復用還可以實現0RTT的連接,具體做法是:
- client hello中除發送上面的信息外還需要額外發送:psk_key_exchange_modes(psk的交換模式)、pre_shared_key拓展(將之前握手后發送的ticket處理之后形成的預共享密鑰,簡稱psk,由于server可能會發送多次,所以可能會出現多個),early_data拓展(server是否支持0RTT的連接)以及應用數據。
- server hello會有多種情況,如果server驗證ticket成功并且愿意0RTT連接,那么sever hello會發送Encrypted Extensions,里面攜帶了early data拓展表示自己會讀取early data,說明0RTT成功;如果 server沒有配置讀取early data的選項,但是ticket驗證成功,那么server就會忽略client發送的Application data,這樣只表示會話復用成功,但不立刻接收數據,需要1RTT;如果ticket驗證失敗,則回到了上面tls1.3 1RTT建立連接的過程。
0RTT的會話復用
3.HTTP/2
HTTP/2是對之前HTTP協議的擴展,而非替代,HTTP 方法、狀態代碼、URI 和標頭字段等這些核心概念不變,只是傳輸過程做了優化。http1為了實現請求的多并發,只能創建多個連接和域名分片上下手,但是六個瀏覽器都有自己的最大連接數,這種策略不能從根本上解決問題;而且創建多個連接也會增加握手消耗。
①ALPN協議:http2設計的時候為了考慮兼容性的問題,需要客戶端對服務器發起一次詢問,但是這樣浪費一次RTT,為了減少浪費,客戶端會在client hello時加上http詢問請求,這樣做不僅減少了浪費,還達到了h2強制使用tls的目的,確保通信安全,這種機制叫做ALPN(Application Layer Protocol Negotiation)wireshark捕獲的alpn協議
②二進制分幀:h2為了應對http1中的隊頭阻塞問題什么是http隊頭阻塞和tcp隊頭阻塞以及如何解決,設計出了二進制分幀技術,http1中的數據都是通過文本文本傳輸的,但是二進制結構更有利于數據處理,因為計算機只認識二進制,所以傳輸時不需要文本與二進制之間的轉換;http1中的管道技術雖然支持反多次請求,但是響應必須按照FIFO的順序返回,不能實現真正的并發,所以存在隊頭阻塞問題。
針對這個問題,h2將每個響應分成若干個幀序列,而每個幀序列都有流號,標記屬于哪個響應,這樣即使每個幀序列亂序到客戶端,還是能夠根據流id重新組裝,解決了http方面的隊頭阻塞,真正實現多路復用。
例如下圖,客戶端請求first函數和second函數,響應便將每個函數分割成若干個幀序列,然后填寫流id和長度(方便客戶端組裝)。其中響應頭會生成headers frame,請求行會生成data frame(除此之外還有其他種類的幀負責標記幀的用途)aa+-----------------------------------------------+ | Length (24) | +---------------+---------------+---------------+ | Type (8) | Flags (8) | +-+-------------+---------------+-------------------------------+ |R| Stream Identifier (31) | +=+======================================================>=======+ | Frame Payload (0...) ... +---------------------------------------------------------------+
Length
代表整個 frame 的長度,用一個 24 位無符號整數表示,頭部的 9 字節不算在這個長度里。Type
定義 frame 的類型,用 8 bits 表示。幀類型決定了幀主體的格式和語義,如果 type 為 unknown 應該忽略或拋棄。Flags
是為幀類型相關而預留的布爾標識。標識對于不同的幀類型賦予了不同的語義。如果該標識對于某種幀類型沒有定義語義,則它必須被忽略且發送的時候應該賦值為 (0x0)R
是一個保留的比特位。這個比特的語義沒有定義,發送時它必須被設置為 (0x0), 接收時需要忽略- Stream Identifier 用作流控制,用 31 位無符號整數表示。客戶端建立的 sid 必須為奇數,服務端建立的 sid 必須為偶數,值 (0x0) 保留給與整個連接相關聯的幀 (連接控制消息),而不是單個流
-Frame Payload
是主體內容,由幀類型決定。幀類型主要有以下幾種:
HEADERS
: 報頭幀 (type=0x1),用來打開一個流或者攜帶一個首部塊片段DATA
: 數據幀 (type=0x0),裝填主體信息,可以用一個或多個 DATA 幀來返回一個請求的響應主體PRIORITY
: 優先級幀 (type=0x2),指定發送者建議的流優先級,可以在任何流狀態下發送 PRIORITY 幀,包括空閑 (idle) 和關閉 (closed) 的流PUSH_PROMISE
: 推送幀 (type=0x5),服務端推送,客戶端可以返回一個 RST_STREAM 幀來選擇拒絕推送的流RST_STREAM
: 流終止幀 (type=0x3),用來請求取消一個流,或者表示發生了一個錯誤,payload 帶有一個 32 位無符號整數的錯誤碼 (Error Codes),不能在處于空閑 (idle) 狀態的流上發送 RST_STREAM 幀SETTINGS
: 設置幀 (type=0x4),設置此連接
的參數,作用于整個連接,設置流量控制窗口的大小PING
: PING 幀 (type=0x6),判斷一個空閑的連接是否仍然可用,也可以測量最小往返時間 (RTT)GOAWAY
: GOWAY 幀 (type=0x7),用于發起關閉連接的請求,或者警示嚴重錯誤。GOAWAY 會停止接收新流,并且關閉連接前會處理完先前建立的流WINDOW_UPDATE
: 窗口更新幀 (type=0x8),用于執行流量控制功能,可以作用在單獨某個流上 (指定具體 Stream Identifier) 也可以作用整個連接 (Stream Identifier 為 0x0),只有 DATA 幀受流量控制影響。初始化流量窗口后,發送多少負載,流量窗口就減少多少,如果流量窗口不足就無法發送,WINDOW_UPDATE 幀可以增加流量窗口大小。note:數據優先級:
http2可以進行幀的多路復用,但是某些文件可能需要盡快處理?為此http2設計了數據幀優先級:通過幀之間的依賴關系和權重值來分配資源帶寬。例如上圖示例Ⅰ,可知幀AB處于同一級別,但是權重不同,所以幀A將分配到3/4的帶寬,而幀B只分配到1/4的帶寬。幀的依賴關系與權重
示例Ⅱ說明,D是C的父級,所以先分配D的帶寬,再分配C的帶寬。
示例Ⅲ說明,先D的帶寬,再分配C的帶寬,最后根據權重分配AB的帶寬。
Note:但是數據流依賴關系和權重只表示傳輸優先級,而不是一種必要行為,因此不能保證特定的處理或傳輸順序。因為我們不希望在優先級較高的資源受到阻止時,還阻止服務器處理優先級較低的資源。③頭部壓縮:h2的另一個優勢便是頭部壓縮,在大多數時候,我們請求或者響應的數據可能很少(例如ajax請求),但是同樣需要傳輸各種請求頭,尤其是cookie。這無疑給http帶來負擔,而頭部壓縮就是解決重復請求頭發送的問題。為了方便,所以直接使用鏈接中的圖片 HTTP/2 頭部壓縮技術介紹
正如圖中所示,左邊是請求頭,中間分為上下兩部分,上面是靜態表(將常見的請求頭編碼),下面是動態表(請求響應時動態添加的內容,如cookies)(兩個表使用連續的空間,所以畫在了一起)右邊是根據頭部壓縮得到的編碼。頭部壓縮每個字節都表示不同的含義,根據字節前面的標識就可以分辨是哪種請求頭。下面講解一下頭部壓縮的編解碼大致原理:頭部壓縮示意圖
- 請求頭在靜態索引表或者動態表中的,如method:get或者path:/或者動態表添加的內容等,可以直接使用索引表示。索引表中的內容以1開始,剩下的七位表示索引表中的條目,這樣只用1個字節就可以表示原來很多字節的內容。
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 1 | Index (7+) | +---+---------------------------+
請求頭根據索引表編碼
- 請求頭不在字典表中,但是需要添加到動態表中的,使用“01”開頭,后面如果是“000000”,說明請求頭參數名不在字典中,否則可以根據索引值在靜態或者動態表中找到條目。接著后跟的字節標識請求頭參數值,H為1表示使用哈夫曼編碼,否則表示使用字符串編碼。如下圖選中的字節“0110000”表示不在表中,但是請求頭參數名在字典中,為32表示cookie;剩下的字節“10011100”表示參數值使用哈夫曼編碼,長度是28,而開頭字母u的哈夫曼編碼表示為“101101”,正好與接下來的序列相符,這里我們看出頭部壓縮的好處。
字符串的哈夫曼編碼可能不是以整八位比特位結束的,這就需要在哈夫曼編碼的最后添加“EOS(填充1)”結束標記。(哈夫曼編碼是根據各字符經常使用頻率而創造的序列,可以減少編碼量,如字符a需要使用1個字節,但哈夫曼編碼“00011”即可表示它),一般我們也會看到十六進制表示的請求頭,這是由二進制編碼轉換而來的。哈夫曼編碼表//請求頭的參數名在字典中,但是參數值不在表中 0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 0 | 1 | Index (6+) | +---+---+-----------------------+ | H | Value Length (7+) | +---+---------------------------+ | Value String (Length octets) | +-------------------------------+ //請求頭的參數名和參數值都不在表中 0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 0 | 1 | 0 | +---+---+-----------------------+ | H | Name Length (7+) | +---+---------------------------+ | Name String (Length octets) | +---+---------------------------+ | H | Value Length (7+) | +---+---------------------------+ | Value String (Length octets) | +-------------------------------+
需要添加到動態表中的請求頭編碼
- 請求頭不在字典中,但是不添加到動態表中,使用“0001”開頭,大致過程與第二種情況相同。
//請求頭的參數名在字典中,但是參數值不在表中 0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 0 | 0 | 0 | 1 | Index (4+) | +---+---+-----------------------+ | H | Value Length (7+) | +---+---------------------------+ | Value String (Length octets) | +-------------------------------+ ///請求頭的參數名和參數值都不在表中 0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 0 | 0 | 0 | 1 | 0 | +---+---+-----------------------+ | H | Name Length (7+) | +---+---------------------------+ | Name String (Length octets) | +---+---------------------------+ | H | Value Length (7+) | +---+---------------------------+ | Value String (Length octets) | +-------------------------------+
請求頭不在字典中,并且不添加動態表中但是需要說明的是,如果請求頭參數名在字典中的索引很大,比如1234怎么辦,我們這種結構只能存儲最大值為15的索引?這就需要用到前綴編碼,以第三種情況眾多的四位前綴編碼為例,存儲x = 1234過程,這個過程與十進制轉換二進制的過程有些相似:
- 首先將最大值1111,填入后四位;那么x = x-15 = 1219;
- 接著需要將剩余的部分填入接下來的字節中,計算x = x%128=67(為什么使用128而不是256,因為需要一位標識這個數據是否結束,因為數據可能需要填入多個字節中,為0則表示數據結束。)需要將67填到下個字節的剩下七位中。接著計算x = x/128=9;
- x = 9<128,數據計算結束,將下個字節的首位置零(標識這個數據一表示完成),剩下7位填入9;得到的序列就是 0001 1111,1100 0011,0000 1001。解碼時可以將后兩個字節去除首部顛倒順序排列,再加上1111就可以得到1234。 計算代碼如下。
//編碼過程 if I < 2^N - 1, encode I on N bits else encode (2^N - 1) on N bits I = I - (2^N - 1) while I >= 128 encode (I % 128 + 128) on 8 bits I = I / 128 encode I on 8 bits //解碼過程 decode I from the next N bits if I < 2^N - 1, return I else M = 0 repeat B = next octet I = I + (B & 127) * 2^M M = M + 7 while B & 128 == 128 return I
-還有一種請求頭不在表中,但是絕對不添加索引的首部;這種和第三種情況相同,唯一不同的是,這種情況會作用于網絡中的每一跳(每一個路由設備)如果中間通過代理,代理必須原樣轉發不能另行編碼。而上一種首部只是作用當前跳,通過代理后可能會被重新編碼
④Server Push
在http1中,上一個響應發送完了,服務器才能發送下一個請求,但在http2里,可以將多個響應一起發送。例如在一個html文件中有個多js,css文件,為了快速解析頁面,可以直接向客戶端推送js文件,從而減少客戶端請求的時間。http2多路復用的好處:
- 連接少,握手成本就少。
- 壓縮比高,因為一個連接上積累的信息很多,壓縮比會更高。
- 更好地利用 TCP 的特性,為什么?因為 TCP 的擁塞控制流量控制都是基于單個連接的,如果使用很多個連接,特別是在網絡擁塞的情況下,會放大擁塞的系數,加劇網絡擁塞,如果使用一個連接,當得知該窗口已經擁塞,響應很慢便不會繼續發送了,但是多個連接的情況,可能大家都會嘗試發一下,而導致加劇網絡擁塞。
- 使用更少的域名,這也是為了更好地使用連接,并且減少了 DNS 解析時間。
http2存在的缺陷:http2雖然實現了多路復用,但是并沒有解決tcp方面的隊頭阻塞。而且,同一個連接內多個流傳輸,可能會導致RTT時間變長,相比http1容易出現超時重傳;再者,h2適合于大量冗余的請求,對于少量請求并沒有太大優勢。
4.HTTP3
前面提過http2雖然解決了http方面的隊頭阻塞,但是并沒有解決傳輸層的隊頭阻塞(tcp層),那么tcp層的隊頭阻塞是什么意思?
tcp是可靠的傳輸層協議,所以我們的http以及ftp都是用tcp作為傳輸層協議,但是也是因為tcp的可靠傳輸造就了tcp的隊頭阻塞問題;因為tcp為了保證數據準確且有序的發送到接收端,使用了滑動窗口協議。
滑動窗口協議:發送端會為發送的數據標上序列號(便于接收端組裝處理),接收端接收只有接收到按需到達的數據后才能交給應用層;如果有發送報文丟失,那么未按序列到達的報文只能等待之前序列號的報文到達才行。例如,發送依次發送1,2,3,4,5,6六段報文,到達順序為1,2,4,3,6,5報文丟失,那么12報文按序到達可以直接交給應用層處理,4報文需要等到3報文到達后才能交給應用層,6報文屬于未按序列到達,此時只能等到5報文超時重傳才能向上傳遞。所以,即使http2使用流分幀技術,但是tcp層還是需要一個個的按序傳輸才行。
除此之外,為了提高了網絡傳輸的性能,人們開始研究新型的http協議。如果還從tcp協議入手,不僅不能解決http2的隊頭阻塞,還有一些其他的問題:滑動窗口協議- 中間設備的僵化,一些路由器、網關或者防火墻默認tcp協議的端口和選項導致tcp無法出現使用新的格式。
- 操作系統的僵化,tcp協議棧是屬于內核的一部分,我們只能使用了socket接口。一旦修改tcp,可以需要修改操作系統的代碼。
- 握手時間長,tcp建立連接總是需要三次握手,四次揮手,這樣無形中增加了浪費。
udp可以很好的避開上面的所有問題,所以新的http3協議使用udp為傳輸層協議,但是udp是不可靠的傳輸層協議,所以就需要自己實現tcp的流量控制和擁塞控制。
4.1http3還是比http2的優點
- 更快的連接時延
http1一次tcp握手,兩次tls握手,至少需要三次握手才能發送數據;TLS1.3可以實現兩次握手就發送數據;到后來的TFO+TLS1.3的session reuse才能真正實現0RTT的握手連接,但是這需要通信雙發都支持TFO,TLS1.3而且是session復用的情況下。
而udp不需要握手就可以直接發送數據,再加上支持tls1.3,所以可以實現1RTT或者0RTT的連接。
- 沒有tcp的隊頭阻塞
沒有使用tcp,自然也不會有tcp的隊頭阻塞,但是如何保證數據有序到達呢?
http3使用http2中流的思想,在應用層和傳輸層加了個quic協議層,將從http3應用層收到的數據封裝成quic幀,每幀都有流id和字節長度。這樣接收到數據后就可以根據流id和字節長度重新拼接數據。同樣的,一個連接可以建立多個流,而每個流可都以不影響的發送各自的幀,因為udp不需要整個字節序列按序達到就可以交給quic層,而quic則可以根據流id組裝數據。
http2與http3的協議框架
- 加密的報文頭部
tcp使用tls雖然能加密報文數據,但是tcp首部沒有加密。所以在傳輸過程中很容易被中間網絡設備篡改,注入和竊聽。比如修改序列號、滑動窗口。quic 的 packet 可以說是武裝到了牙齒。除了個別報文外所有報文頭部都是經過認證的,報文 Body 都是經過加密的。
- 連接遷移
什么叫連接遷移?就是當網絡環境發生變化時,這條連接不中斷。一般我們使用手機上網時,可能會因為移動或者切換網絡而導致IP地址變化,這樣我們又需要與原來的服務器進行重新連接。而我們的請求可能也需要重發。又比如,我們使用一個公共ip上網時,可能會因為競爭需要重新進行端口映射。這是因為傳統網絡使用四元組(源ip,目的ip,源端口,目的端口)來辨別連接,http3不使用四元組,而是用conntion id來標識連接。即使ip或者端口發生變化,連接也不會中斷,上層邏輯不感知變化。
4.2擁塞控制 [QUIC-流量控制]
擁塞控制是為了防止過多的數據進入網絡導致網絡阻塞,網絡數據丟失率高(例如中間設備未能及時的接收數據包而丟棄)
http3的udp并沒有完成擁塞控制算法,這就需要我們自己實現與擁塞控制相同的功能。雖然工作量多了,但是現在設計的擁塞控制功能可以使用最新的算法,彌補或者避免過去tcp的缺陷。
①可插拔的算法設計:
- 我們可以在用戶層實現自己的擁塞控制算法,可以很方便的升級和修改配置,不需要改變內核部分。
- 不同應用程序的不同連接可以配置不同的擁塞控制算法,更精準有效。
②避免RTT計算的歧義
RTT是一次數據在客戶端接收端往返的時間,它會根據上次數據的往返時間(從發送數據開始到接收到數據的Ack為止)不斷更新。但是如果發生重傳,原來數據的Ack與重傳數據Ack的序列號相同,這就會在計算RTT的時候就會發生歧義。無論以哪個計算都不能確定RTT一定是準確的。為了解決這個問題,http3在每個quic幀上都添加了自增的Packet Number,這樣就可以區分到底是哪個數據的ack了。但是單純依靠嚴格遞增的 Packet Number 肯定是無法保證數據的順序性和可靠性。QUIC 又引入了一個 Stream Offset 的概念。quic幀可以依靠 Stream 的 Offset 來保證應用數據的順序,即使發生重傳,幀也可以根據offset進行排序。
包發生重傳時RTT可能出現的歧義
④更多的sack塊
如果幀沒有按序到達,為了減少重傳的幀序列,會在tcp幀的首部增加已經到達的幀的范圍,例如,1,2,3,4,5,如果1245到達,3缺失,在發送ack幀時就可以選擇發送2的確認ack以及4至5的sack。這就告訴發送端幀2收到,可以從幀3發送,但是幀45已經收到,不需要再發送了。原來的tcp首部由于長度的限制最多填寫3個幀范圍。而現在的quic首部可以填寫更多sack幀范圍。
⑤不允許Reneging
什么叫 Reneging 呢?就是接收方已經接收并且上報給 SACK 選項的內容 ,但是接收方因為服務器資源有限,比如 Buffer 溢出,內存不夠等情況而丟棄數據,這種方式在http3中不被允許,可以減少干擾。
4.3流量控制 QUIC-流量控制
流量控制用于解決發送端和接收端速度不匹配的情況,如果發送端發送太快會導致接收端無法接收而丟棄數據包,數據接收率低;如果接收端太快而發送端比較慢,也會使得網絡傳輸率低。流量控制就是為了平衡接收端和發送端的速度問題或者特定數據流的傳輸請求等(例如視頻倍速播放等)。
流量控制的大致過程:圖中flow control receive offset表示最大的接收窗口,也可以看成初始時服務器端允許客戶端發送的最大窗口,綠色部分表示接收端讀取的數據,黃色部分表示到達但還沒有接受的數據(空白間隙表示沒有按序到達的塊),flow control receive window表示接收窗口,當接收端讀取的數據(綠色部分)大于最大接收窗口(flow control receive offset)的一半時,會增加最大接收窗口(讀取多少,擴大多少),同時發送WINDOW_UPDATE幀通知客戶端發送窗口。如果接收窗口(flow control receive window)小于0,則發送BLOCKED幀通知發送端停止發送。
http3流控中接收端發送WINDOW_UPDATE幀通知對端接收窗口的大小增長,比如說能接收更多的數據,發送端也可以發送BLOCKED幀表明數據發送受制于接收端的窗口,無法發送數據。
接收窗口quic的流量控制與tcp的不同在于:TCP 為了保證可靠性,窗口左邊沿向右滑動時的長度取決于已經確認的字節數。如果中間出現丟包,就算接收到了更大序號的 Segment,窗口也無法超過這個序列號;而QUIC 不同,它的滑動只取決于接收到的最大偏移字節數(圖中黃色最右邊的位置),即使中間有包未收到,也跟接收窗口沒有關系。接收窗口擴大
此外,http3和http2一樣,同時提供流級和鏈接級別的流量控制。還有流和連接兩種流量控制,連接的流量控制是針對連接內所有流的。
參考:哈夫曼編碼的理解
https過程解析
淺談SSL/TLS工作原理 - 知乎 (zhihu.com)
ALPN協議
HTTP/2協議
使用 Wireshark 抓取 HTTP1/2 流量
TLS 1.3 協議詳解
TCP選項之SACK選項概述
TCP 的那些事 | D-SACK
TCP中的RST標志(Reset)詳解
TCP的快速重傳機制與累計確認機制
HTTP協議學習筆記--http1.0、http1.1、http2.0、https、http3
面試官:一個TCP連接可以發多少個HTTP請求? - 知乎 (zhihu.com)
圖解 ECDHE 密鑰交換算法 - 愛碼網 (likecs.com)
幾幅圖,拿下 HTTPS (qq.com)
關于隊頭阻塞(Head-of-Line blocking),看這一篇就足夠了
HPACK 介紹 (gohalo.me)
HTTP2 詳解 - 簡書 (jianshu.com)
QUIC詳解-網絡編程/專項技術區
HTTP 協議概述 - 博客園 (cnblogs.com)