QUIC是Google新開發的一個基于UDP的協定,它提供了像TCP一樣的傳輸可靠性保證,可以實作資料傳輸的0-RTT延遲,靈活的設計使我們可以對它的擁塞控制及流量控制做更多的定制,它還提供了傳輸的安全性保障,以及像HTTP/2一樣的應用資料二進制分幀傳輸。
而QUIC協定最最吸引人的特性有兩點,一是對隊首阻塞問題的解決更為徹底。基于TCP的HTTP/2,盡管從邏輯上來說,不同的流之間互相獨立,不會互相影響,但在實際傳輸方面,資料還是要一幀一幀的發送和接收,一旦某一個流的資料有丢包,則同樣會阻塞在它之後傳輸的其它與它毫不相幹的流的資料的傳輸。而基于UDP的QUIC協定則可以更為徹底地解決這樣的問題,讓不同的流之間真正的實作互相獨立傳輸,互不幹擾。
另一個特性切換網絡時的連接配接保持。目前移動端的應用環境,使用者的網絡可能會經常切換,比如從辦公室或家裡出門,WiFi斷開,網絡切換為3G或4G。基于TCP的協定,由于切換網絡之後,IP會改變,因而之前的連接配接不可能繼續保持。而基于UDP的QUIC協定,則可以内建與TCP中不同的連接配接辨別方法,進而在網絡完成切換之後,恢複之前與伺服器的連接配接。
由于這些良好的特性,QUIC協定已經有在gmail中得到了大量的應用。打開Wireshark,随便對某個網卡抓包,都能看到大量的QUIC協定包:
這裡我們來跑一下QUIC。
選擇一份QUIC代碼
下面的說明是用來基于chromium代碼庫編譯QUIC代碼。在Chrome支援的任何平台上,這裡的說明都能保證是有效的,遇到問題時可以檢視一些擴充的故障排查的文檔。如果不想下載下傳整個chromium代碼庫,則可以嘗試github上快速而幹淨的proto-quic庫。這是chromium中的代碼的一份克隆,但剔除了大多數不必要的依賴,因而下載下傳它要快得多,編譯也更快,但不一定在所有的平台上都能用。事實上,github上的proto-quic庫,目前隻支援在Ubuntu linux上編譯。
編譯QUIC用戶端和伺服器
Chromium中提供了一個示例用戶端和伺服器實作。要使用這些東西,你首先應該已經下載下傳Chromium的源代碼,然後建構二進制檔案:
ninja -C out/Debug quic_server quic_client
這就像編譯chromium的任何子產品一樣。可以參考 懶人chromium net android移植指南 來對Chromium的建構系統做更多了解。
如果條件允許,github上的proto-quic庫,編譯起來也很簡單快捷。首先需要下載下傳代碼及建構依賴的整個工具鍊:
git clone https://github.com/google/proto-quic.gitcd proto-quicexport PATH=$PATH:`pwd`/depot_tools
./proto_quic_tools/sync.sh
然後編譯就是了,與普通的chromium子產品編譯一樣:
cd src
gn gen out/Default && ninja -C out/Default quic_client quic_server net_unittests
從www.example.org準備測試資料
下載下傳一份www.example.org的拷貝,它主要是給quic_server二進制可執行檔案用來提供本地服務的:
mkdir ~/quic-data
cd ~/quic-data
wget -p --save-headers https://www.example.org
這裡主要是要下載下傳一個html檔案,且儲存檔案的所有的HTTP header,當然也可以從其它的站點下載下傳這個檔案。
手動地編輯index.html,并調整如下的headers:
- 移除(如果存在的話):"Transfer-Encoding: chunked"
- 移除(如果存在的話):"Alternate-Protocol: ..."
- 添加:X-Original-Url: https://www.example.org/
生成證書
為了運作伺服器,需要一個有效的證書,及一個pkcs8格式的私有key。如果沒有,則可以使用腳本來産生它們:
cd net/tools/quic/certs
./generate-certs.shcd -
除了伺服器的證書及public key,這個腳本也會産生一個CA憑證 (net/tools/quic/certs/out/2048-sha256-root.pem),需要把它添加到作業系統的根證書商店以便于在證書驗證期間它被信任。
一種比較簡單的管理證書的方法是使用chrome浏覽器。在位址欄中輸入
chrome://settings/search#ssl
,然後點選“管理證書”:
在彈出的視窗中選擇“導入...”,然後按照提示一步步将證書導入即可。
在linux上管理證書相關的更多資訊,請參考 這些說明 。
如果遺漏了這裡的添加CA憑證的步驟的話,後面在執行quic_client的時候會報出如下的證書驗證錯誤:
$ ./out/Default/quic_client --host=127.0.0.1 --port=80 https://www.example.org/[1008/164047:ERROR:cert_verify_proc_nss.cc(942)] CERT_PKIXVerifyCert for www.example.org failed err=-8179[1008/164047:WARNING:proof_verifier_chromium.cc(466)] Failed to verify certificate chain: net::ERR_CERT_AUTHORITY_INVALID
Failed to connect to 127.0.0.1:80. Error: QUIC_PROOF_INVALID
運作QUIC伺服器和用戶端
運作quic_server:
./out/Default/quic_server \
--quic_in_memory_cache_dir=~/quic-data/www.example.org \
--certificate_file=net/tools/quic/certs/out/leaf_cert.pem \
--key_file=net/tools/quic/certs/out/leaf_cert.pkcs8
還可以通過--port參數指定quic_server監聽的端口,及--v參數指定輸出更多資訊,如:
./out/Default/quic_server --certificate_file=~/proto-quic/src/net/tools/quic/certs/out/leaf_cert.pem --key_file=~/proto-quic/src/net/tools/quic/certs/out/leaf_cert.pkcs8 --quic_in_memory_cache_dir=~/quic-data/www.example.com --port=32457 --v=1
quic_in_memory_cache_dir參數指定存放資源檔案的目錄路徑。如我們前面看到的,從www.example.org下載下傳到頁面之後,需要調整headers,其中為
X-Original-Url
設定的是相應資源的url。quic_server起來的時候,會加載該目錄下的檔案。要為quic_server添加其它的測試資源,也要注意正确的設定其headers。如json接口:
# cat testApi1
HTTP/1.1 200 OK
Cache-Control: privateContent-Length: 3214Content-Type: application/json; charset=utf-8Server: Microsoft-IIS/7.5X-AspNet-Version: 4.0.30319X-Powered-By: ASP.NETDate: Thu, 13 Oct 2016 02:12:28 GMT
X-Original-Url: https://www.wolfcstech.com:6121/testApi1{"res":[{"ctime":"2016-10-11","title":"溫州","des":"國内","url":"http://news.163.com/16/1011/11/C33GFLIP0001124J.html"},{"ime":"2016","tit":"b","des":"a"}, {"ime":"2016","tit":"b","des":"a"}],"rea":"Su"}
然後就可以使用quic_client以QUIC協定請求檔案了:
./out/Default/quic_client --host=127.0.0.1 --port=32457 https://www.example.org/
注意,如果要讓伺服器運作于端口32457上,則必須為用戶端指定端口,因為它預設是80。
此外,如果本地機器有多個loopback位址 (由于它同時使用IPv4 和 IPv6),則不得不標明一個位址。
目前還不确定後面的缺點是不是一個bug。
注意:client和server都主要是為了做內建測試的:它們都不能大規模使用。
要使用chrome來測試相同的下載下傳過程,可以執行:
chromium-browser \ --user-data-dir=/tmp/chrome-profile \
--no-proxy-server \
--enable-quic \
--origin-to-force-quic-on=www.example.org:443 \
--host-resolver-rules='MAP www.example.org:443 127.0.0.1:32457' \
https://www.example.org
使用Cronet通路QUIC服務
除了使用quic_client和Chrome浏覽器之外,還可以在移動端使用Cronet通路QUIC服務。正常的使用Cronet通路網絡服務的流程為:
- 使用CronetEngine.Builder建立CronetEngine,也及URL請求上下文,設定是否啟用緩存,是否啟用HTTP/2等。
- 實作UrlRequest.Callback,用以接收請求執行的結果。
- 使用UrlRequest.Builder,以前面建立的CronetEngine和UrlRequest.Callback建立UrlRequest。
- 啟動UrlRquest的執行。
- 請求執行完成之後,在UrlRequest.Callback中處理請求執行結果。
通路QUIC服務相對于正常的使用Cronet通路網絡服務的流程的差别主要有以下幾點:
- 在上面的第1部,建立CronetEngine時,要啟用QUIC,同時要為CronetEngine添加Quic Hint。如:
CronetEngine.Builder builder = new CronetEngine.Builder(context); builder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_IN_MEMORY, 100 * 1024) .enableHttp2(true) .enableQuic(true) .enableSDCH(true) .setLibraryName("cronet") .addQuicHint("www.example.org", 32457, 32457) .addQuicHint("www.wolfcstech.com", 443, 6121) .addQuicHint("www.wolfcstech.cn", 443, 6121) .addQuicHint("www.wolfcstech.com", 6121, 6121) .addQuicHint("www.wolfcstech.cn", 6121, 6121); mCronetEngine = builder.build();
- 用以https為scheme的URL通路QUIC服務。如:
String url = "https://www.wolfcstech.com:6121/testApi1"; UrlRequest.Builder builder = new UrlRequest.Builder(url, callback, mExecutor, mCronetEngine); applyPostDataToUrlRequestBuilder(builder, mExecutor, postData); builder.build().start();
QuicHint是 (主機名, 端口号, 備選端口号) 的三元組。為CronetEngine添加Quic Hint訓示其在執行請求時,在以TCP協定連接配接 (主機名, 端口号) 并向其發送正常http或https請求的同時,以QUIC協定向 (主機名, 備選端口号) 請求相同的服務。
為CronetEngine添加加Quic Hint,是将Quic Hint添加進了構造中的URLRequestContextConfig:
static void AddQuicHint(JNIEnv* env, const JavaParamRef<jclass>& jcaller,
jlong jurl_request_context_config, const JavaParamRef<jstring>& jhost,
jint jport,
jint jalternate_port) {
URLRequestContextConfig* config = reinterpret_cast<URLRequestContextConfig*>(jurl_request_context_config);
config->quic_hints.push_back(
base::WrapUnique(new URLRequestContextConfig::QuicHint(
base::android::ConvertJavaStringToUTF8(env, jhost), jport,
jalternate_port)));
}
在構造net::URLRequestContext時,相關的這些資訊會被儲存起來,以備後續通路QUIC服務之用:
if (config->enable_quic) { for (auto hint = config->quic_hints.begin();
hint != config->quic_hints.end(); ++hint) { const URLRequestContextConfig::QuicHint& quic_hint = **hint; if (quic_hint.host.empty()) { continue;
}
url::CanonHostInfo host_info;
std::string canon_host(net::CanonicalizeHost(quic_hint.host, &host_info)); if (!host_info.IsIPAddress() &&
!net::IsCanonicalizedHostCompliant(canon_host)) { continue;
} if (quic_hint.port <= std::numeric_limits<uint16_t>::min() ||
quic_hint.port > std::numeric_limits<uint16_t>::max()) { continue;
} if (quic_hint.alternate_port <= std::numeric_limits<uint16_t>::min() ||
quic_hint.alternate_port > std::numeric_limits<uint16_t>::max()) { continue;
}
url::SchemeHostPort quic_server("https", canon_host, quic_hint.port);
net::AlternativeService alternative_service(
net::AlternateProtocol::QUIC, "", static_cast<uint16_t>(quic_hint.alternate_port));
context_->http_server_properties()->SetAlternativeService(
quic_server, alternative_service, base::Time::Max());
}
}
這種通路QUIC服務的奇怪方式,要求終端事先知道通路一個特定網站所用的協定及服務的端口。這樣是非常不靈活的,因而它主要用于缺乏适當的協定協商機制的情況下,用于QUIC協定的實驗階段。不久前,有一個稱為 替代服務(Alternative Services) 的HTTP機制标準化了,其标準規範文檔為 RFC7838。這種機制允許對一個HTTP資源的通路,被重定向到另外的一個網絡位置,甚至是以一種不同的協定配置來通路。具體而言,這種機制為HTTP新增了一個頭部字段
Alt-Svc
,HTTP伺服器可以通過這個頭部字段,告知用戶端伺服器被重定向到的服務的資訊,包括協定,端口号等,如:
Alt-Svc: h2="new.example.org:80"
故障排查
如果你在運作時遇到了問題,則可以以--v=1參數運作伺服器或用戶端。它将提升日志的verbosity,更多的日志常常可以幫助暴露底層的問題。
參考文檔: Playing with QUIC
網易雲新使用者大禮包:https://www.163yun.com/gift
本文來自網易雲社群,經作者韓鵬飛授權釋出。