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对比数据

测试请求图片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源代码。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容