前面的文章也提到了目前的移動端網絡常見性能問題,以及對應的優化策略,若是把HTTP1.1 替換爲 HTTP2.0,能夠說是網絡性能優化的一步大棋。這幾天對 iOS HTTP2.0 進行了簡單的調研、測試,在此作個簡單的總結html
本文的大概思路是介紹 HTTP1.1 的弊端、HTTP2.0 的優點、HTTP2.0 的協商機制、iOS 客戶端如何接入 HTTP2.0,以及如何對其進行調試。主要仍是加深記憶、方便後期查閱,文末的資料相比本文或許是更有價值的。ios
雖然 HTTP1.1 默認是開啓 Keep-Alive 長鏈接的,必定程度上彌補了HTTP1.0每次請求都要建立鏈接的缺點,可是依然存在 head of line blocking,若是出現一個較差的網絡請求,會影響後續的網絡請求。爲何呢?若是你發出一、二、3 三個網絡請求,那麼 Response 的順序 二、3 要在第一個網絡請求以後,以此類推git
針對同一域名,在請求較多的狀況下,HTTP1.1 會開闢多個鏈接,聽說瀏覽器通常是6-8 個,較多鏈接也會致使延遲增大,資源消耗等問題github
HTTP1.1 不安全,可能存在被篡改、被竊聽、被假裝等問題。固然,前陣子 Apple 推廣 HTTPS 的時候,相信不少人已經接入 HTTPSweb
HTTP 的頭部沒有壓縮,header 的大小也是傳輸的負擔,帶來更多的流量消耗和傳輸延遲。而且不少 header 是相同的,重複傳輸是沒有必要的。chrome
服務端沒法主動推送資源到客戶端segmentfault
HTTP1.1的格式是文本格式,基於文本作一些擴展、優化相對比較困難,可是文本格式易於閱讀和調試,但HTTPS以後,也變成二進制格式了,這個優點也不復存在瀏覽器
在 HTTP2.0中,上面的問題幾乎都不存在了。HTTP2.0 的設計來源於 Google 的 SPDY 協議,若是對 SPDY 協議不瞭解的話,也能夠先對 SPDY 進行了解,不過這不影響繼續閱讀本文緩存
(HEADERS)
和數據(DATA)
幀組成了基本的HTTP 請求和響應;其餘幀例如 設置(SETTINGS)
,窗口更新(WINDOW_UPDATE)
, 和推送承諾(PUSH_PROMISE)
是用來實現HTTP/2的其餘功能。那些請求和響應的幀數據經過流來進行數據交換。新的二進制格式是流量控制、優先級、server push等功能的基礎。流(Stream):一個Stream是包含一條或多條信息、ID和優先級的雙向通道安全
消息(Message):消息由幀組成
幀(Frame):幀有不一樣的類型,而且是混合的。他們經過stream id被從新組裝進消息中
單一鏈接:剛纔也說到 1.1 在請求多的時候,會開啓6-8個鏈接,而 HTTP2 只會開啓一個鏈接,這樣就減小握手帶來的延遲。
頭部壓縮:HTTP2.0 經過 HPACK 格式來壓縮頭部,使用了哈夫曼編碼壓縮、索引表來對頭部大小作優化。索引表是把字符串和數字之間作一個匹配,好比method: GET
對應索引表中的2,那麼若是以前發送過這個值是,就會緩存起來,以後使用時發現以前發送過該Header字段,而且值相同,就會沿用以前的索引來指代那個Header值。具體實驗數據能夠參考這裏:HTTP/2 頭部壓縮技術介紹
除了上面講到的特性,HTTP2.0 還有流量控制、流優先級和依賴性等功能。更多細節能夠參考:Hypertext Transfer Protocol Version 2 (HTTP/2)
iOS 如何接入 HTTP 2.0呢?其實很簡單:
上面說了一堆名次,什麼NPN、ALPN呀,還有h二、h2c之類的,有點懵逼。NPN(Next Protocol Negotiation)是一個 TLS 擴展,由 Google 在開發 SPDY 協議時提出。隨着 SPDY 被 HTTP/2 取代,NPN 也被修訂爲 ALPN(Application Layer Protocol Negotiation,應用層協議協商)。兩者目標一致,但實現細節不同,相互不兼容。如下是它們主要差異:
同時,目前不少地方開始中止對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 中列出了本身支持的各類應用層協議,好比 spdy三、h2。那麼接着看 Server hello 是如何回覆的
服務端會根據 client hello 中的協議列表,發過去本身支持的網絡協議,假如服務端支持 h2,則直接返回h2,協商成功,若是不支持 h2,則返回一個其餘支持的協議,好比HTTP1.一、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 進行測試
點擊小閃電,會進入一個頁面,列舉了當前瀏覽器訪問的所有 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 複製代碼