本文来自于腾讯 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源代码。