關(guān)于 iOS HTTP2.0 的一次學(xué)習(xí)實(shí)踐

前面的文章也提到了目前的移動(dòng)端網(wǎng)絡(luò)常見(jiàn)性能問(wèn)題,以及對(duì)應(yīng)的優(yōu)化策略,如果把HTTP1.1 替換為 HTTP2.0,可以說(shuō)是網(wǎng)絡(luò)性能優(yōu)化的一步大棋。這幾天對(duì) iOS HTTP2.0 進(jìn)行了簡(jiǎn)單的調(diào)研、測(cè)試,在此做個(gè)簡(jiǎn)單的總結(jié)

本文的大概思路是介紹 HTTP1.1 的弊端、HTTP2.0 的優(yōu)勢(shì)、HTTP2.0 的協(xié)商機(jī)制、iOS 客戶端如何接入 HTTP2.0,以及如何對(duì)其進(jìn)行調(diào)試。主要還是加深記憶、方便后期查閱,文末的資料相比本文或許是更有價(jià)值的。

HTTP 1.1

  • 雖然 HTTP1.1 默認(rèn)是開(kāi)啟 Keep-Alive 長(zhǎng)連接的,一定程度上彌補(bǔ)了HTTP1.0每次請(qǐng)求都要?jiǎng)?chuàng)建連接的缺點(diǎn),但是依然存在 head of line blocking,如果出現(xiàn)一個(gè)較差的網(wǎng)絡(luò)請(qǐng)求,會(huì)影響后續(xù)的網(wǎng)絡(luò)請(qǐng)求。為什么呢?如果你發(fā)出1、2、3 三個(gè)網(wǎng)絡(luò)請(qǐng)求,那么 Response 的順序 2、3 要在第一個(gè)網(wǎng)絡(luò)請(qǐng)求之后,以此類(lèi)推

  • 針對(duì)同一域名,在請(qǐng)求較多的情況下,HTTP1.1 會(huì)開(kāi)辟多個(gè)連接,據(jù)說(shuō)瀏覽器一般是6-8 個(gè),較多連接也會(huì)導(dǎo)致延遲增大,資源消耗等問(wèn)題

  • HTTP1.1 不安全,可能存在被篡改、被竊聽(tīng)、被偽裝等問(wèn)題。當(dāng)然,前陣子 Apple 推廣 HTTPS 的時(shí)候,相信很多人已經(jīng)接入 HTTPS

  • HTTP 的頭部沒(méi)有壓縮,header 的大小也是傳輸?shù)呢?fù)擔(dān),帶來(lái)更多的流量消耗和傳輸延遲。并且很多 header 是相同的,重復(fù)傳輸是沒(méi)有必要的。

  • 服務(wù)端無(wú)法主動(dòng)推送資源到客戶端

  • HTTP1.1的格式是文本格式,基于文本做一些擴(kuò)展、優(yōu)化相對(duì)比較困難,但是文本格式易于閱讀和調(diào)試,但HTTPS之后,也變成二進(jìn)制格式了,這個(gè)優(yōu)勢(shì)也不復(fù)存在

HTTP2.0

在 HTTP2.0中,上面的問(wèn)題幾乎都不存在了。HTTP2.0 的設(shè)計(jì)來(lái)源于 Google 的 SPDY 協(xié)議,如果對(duì) SPDY 協(xié)議不了解的話,也可以先對(duì) SPDY 進(jìn)行了解,不過(guò)這不影響繼續(xù)閱讀本文

  • HTTP 2.0 使用新的二進(jìn)制格式:基本的協(xié)議單位是幀,每個(gè)幀都有不同的類(lèi)型和用途,規(guī)范中定義了10種不同的幀。例如,報(bào)頭(HEADERS)和數(shù)據(jù)(DATA)幀組成了基本的HTTP 請(qǐng)求和響應(yīng);其他幀例如 設(shè)置(SETTINGS),窗口更新(WINDOW_UPDATE), 和推送承諾(PUSH_PROMISE)是用來(lái)實(shí)現(xiàn)HTTP/2的其他功能。那些請(qǐng)求和響應(yīng)的幀數(shù)據(jù)通過(guò)流來(lái)進(jìn)行數(shù)據(jù)交換。新的二進(jìn)制格式是流量控制、優(yōu)先級(jí)、server push等功能的基礎(chǔ)。

流(Stream):一個(gè)Stream是包含一條或多條信息、ID和優(yōu)先級(jí)的雙向通道

消息(Message):消息由幀組成

幀(Frame):幀有不同的類(lèi)型,并且是混合的。他們通過(guò)stream id被重新組裝進(jìn)消息中

  • 多路復(fù)用:也就是連接共享,剛才說(shuō)到 HTTP1.1的 head of line blocking,那么在多路復(fù)用的情況下,blocking 已經(jīng)不存在了。每個(gè)連接中 可以包含多個(gè)流,而每個(gè)流中交錯(cuò)包含著來(lái)自?xún)啥说膸?。也就是說(shuō)同一個(gè)連接中是來(lái)自不同流的數(shù)據(jù)包混合在一起,如下圖所示,每一塊代表幀,而相同顏色塊來(lái)自同一個(gè)流,每個(gè)流都有自己的 ID,在接收端會(huì)根據(jù) ID 進(jìn)行重裝組合,就是通過(guò)這樣一種方式來(lái)實(shí)現(xiàn)多路復(fù)用。
  • 單一連接:剛才也說(shuō)到 1.1 在請(qǐng)求多的時(shí)候,會(huì)開(kāi)啟6-8個(gè)連接,而 HTTP2 只會(huì)開(kāi)啟一個(gè)連接,這樣就減少握手帶來(lái)的延遲。

  • 頭部壓縮:HTTP2.0 通過(guò) HPACK 格式來(lái)壓縮頭部,使用了哈夫曼編碼壓縮、索引表來(lái)對(duì)頭部大小做優(yōu)化。索引表是把字符串和數(shù)字之間做一個(gè)匹配,比如method: GET對(duì)應(yīng)索引表中的2,那么如果之前發(fā)送過(guò)這個(gè)值是,就會(huì)緩存起來(lái),之后使用時(shí)發(fā)現(xiàn)之前發(fā)送過(guò)該Header字段,并且值相同,就會(huì)沿用之前的索引來(lái)指代那個(gè)Header值。具體實(shí)驗(yàn)數(shù)據(jù)可以參考這里:HTTP/2 頭部壓縮技術(shù)介紹

  • Server Push:就是服務(wù)端可以主動(dòng)推送一些東西給客戶端,也被稱(chēng)為緩存推送。推送的資源可以備客戶端日后之需,需要的時(shí)候直接拿出來(lái)用,提升了速率。具體的實(shí)驗(yàn)可以參考這里:iOS HTTP/2 Server Push 探索

除了上面講到的特性,HTTP2.0 還有流量控制、流優(yōu)先級(jí)和依賴(lài)性等功能。更多細(xì)節(jié)可以參考:Hypertext Transfer Protocol Version 2 (HTTP/2)

iOS 客戶端接入HTTP 2.0

iOS 如何接入 HTTP 2.0呢?其實(shí)很簡(jiǎn)單:

  • 保證服務(wù)端支持 HTTP2.0,并且留意下 NPN 或 ALPN
  • 客戶端系統(tǒng)版本 iOS 9 +
  • 使用 NSURLSession 代替 NSURLConnection
  • 客戶端是使用 h2c 還是 h2,它們可以說(shuō)是 HTTP2.0的兩個(gè)版本,h2 是使用 TLS 的HTTP2.0協(xié)議,h2c是運(yùn)行在明文 TCP 協(xié)議上的 HTTP2.0協(xié)議。瀏覽器目前只支持h2,也就是說(shuō)必須基于HTTPS部署,但是客戶端可以不部署HTTPS,因?yàn)槲宜驹缫巡渴餒TTPS,所以我這里的實(shí)踐都是基于h2的

HTTP 2.0的協(xié)商機(jī)制

上面說(shuō)了一堆名次,什么NPN、ALPN呀,還有h2、h2c之類(lèi)的,有點(diǎn)懵逼。NPN(Next Protocol Negotiation)是一個(gè) TLS 擴(kuò)展,由 Google 在開(kāi)發(fā) SPDY 協(xié)議時(shí)提出。隨著 SPDY 被 HTTP/2 取代,NPN 也被修訂為 ALPN(Application Layer Protocol Negotiation,應(yīng)用層協(xié)議協(xié)商)。二者目標(biāo)一致,但實(shí)現(xiàn)細(xì)節(jié)不一樣,相互不兼容。以下是它們主要差別:

  • NPN 是服務(wù)端發(fā)送所支持的 HTTP 協(xié)議列表,由客戶端選擇;而 ALPN 是客戶端發(fā)送所支持的 HTTP 協(xié)議列表,由服務(wù)端選擇;
  • NPN 的協(xié)商結(jié)果是在 Change Cipher Spec 之后加密發(fā)送給服務(wù)端;而 ALPN 的協(xié)商結(jié)果是通過(guò) Server Hello 明文發(fā)給客戶端

同時(shí),目前很多地方開(kāi)始停止對(duì)NPN的支持,僅支持 ALPN,所以公司使用的話,最佳是直接使用 ALPN。

下面就直接來(lái)看看 ALPN 的協(xié)商過(guò)程是怎樣的,ALPN 作為 TLS 的一個(gè)擴(kuò)展,其過(guò)程可以通過(guò) WireShark 查看 TLS握手過(guò)程來(lái)查看

下面通過(guò) WireShark 來(lái)進(jìn)行調(diào)試,接入真機(jī),然后終端輸入
rvictl -s 設(shè)備 UDID來(lái)創(chuàng)建一個(gè)映射到 iPhone 的虛擬網(wǎng)卡,UUID 可以在 iTunes 中獲取到,運(yùn)行命令后會(huì)看到成功創(chuàng)建 rvi0 虛擬網(wǎng)卡的,雙擊 rvi0 開(kāi)始調(diào)試。

進(jìn)入之后,在手機(jī)上訪問(wèn)頁(yè)面會(huì)有源源不斷的請(qǐng)求顯示在 WireShark 的界面上,數(shù)據(jù)太多而不利于我們針對(duì)性調(diào)試,你可以過(guò)濾下域名,只關(guān)注你想測(cè)試的 ip 地址,比如: ip.addr==111.89.211.191 ,當(dāng)然你的 ip 要支持 HTTP2.0才會(huì)有預(yù)想的效果哦

下面,就開(kāi)始通過(guò)查看 TLS 握手的過(guò)程分析HTTP2.0 的協(xié)商過(guò)程,剛才也說(shuō)道 ALPN 協(xié)商結(jié)果是在 Client hello 和 Server hello 中顯示的,那就先來(lái)看一下Client hello

可以看到客戶端在 Client hello 中列出了自己支持的各種應(yīng)用層協(xié)議,比如 spdy3、h2。那么接著看 Server hello 是如何回復(fù)的

服務(wù)端會(huì)根據(jù) client hello 中的協(xié)議列表,發(fā)過(guò)去自己支持的網(wǎng)絡(luò)協(xié)議,假如服務(wù)端支持 h2,則直接返回h2,協(xié)商成功,如果不支持 h2,則返回一個(gè)其他支持的協(xié)議,比如HTTP1.1、spdy3

這個(gè)是h2的協(xié)商過(guò)程,對(duì)于剛才提到的 h2c 的協(xié)商過(guò)程,與此不同,h2c 利用的是HTTP Upgrade 機(jī)制,客戶端會(huì)發(fā)送一個(gè) http 1.1的請(qǐng)求到服務(wù)端,這個(gè)請(qǐng)求中包含了 http2的升級(jí)字段,例如:

  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>

服務(wù)端收到這個(gè)請(qǐng)求后,如果支持 Upgrade 中 列舉的協(xié)議,這里是 h2c,就會(huì)返回支持的響應(yīng):

  HTTP/1.1 101 Switching Protocols
  Connection: Upgrade
  Upgrade: h2c

  [ HTTP/2 connection ...

當(dāng)然,不支持的話,服務(wù)器會(huì)返回一個(gè)不包含 Upgrade 的報(bào)頭字段的響應(yīng)。

我的客戶端支持了嗎?

一切準(zhǔn)備就緒之后,也是時(shí)候?qū)Y(jié)果進(jìn)行驗(yàn)證了,除了剛才提到的 WireShark 之外,你還可以使用下面的幾個(gè)工具來(lái)對(duì) HTTP 2.0 進(jìn)行測(cè)試

  • Chrome 上的一個(gè)插件,HTTP/2 and SPDY indicator 會(huì)在你訪問(wèn) http2.0 的網(wǎng)頁(yè)的時(shí)候,以小閃電的形式進(jìn)行指示

點(diǎn)擊小閃電,會(huì)進(jìn)入一個(gè)頁(yè)面,列舉了當(dāng)前瀏覽器訪問(wèn)的全部 http2.0的請(qǐng)求,所以,你可以把你想要測(cè)試的客戶端接口在瀏覽器訪問(wèn),然后在這個(gè)頁(yè)面驗(yàn)證下是否支持 http2.0

  • charles:這個(gè)大家應(yīng)該都用過(guò),4.0 以上的新版本對(duì) HTTP2.0做了支持,為了方便,你也可以在 charles 上進(jìn)行調(diào)試,但是我發(fā)現(xiàn)好像存在 http2.0的一些 bug,目前還沒(méi)搞清楚什么原因

  • 使用 nghttp2 來(lái)調(diào)試,這是一個(gè) C 語(yǔ)言實(shí)現(xiàn)的 HTTP2.0的庫(kù),具體使用方法可以參考:使用 nghttp2 調(diào)試 HTTP/2 流量

  • 再者簡(jiǎn)單粗暴,直接在 iOS 代碼中打印,_CFURLResponse 中包含了 httpversion,獲取方法就是基于 CFNetwork 相關(guān)的 API 來(lái)做,這里直接丟出關(guān)鍵代碼,完整代碼可以參考 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      
    

大禮包

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容