QUIC 協議初探 - iOS 實踐
本文來自於騰訊 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,無疑會產生更大的延遲(如下圖所示)。
首次,QUIC協議可以在1個RTT中啟動一個連線並且獲取完成握手所需的必要資訊。
QUIC 1 RTT
如果連線的是一個新的伺服器,這時候client是沒有server的任何資訊的,當然也不知道用那種金鑰交換演算法,沒有公鑰資訊,就不可能實現0 RTT握手,所以,對於新的QUIC連線至少需要1 RTT才能完成握手。
在QUIC中,伺服器的配置是完全靜態的,而且配置是有過期時間的,由於伺服器配置是靜態的,因而不是每個連線都需要重新進行簽名操作,一個簽名可以適用於多個連線。
另外,QUIC採用了兩級金鑰機制:初始金鑰和會話金鑰。QUIC在握手過程中使用Diffie-Hellman 演算法協商初始金鑰。初始金鑰協商完畢後,伺服器會提供一個臨時隨機數,會馬上再協商會話金鑰,這樣可以保證金鑰的前向安全性,之後可以在通訊的過程中就實現對金鑰的更新。接收方意識到有新的金鑰要更新時,會嘗試用新舊兩種金鑰對資料進行解密,直到成功才會正式更新金鑰,否則會一直保留舊金鑰有效。
具體握手過程如圖(圖片引用daveywu的文章)所示:
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專案目錄結構:
- 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解析和規範化庫。
各個模組之間的依賴關係如圖所示
(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原始碼,這下就可以放心了,說明它是有在執行的。
5個小時後,終於編譯結束,但失敗了,出現下面截圖中的錯誤。
/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 庫。_
(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裡面開啟工程。
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二維碼:
附件中有具體的程式碼,有興趣可以看下,或者直接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協議,特地抓包看了下:
最終,引入libstellite_http_client.a庫,安裝包增加了3M左右。有經驗表明可以對Chromium原始碼進行剝離,減少安裝包大小,這個還待研究
4、QUIC協議和Http2對比資料
感謝yippeehuang 提供的圖片,因為QQ空間遊戲應用頁面現在用的是QUIC協議,所以該測試資料直接是連線的他們的伺服器。
我用 QUIC 和 HTTP2 分別在 wifi網路 和 4G網路 請求上面的圖片(圖片大小:33K),wifi和4G下分別做了10組測試,具體的下載總耗時(單位:ms)對比資料如下:
wifi下:
4G網路下:
從表格可以看出,wifi網路和4G網路下,QUIC協議下載的總耗時比Http2要小,相對於Http2,wifi下,QUIC在下載總耗時上提升了14%左右,4G下提升18%左右。當然,這只是針對一張圖片進行的測試,可能不具有代表性,但可以大致看出QUIC在下載耗時方面還是有所提升的。
目前只是對QUIC進行初步研究,後續將會繼續熟悉Chromium原始碼。
相關文章
- QUIC加密協議UI加密協議
- QUIC協議詳解UI協議
- quinn-rs/quinn: QUIC協議的Rust實現UI協議Rust
- 10 分鐘講完 QUIC 協議。UI協議
- gopher 協議初探Go協議
- MQTT協議實踐MQQT協議
- 匯流排協議系列——USART協議初探協議
- 下一代通訊協議:QUIC協議UI
- 讓網際網路更快:新一代QUIC協議在騰訊的技術實踐分享UI協議
- 基於QUIC協議的HTTP/3正式釋出!UI協議HTTP
- RPC協議實踐入門RPC協議
- tuic:基於QUIC協議的Rust高效能代理UI協議Rust
- KCP協議:從TCP到UDP家族QUIC/KCP/ENET協議TCPUDPUI
- QUIC 多流橋接、新增 DDS 協議轉換代理UI橋接協議
- 谷歌QUIC協議推動Web從TCP遷移到UDP谷歌UI協議WebTCPUDP
- 下一代HTTP底層協議將棄用TCP協議 改用QUIC技術HTTP協議TCPUI
- Android上kcp協議使用初探Android協議
- WebSocket原理與實踐(二)---WebSocket協議Web協議
- 時間同步協議NTP - 原理&實踐協議
- HLS直播協議在B站的實踐協議
- 網易雲信 QUIC 應用優化實踐UI優化
- React Hooks:初探·實踐ReactHook
- 積跬步至千里:QUIC 協議在螞蟻集團落地之綜述UI協議
- 大型網站的 HTTPS 實踐(四):協議層以外的實踐網站HTTP協議
- 大型網站的 HTTPS 實踐(四)——協議層以外的實踐網站HTTP協議
- MQTT協議詳解及v5.0實踐MQQT協議
- MOSN 多協議擴充套件開發實踐協議套件
- DDS協議解讀及測試開發實踐協議
- iOS網路基礎 HTTP協議iOSHTTP協議
- iOS開發- tableView的協議iOSView協議
- 支援Http3和Quic協議的Netty孵化器版本釋出HTTPUI協議Netty
- SMTP協議初探(二)----linux下c程式設計實現發郵件協議LinuxC程式程式設計
- Java安全之初探weblogic T3協議漏洞JavaWeb協議
- 實用TCP協議(1):TCP 協議簡介TCP協議
- 協程初探
- 實現 Raft 協議Raft協議
- 谷歌的QUIC協議會取代TCP嗎? - levelup谷歌UI協議TCP
- SMTP協議初探(1)----dos下telnet命令發郵件協議