QUIC 協議初探 - iOS 實踐

weixin_34320159發表於2018-02-26

本文來自於騰訊 Bugly 公眾號(weixinBugly), 作者:emilymmwang,未經作者同意,請勿轉載,原文地址:https://mp.weixin.qq.com/s/NbewZ1NU49qSjIcdFrpotw

| 導語 本文主要介紹了QUIC協議,以及初步研究的過程,用實踐證明了QUIC協議在iOS平臺的可行性

1、QUIC介紹

(1)QUIC(Quick UDP Internet Connections)協議

是一種全新的基於UDP的web開發協議。可以用一個公式大致概括:

TCP + TLS + HTTP2 = UDP + QUIC + HTTP2’s API

從公式可看出:QUIC協議雖然是基於UDP,但它不但具有TCP的可靠性、擁塞控制、流量控制等,且在TCP協議的基礎上做了一些改進,比如避免了隊首阻塞;另外,QUIC協議具有TLS的安全傳輸特性,實現了TLS的保密功能,同時又使用更少的RTT建立安全的會話。

(2)QUIC協議的主要目的

是為了整合TCP協議的可靠性和UDP協議的速度和效率。

QUIC的維基百科頁面的介紹:

QUIC是快速UDP網路連線(英語:Quick UDP Internet Connections)的縮寫,這是一種實驗性的傳輸層網路傳輸協議,由Google公司開發,在2013年實現。QUIC使用UDP協議,它在兩個端點間建立連線,且支援多路複用連線。在設計之初,QUIC希望能夠提供等同於SSL/TLS層級的網路安全保護,減少資料傳輸及建立連線時的延遲時間,雙向控制頻寬,以避免網路擁塞。Google希望使用這個協議來取代TCP協議,使網頁傳輸速度加快,計劃將QUIC提交至網際網路工程任務小組(IETF),讓它成為下一代的正式網路規範。

(3)QUIC的特性

1)低延遲連線的建立 (Connection Establishment Latency)

這對已建立的連線很有好處。

眾所周知,建立一個TCP連線需要進行三次握手,這意味著每次連線都會產生額外的RTT,從而給每個連線增加了顯著的延遲(如下圖1所示)。

另外,如果還需要TLS協商來建立一個安全的、加密的https連線,那麼就需要更多的RTT,無疑會產生更大的延遲(如下圖所示)。


2163219-e07cadf71f11b674
這裡寫圖片描述

首次,QUIC協議可以在1個RTT中啟動一個連線並且獲取完成握手所需的必要資訊。

QUIC 1 RTT

如果連線的是一個新的伺服器,這時候client是沒有server的任何資訊的,當然也不知道用那種金鑰交換演算法,沒有公鑰資訊,就不可能實現0 RTT握手,所以,對於新的QUIC連線至少需要1 RTT才能完成握手。

在QUIC中,伺服器的配置是完全靜態的,而且配置是有過期時間的,由於伺服器配置是靜態的,因而不是每個連線都需要重新進行簽名操作,一個簽名可以適用於多個連線。

另外,QUIC採用了兩級金鑰機制:初始金鑰和會話金鑰。QUIC在握手過程中使用Diffie-Hellman 演算法協商初始金鑰。初始金鑰協商完畢後,伺服器會提供一個臨時隨機數,會馬上再協商會話金鑰,這樣可以保證金鑰的前向安全性,之後可以在通訊的過程中就實現對金鑰的更新。接收方意識到有新的金鑰要更新時,會嘗試用新舊兩種金鑰對資料進行解密,直到成功才會正式更新金鑰,否則會一直保留舊金鑰有效。

具體握手過程如圖(圖片引用daveywu的文章)所示:


2163219-fff182e0a320d852
這裡寫圖片描述

QUIC 0 RTT

客戶端在快取了ServerConfig的情況下,客戶端根據快取的ServerConifg獲取到金鑰交換演算法及公鑰,同時生成一個全新的金鑰,直接向伺服器傳送full Client hello訊息,開始正式握手,訊息中包括客戶端選擇的公開數。伺服器收到full Client hello,不同意回覆REJ;同意連線,則根據客戶端的公開數計算出初始金鑰,回覆SHLO訊息。

客戶端和伺服器根據臨時公開數和初始金鑰,各自基於SHA-256演算法推匯出會話金鑰。雙方更換會話金鑰通訊,初始金鑰已無用,至此,QUIC握手過程結束。

2)改進的擁塞控制 (Improved Congestion Control)
QUIC協議當前預設使用TCP協議的Cubic擁塞控制演算法。看似QUIC協議只是吧TCP的擁塞演算法重新實現了一遍,其實不然。QUIC協議在TCP擁塞演算法基礎上做了些改進:

1.可插拔

  • 應用程式層面就能實現不同的擁塞控制演算法,不需要作業系統或核心支援。
  • 單個應用程式的不同連線也能支援配置不同的擁塞控制。
  • 不需要停機和升級就能實現擁塞控制的變更。

2.單調遞增的Packet Number

  • QUIC並沒有使用TCP的基於位元組序號及ACK來確認訊息的有序到達,QUIC使用的是Packet Number,每個Packet Number嚴格遞增,所以如果Packet N丟失了,重傳Packet N的Packet Number已不是N,而是一個大於N的值。 這樣就很容易解決TCP的重傳歧義問題。

3.更多的ACK塊

  • QUIC ACK幀支援256個ACK塊,相比TCP的SACK在TCP選項中實現,有長度限制,最多隻支援3個ACK塊

4.精確計算RTT時間

  • QUIC ACK包同時攜帶了從收到包到回覆ACK的延時,這樣結合遞增的包序號,能夠精確的計算RTT。

3)無隊頭阻塞的多路複用 (Multiplexing without head-of-line blocking)

HTTP2的最大特性就是多路複用,而HTTP2最大的問題就是隊頭阻塞。

首先了解下為什麼會出現隊頭阻塞。比如HTTP2在一個TCP連線上同時傳送3個stream,其中第2個stream丟了一個Packet,TCP為了保證資料可靠性,需要傳送端重傳丟失的資料包,雖然這時候第3個資料包已經到達接收端,但被阻塞了。這就是所謂的隊頭阻塞。

而QUIC多路複用可以避免這個問題,因為QUIC的丟包、流控都是基於stream的,所有stream是相互獨立的,一條stream上的丟包,不會影響其他stream的資料傳輸。

4)前向糾錯 (Forward Error Correction)

QUIC使用了FEC(前向糾錯碼)來恢復資料,FEC採用簡單異或的方式,每傳送一組資料,包括若干個資料包後,並對這些資料包依次做異或運算,最後的結果作為一個FEC包再傳送出去。接收方收到一組資料後,根據資料包和FEC包即可以進行校驗和糾錯。比如:10個包,編碼後會增加2個包,接收端丟失第2和第3個包,僅靠剩下的10個包就可以解出丟失的包,不必重新傳送,但這樣也是有代價的,每個UDP資料包會包含比實際需要更多的有效載荷,增加了冗餘和CPU編解碼的消耗。

5)連線遷移 (Connection Migration)
TCP的連線是基於4元組的,而QUIC使用64為的Connection ID進行唯一識別客戶端和伺服器的邏輯連線,這就意味著如果一個客戶端改變IP地址或埠號,TCP連線不再有效,而QUIC層的邏輯連線維持不變,仍然採用老的Connection ID。

2、iOS平臺QUIC協議的可行性研究

QUIC協議在web端的應用有不少,比如Chromium專案,但移動端支援QUIC還比較少。所以在iOS平臺上,QUIC協議的可行性還不太確定。

(1)研究Chromium Projects

Chromium專案是開源的, The Chromium Projects(http://dev.chromium.org/chromium-projects) 文件詳細介紹了Chromium專案的實現原理,以及如何獲取原始碼並進行編譯。

獲取原始碼之前,需要先安裝depot_tools

$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

然後要配置環境變數

$ export PATH="$PATH:/path/to/depot_tools"

獲取原始碼:

$ mkdir chromium && cd chromium

$ fetch ios

$ cd src

獲取原始碼是很漫長的過程,Chromium專案的原始碼有8G,如果你的電腦剩餘儲存空間不足10G,基本就可以放棄了。另外獲取原始碼必須要翻牆,在公司的staff-wifi下,足足等了5個小時才獲取完原始碼。

然後就是編譯了,編譯也是需要很漫長的等待,不過可能跟機器的效能有關吧,反正我是等了1個多小時才編譯好……

首先編譯 ios/build/tools/setup-gn.py,編譯完會在out 目錄下生成幾個目錄,同時會生成一個Xcode工程。

到這裡,你可以選擇用Xcode編譯工程,或者直接用下面的命令列進行編譯

$ ninja -C out/Debug-iphonesimulator gn_all

詳細的過程請見Checking out and building Chromium for iOS(https://chromium.googlesource.com/chromium/src/+/master/docs/ios/build_instructions.md)

這裡其實走了不少彎路,首先是網路問題,必須要翻牆,開始是選擇公司dev-wifi,但dev-wifi下,命令列配置了代理仍然不能git clone。然後就想著直接從瀏覽器下載,下載是挺快的,用了不到1個小時,但編譯的時候提示沒有.git,還有各種檔案也找不到。。。看來是必須要git clone才行。 無奈之下,只好選擇用staff-wifi,但staff-wifi的網路很不穩定,git clone等待了5個小時才搞定。

用Xcode開啟上面生成的Xcode工程檔案,可以很清晰地看到Chromium專案目錄結構:

2163219-44fdd557c72261ab
這裡寫圖片描述
  • base:所有專案共享的程式碼,比如字串操作,工具類等。
  • build:編譯相關的檔案
  • cc:chromium compositor(合成器)實現。
  • chrome:Chromium browser相關程式碼
  • content:包含建立 多程式瀏覽器 所需要的核心程式碼。這裡 描述了為什麼要把這塊程式碼獨立出來。
  • net:網路庫
  • sql:對sqlite的封裝
  • third_party:一系列第三方庫,比如圖片解碼和壓縮庫, chrome/third_party 包含一些專門給Chrome用的第三方庫
  • ui/gfx:共享的繪圖類,基於Chromium的UI繪相簿。
  • ui/views:進行 UI 開發的簡單框架,提供了渲染、佈局、事件處理機制。大部分的瀏覽器 UI 都基於這個框架來實現。
  • url:Google的開源URL解析和規範化庫。

各個模組之間的依賴關係如圖所示


2163219-371c6f662afafd81
這裡寫圖片描述

(2)Stellite庫

公司內部也有一些使用QUIC協議的應用,比如QQ空間黃鑽頁面和遊戲應用頁面PC端,以及騰訊雲移動直播都已支援QUIC協議。這也讓我們有繼續研究下去的信心。

Line利用Cronet,用C++封裝了一層API,實現了Stellite,並在Github上進行了開源。開原始碼(https://github.com/line/stellite)

事實上,騰訊雲移動直播就是在Stellite基礎上對程式碼進行剝離,實現了自己的SDK。既然有先例,不妨就先用Stellite庫試下,搞起~

首先是編譯client,很簡單,Stellite提供了編譯指令碼

./tools/build.py --target-platform=ios --target stellite_http_client build

這個編譯也是很漫長的,因為它會把chromium的原始碼先clone下來,然後再編譯。一共花了5個多小時才編譯出來,比較坑的是,編譯是完全沒有log列印出來,一度以為是我的電腦卡住了,ctrl+c停止執行,居然列印出來下面這些log!!⊙︿⊙ 很明顯,它是在下載chromium原始碼,這下就可以放心了,說明它是有在執行的。


2163219-84285d22994f6a7d
這裡寫圖片描述

5個小時後,終於編譯結束,但失敗了,出現下面截圖中的錯誤。

2163219-522c5ad3859290ef
這裡寫圖片描述

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.0.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSUUID.h:26:49: error: nullability specifier ‘_Nullable’ cannot be applied to non-pointer type ‘uuid_t’ (aka ‘unsigned char [16]’)

  • (instancetype)initWithUUIDBytes:(const uuid_t _Nullable)bytes;

解決方法是:Xcode的Command Line Tools 選擇Xcode 8.0,猜測是因為Stellite庫編譯不支援iOS 11模擬器。

改為Xcode 8.0之後,重新編譯,終於在out目錄下看到了期盼已久的libstellite_http_client.a 庫。_

2163219-25fa8c96e6a614de
這裡寫圖片描述

(3)Cronet庫

Google Chrome提供了一個網路模組Cronet SDK,封裝了Chromium net,提供了Java介面和OC介面。業界也有直接使用Cronet的案例,比如蘑菇街(http://www.infoq.com/cn/articles/mogujie-app-chromium-network-layer

Andorid編譯Cronet庫是很方便的,而且Google有專門提供文件,Checking out and building Cronet for
Android(https://chromium.googlesource.com/chromium/src/+/master/components/cronet/android/build_instructions.md)

相對來說,iOS編譯就比較麻煩了。

首先要將cr_cronet.py link到你的當前目錄下,比如src目錄下。這樣用起來會比較方便,當然你也可以忽略這一步,每次都用cr_cronet.py的完整路徑。。。

~/chromium/src $ ln -s components/cronet/tools/cr_cronet.py somewhere/in/your/path

然後建立編譯資料夾:

~/chromium/src $ python cr_cronet.py gn

之後就可以開始編譯了

~/chromium/src $ cr_cronet.py build -d out/Debug-iphonesimulator

如果想deploy到真機,可以用下面的命令列

~/chromium/src $ python cr_cronet.py gn -i

- ~/chromium/src $ python cr_cronet.py build -i -d out/Debug-iphoneos

如果你沒有安裝最新的JDK,編譯的時候會一直提醒你進行安裝,所以最好是確保已安裝了最新的JAVA JDK和JRE。

編譯成功後,就可以在out目錄下看到生成的framework,可以直接在Xcode裡面開啟工程。


2163219-74fc9bfefebf8350
這裡寫圖片描述

3、QUIC協議實踐

因為Stellite 編譯比較簡單,這裡我是直接採用Stellite庫,將Chromium net移植到iOS,測試QUIC協議的。

Stellite提供了一些很方便的
API(https://github.com/line/stellite/blob/master/CLIENT_GUIDE.md),但Stellite是C++寫的,因為很久沒寫C++了,順便惡補了下語法,哈哈哈哈。。。

Xcode中引入libstellite_http_client.a庫,這個不贅述了,相信大家都會。

為了測試QUIC,以及對比QUIC和HTTP2的效能,我寫了個初步的Demo,Demo二維碼:

2163219-644fbcf0e08d101c
這裡寫圖片描述

附件中有具體的程式碼,有興趣可以看下,或者直接git clone http://git.code.oa.com/emilymmwang/QuicTest.git 檢視demo程式碼

Demo中使用Stellite庫提供的API請求url,程式碼如下:


- (void)requestUrl:(NSString*)url useQuic:(BOOL)useQuic
{
    if (url.length == 0) {
        return;
    }

    // 設定header
    stellite::HttpRequestHeader *header = new stellite::HttpRequestHeader;
    header->SetHeader("Q-UA","V1_IPH_SQ_7.3.0_0_HDBM_T");
    stellite::HttpRequest *request = new stellite::HttpRequest;
    request->url = [url UTF8String];
    request->request_type = stellite::HttpRequest::GET;

    // 設定params
    stellite::HttpClientContext::Params *stParams = new stellite::HttpClientContext::Params;
    if (useQuic) {
        stParams->using_quic = true;
        stParams->using_disk_cache = true;
        std::vector<std::string> strings;
        strings.push_back("https://stellite.io:443");
        stParams->origins_to_force_quic_on = strings;
    } else {
        stParams->using_http2 = true;
        stParams->using_disk_cache = true;
    }

    // 初始化context
    stellite::HttpClientContext *context = new stellite::HttpClientContext(*stParams);
    context->Initialize();

    downloadDuration = CFAbsoluteTimeGetCurrent();
    // 開始請求
    MyHttpResponseDelegate *delegate = new MyHttpResponseDelegate;
    stellite::HttpClient *client = context->CreateHttpClient(delegate);
    client->Request(*request);
}

useQuic 為YES表示用QUIC協議,NO表示用http2協議

MyHttpResponseDelegate 程式碼:

class MyHttpResponseDelegate:public stellite::HttpResponseDelegate
{
public:
    void OnHttpResponse(int request_id, const stellite::HttpResponse& response,
                        const char* body, size_t body_len) {
        if (response.response_code == 200) { // 成功
            downloadDuration = CFAbsoluteTimeGetCurrent() - downloadDuration;
            NSData *data = [NSData dataWithBytes:body length:body_len];
            BOOL useQuic = (response.connection_info == stellite::HttpResponse::CONNECTION_INFO_QUIC1_SDPY3);
            [[libTest instance] saveImage:[UIImage imageWithData:data] downloadDuration:downloadDuration useQuic:useQuic];
            NSLog(@"OnHttpResponse success downloadDuration=%lf data:%s connect_info=%zd",downloadDuration, body, response.connection_info);
        }
    }
    void OnHttpStream(int request_id, const stellite::HttpResponse& response,
                      const char* stream, size_t stream_len,
                      bool is_last){
    }
    // The error code are defined at net/base/net_error_list.h
    void OnHttpError(int request_id, int error_code,
                     const std::string& error_message) {
    }

    virtual void OnHttpHeader(int request_id, const stellite::HttpResponse& response) {
        NSLog(@"OnHttpHeader downloadDuration=%lf", CFAbsoluteTimeGetCurrent() - downloadDuration);
    }
};

為了確保確實是使用的QUIC協議,特地抓包看了下:


2163219-f8d05e510518984b
這裡寫圖片描述

最終,引入libstellite_http_client.a庫,安裝包增加了3M左右。有經驗表明可以對Chromium原始碼進行剝離,減少安裝包大小,這個還待研究

4、QUIC協議和Http2對比資料

測試請求圖片url:
https://vip.qzone.qq.com/proxy/domain/qzonestyle.gtimg.cn/qzone/space_item/boss_pic/2472_2017_11/1512034326193_704231.jpg

感謝yippeehuang 提供的圖片,因為QQ空間遊戲應用頁面現在用的是QUIC協議,所以該測試資料直接是連線的他們的伺服器。

我用 QUIC 和 HTTP2 分別在 wifi網路 和 4G網路 請求上面的圖片(圖片大小:33K),wifi和4G下分別做了10組測試,具體的下載總耗時(單位:ms)對比資料如下:

wifi下:


2163219-5f7425a207e15912
這裡寫圖片描述

4G網路下:


2163219-8572cf30fa756c1a
這裡寫圖片描述

從表格可以看出,wifi網路和4G網路下,QUIC協議下載的總耗時比Http2要小,相對於Http2,wifi下,QUIC在下載總耗時上提升了14%左右,4G下提升18%左右。當然,這只是針對一張圖片進行的測試,可能不具有代表性,但可以大致看出QUIC在下載耗時方面還是有所提升的。

目前只是對QUIC進行初步研究,後續將會繼續熟悉Chromium原始碼。

相關文章