QUIC协议初探-iOS实践
| 导语 本文主要介绍了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目录下看到了期盼已久的libstellitehttp_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
附件中有具体的代码,有兴趣可以看下,或者直接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对比数据
测试请求图片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下:
4G网络下:
从表格可以看出,wifi网络和4G网络下,QUIC协议下载的总耗时比Http2要小,相对于Http2,wifi下,QUIC在下载总耗时上提升了14%左右,4G下提升18%左右。当然,这只是针对一张图片进行的测试,可能不具有代表性,但可以大致看出QUIC在下载耗时方面还是有所提升的。
目前只是对QUIC进行初步研究,后续将会继续熟悉Chromium源代码。
如果您觉得我们的内容还不错,就请转发到朋友圈,和小伙伴一起分享吧~
- 拼凑了几个自定义的Panel(包括FishEyePanel,WrapPanel等几个常用的布局)
- jquery获取父级一级节点的序号
- Docker容器学习梳理--基础知识(2)
- Blend生成的TransformGroup如何引用?
- 今日头条写新闻机器人获吴文俊人工智能科技发明奖
- Docker容器学习梳理--应用程序容器环境部署
- 异步方式访问网页
- Silverlight:利用Panel实现自定义布局
- 《物联网智能终端信息安全白皮书》再次敲响物联网时代警钟
- Gitlab可视化代码树插件-Octotree
- 子线程调用UI线程的方法
- Silverlight:Dependency Property(依赖属性)学习笔记
- Silverlight:利用异步加载Xap实现自定义loading效果
- Docker容器学习梳理--手动制作系统镜像
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- 数学--数论--HDU--5878 Count Two Three 2016 ACM/ICPC Asia Regional Qingdao Online 1001
- ACM-ICPC 2019 山东省省赛D Game on a Graph
- 数学--数论--HDU6919 Senior PanⅡ【2017多校第九场】
- 数学--数论--Alice and Bob (CodeForces - 346A )推导
- ACM-ICPC 2019 山东省省赛 M Sekiro
- 数学--数论--HDU2136 Largest prime factor 线性筛法变形
- ACM-ICPC 2019 山东省省赛 C Wandering Robot
- 数据库SQL语言从入门到精通--Part 2--MySQL安装
- ACM-ICPC 2019 山东省省赛 A Calandar
- POJ 1845-Sumdiv(厉害了这个题)
- 数据库SQL语言从入门到精通--Part 1--SQL语言概述
- DP背包(一)
- 程序员最喜欢用的在线代码编译器,什么?你竟然不知道!可以在网页敲代码,运行调试!
- ZOJ 3623 Battle Ships
- POJ 2955 区间DP必看的括号匹配问题,经典例题