前面的文章也提到了目前的移動端網路常見效能問題,以及對應的優化策略,如果把HTTP1.1 替換為 HTTP2.0,可以說是網路效能優化的一步大棋。這幾天對 iOS HTTP2.0 進行了簡單的調研、測試,在此做個簡單的總結
本文的大概思路是介紹 HTTP1.1 的弊端、HTTP2.0 的優勢、HTTP2.0 的協商機制、iOS 客戶端如何接入 HTTP2.0,以及如何對其進行除錯。主要還是加深記憶、方便後期查閱,文末的資料相比本文或許是更有價值的。
HTTP 1.1
雖然 HTTP1.1 預設是開啟 Keep-Alive 長連線的,一定程度上彌補了HTTP1.0每次請求都要建立連線的缺點,但是依然存在 head of line blocking,如果出現一個較差的網路請求,會影響後續的網路請求。為什麼呢?如果你發出1、2、3 三個網路請求,那麼 Response 的順序 2、3 要在第一個網路請求之後,以此類推
針對同一域名,在請求較多的情況下,HTTP1.1 會開闢多個連線,據說瀏覽器一般是6-8 個,較多連線也會導致延遲增大,資源消耗等問題
HTTP1.1 不安全,可能存在被篡改、被竊聽、被偽裝等問題。當然,前陣子 Apple 推廣 HTTPS 的時候,相信很多人已經接入 HTTPS
HTTP 的頭部沒有壓縮,header 的大小也是傳輸的負擔,帶來更多的流量消耗和傳輸延遲。並且很多 header 是相同的,重複傳輸是沒有必要的。
服務端無法主動推送資源到客戶端
HTTP1.1的格式是文字格式,基於文字做一些擴充套件、優化相對比較困難,但是文字格式易於閱讀和除錯,但HTTPS之後,也變成二進位制格式了,這個優勢也不復存在
HTTP2.0
在 HTTP2.0中,上面的問題幾乎都不存在了。HTTP2.0 的設計來源於 Google 的 SPDY 協議,如果對 SPDY 協議不瞭解的話,也可以先對 SPDY 進行了解,不過這不影響繼續閱讀本文
- HTTP 2.0 使用新的二進位制格式:基本的協議單位是幀,每個幀都有不同的型別和用途,規範中定義了10種不同的幀。例如,報頭
(HEADERS)
和資料(DATA)
幀組成了基本的HTTP 請求和響應;其他幀例如 設定(SETTINGS)
,視窗更新(WINDOW_UPDATE)
, 和推送承諾(PUSH_PROMISE)
是用來實現HTTP/2的其他功能。那些請求和響應的幀資料通過流來進行資料交換。新的二進位制格式是流量控制、優先順序、server push等功能的基礎。
流(Stream):一個Stream是包含一條或多條資訊、ID和優先順序的雙向通道
訊息(Message):訊息由幀組成
幀(Frame):幀有不同的型別,並且是混合的。他們通過stream id被重新組裝進訊息中
- 多路複用:也就是連線共享,剛才說到 HTTP1.1的 head of line blocking,那麼在多路複用的情況下,blocking 已經不存在了。每個連線中 可以包含多個流,而每個流中交錯包含著來自兩端的幀。也就是說同一個連線中是來自不同流的資料包混合在一起,如下圖所示,每一塊代表幀,而相同顏色塊來自同一個流,每個流都有自己的 ID,在接收端會根據 ID 進行重灌組合,就是通過這樣一種方式來實現多路複用。
單一連線:剛才也說到 1.1 在請求多的時候,會開啟6-8個連線,而 HTTP2 只會開啟一個連線,這樣就減少握手帶來的延遲。
頭部壓縮:HTTP2.0 通過 HPACK 格式來壓縮頭部,使用了哈夫曼編碼壓縮、索引表來對頭部大小做優化。索引表是把字串和數字之間做一個匹配,比如
method: GET
對應索引表中的2,那麼如果之前傳送過這個值是,就會快取起來,之後使用時發現之前傳送過該Header欄位,並且值相同,就會沿用之前的索引來指代那個Header值。具體實驗資料可以參考這裡:HTTP/2 頭部壓縮技術介紹
- Server Push:就是服務端可以主動推送一些東西給客戶端,也被稱為快取推送。推送的資源可以備客戶端日後之需,需要的時候直接拿出來用,提升了速率。具體的實驗可以參考這裡:iOS HTTP/2 Server Push 探索
除了上面講到的特性,HTTP2.0 還有流量控制、流優先順序和依賴性等功能。更多細節可以參考:Hypertext Transfer Protocol Version 2 (HTTP/2)
iOS 客戶端接入HTTP 2.0
iOS 如何接入 HTTP 2.0呢?其實很簡單:
- 保證服務端支援 HTTP2.0,並且留意下 NPN 或 ALPN
- 客戶端系統版本 iOS 9 +
- 使用 NSURLSession 代替 NSURLConnection
- 客戶端是使用 h2c 還是 h2,它們可以說是 HTTP2.0的兩個版本,h2 是使用 TLS 的HTTP2.0協議,h2c是執行在明文 TCP 協議上的 HTTP2.0協議。瀏覽器目前只支援h2,也就是說必須基於HTTPS部署,但是客戶端可以不部署HTTPS,因為我司早已部署HTTPS,所以我這裡的實踐都是基於h2的
HTTP 2.0的協商機制
上面說了一堆名次,什麼NPN、ALPN呀,還有h2、h2c之類的,有點懵逼。NPN(Next Protocol Negotiation)是一個 TLS 擴充套件,由 Google 在開發 SPDY 協議時提出。隨著 SPDY 被 HTTP/2 取代,NPN 也被修訂為 ALPN(Application Layer Protocol Negotiation,應用層協議協商)。二者目標一致,但實現細節不一樣,相互不相容。以下是它們主要差別:
- NPN 是服務端傳送所支援的 HTTP 協議列表,由客戶端選擇;而 ALPN 是客戶端傳送所支援的 HTTP 協議列表,由服務端選擇;
- NPN 的協商結果是在 Change Cipher Spec 之後加密傳送給服務端;而 ALPN 的協商結果是通過 Server Hello 明文發給客戶端
同時,目前很多地方開始停止對NPN的支援,僅支援 ALPN,所以公司使用的話,最佳是直接使用 ALPN。
下面就直接來看看 ALPN 的協商過程是怎樣的,ALPN 作為 TLS 的一個擴充套件,其過程可以通過 WireShark 檢視 TLS握手過程來檢視
下面通過 WireShark 來進行除錯,接入真機,然後終端輸入rvictl -s 裝置 UDID
來建立一個對映到 iPhone 的虛擬網路卡,UUID 可以在 iTunes 中獲取到,執行命令後會看到成功建立 rvi0 虛擬網路卡的,雙擊 rvi0 開始除錯。
進入之後,在手機上訪問頁面會有源源不斷的請求顯示在 WireShark 的介面上,資料太多而不利於我們針對性除錯,你可以過濾下域名,只關注你想測試的 ip 地址,比如: ip.addr==111.89.211.191 ,當然你的 ip 要支援 HTTP2.0才會有預想的效果哦
下面,就開始通過檢視 TLS 握手的過程分析HTTP2.0 的協商過程,剛才也說道 ALPN 協商結果是在 Client hello 和 Server hello 中顯示的,那就先來看一下Client hello
可以看到客戶端在 Client hello 中列出了自己支援的各種應用層協議,比如 spdy3、h2。那麼接著看 Server hello 是如何回覆的
服務端會根據 client hello 中的協議列表,發過去自己支援的網路協議,假如服務端支援 h2,則直接返回h2,協商成功,如果不支援 h2,則返回一個其他支援的協議,比如HTTP1.1、spdy3
這個是h2的協商過程,對於剛才提到的 h2c 的協商過程,與此不同,h2c 利用的是HTTP Upgrade 機制,客戶端會傳送一個 http 1.1的請求到服務端,這個請求中包含了 http2的升級欄位,例如:
GET /default.htm HTTP/1.1
Host: server.example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>複製程式碼
服務端收到這個請求後,如果支援 Upgrade 中 列舉的協議,這裡是 h2c,就會返回支援的響應:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c
[ HTTP/2 connection ...複製程式碼
當然,不支援的話,伺服器會返回一個不包含 Upgrade 的報頭欄位的響應。
我的客戶端支援了嗎?
一切準備就緒之後,也是時候對結果進行驗證了,除了剛才提到的 WireShark 之外,你還可以使用下面的幾個工具來對 HTTP 2.0 進行測試
- Chrome 上的一個外掛,HTTP/2 and SPDY indicator 會在你訪問 http2.0 的網頁的時候,以小閃電的形式進行指示
點選小閃電,會進入一個頁面,列舉了當前瀏覽器訪問的全部 http2.0的請求,所以,你可以把你想要測試的客戶端介面在瀏覽器訪問,然後在這個頁面驗證下是否支援 http2.0
charles:這個大家應該都用過,4.0 以上的新版本對 HTTP2.0做了支援,為了方便,你也可以在 charles 上進行除錯,但是我發現好像存在 http2.0的一些 bug,目前還沒搞清楚什麼原因
使用 nghttp2 來除錯,這是一個 C 語言實現的 HTTP2.0的庫,具體使用方法可以參考:使用 nghttp2 除錯 HTTP/2 流量
再者簡單粗暴,直接在 iOS 程式碼中列印,_CFURLResponse 中包含了 httpversion,獲取方法就是基於 CFNetwork 相關的 API 來做,這裡直接丟出關鍵程式碼,完整程式碼可以參考 getHTTPVersion
#import "NSURLResponse+Help.h" #import <dlfcn.h> @implementation NSURLResponse (Help) typedef CFHTTPMessageRef (*MYURLResponseGetHTTPResponse)(CFURLRef response); - (NSString *)getHTTPVersion { NSURLResponse *response = self; NSString *version; NSString *funName = @"CFURLResponseGetHTTPResponse"; MYURLResponseGetHTTPResponse originURLResponseGetHTTPResponse = dlsym(RTLD_DEFAULT, [funName UTF8String]); SEL theSelector = NSSelectorFromString(@"_CFURLResponse"); if ([response respondsToSelector:theSelector] && NULL != originURLResponseGetHTTPResponse) { CFTypeRef cfResponse = CFBridgingRetain([response performSelector:theSelector]); if (NULL != cfResponse) { CFHTTPMessageRef message = originURLResponseGetHTTPResponse(cfResponse); CFStringRef cfVersion = CFHTTPMessageCopyVersion(message); if (NULL != cfVersion) { version = (__bridge NSString *)cfVersion; CFRelease(cfVersion); } CFRelease(cfResponse); } } if (nil == version || 0 == version.length) { version = @"獲取失敗"; } return version; } @end 複製程式碼
大禮包
- Jerry Qu的HTTP2.0合輯
- http2-協議協商過程
- h2-13 中文版
- Hypertext Transfer Protocol Version 2 (HTTP/2)
- HPACK: Header Compression for HTTP/2
- Wireshark抓包iOS入門教程
- iOS HTTP/2 Server Push 探索
- HTTP/2 on iOS
- HTTPS 與 HTTP2 協議分析&version=12020110&nettype=WIFI&fontScale=100&pass_ticket=v4f3j82l8ughtmSZjfn5%2FFRoI%2BM4ntCq8S9SgIaAiDpg6FDq6D9dXVa3Hs9kv2R4)
- http2講解
- How to get HTTP protocol version from a given NSHTTPURLResponse?