84

QUIC协议初探-iOS实践

 6 years ago
source link: http://mp.weixin.qq.com/s/NbewZ1NU49qSjIcdFrpotw
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

| 导语 本文主要介绍了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,无疑会产生更大的延迟(如下图所示)。

Image

首次,QUIC协议可以在1个RTT中启动一个连接并且获取完成握手所需的必要信息。

QUIC 1 RTT

如果连接的是一个新的服务器,这时候client是没有server的任何信息的,当然也不知道用那种密钥交换算法,没有公钥信息,就不可能实现0 RTT握手,所以,对于新的QUIC连接至少需要1 RTT才能完成握手。

在QUIC中,服务器的配置是完全静态的,而且配置是有过期时间的,由于服务器配置是静态的,因而不是每个连接都需要重新进行签名操作,一个签名可以适用于多个连接。

另外,QUIC采用了两级密钥机制:初始密钥和会话密钥。QUIC在握手过程中使用Diffie-Hellman 算法协商初始密钥。初始密钥协商完毕后,服务器会提供一个临时随机数,会马上再协商会话密钥,这样可以保证密钥的前向安全性,之后可以在通信的过程中就实现对密钥的更新。接收方意识到有新的密钥要更新时,会尝试用新旧两种密钥对数据进行解密,直到成功才会正式更新密钥,否则会一直保留旧密钥有效。

具体握手过程如图(图片引用daveywu的文章)所示:

Image

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项目目录结构:

Image
  • 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解析和规范化库。

各个模块之间的依赖关系如图所示

Image

(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源码,这下就可以放心了,说明它是有在运行的。

Image

5个小时后,终于编译结束,但失败了,出现下面截图中的错误。

Image

/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 库。^^

Image

(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里面打开工程。

Image

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二维码:

Image

附件中有具体的代码,有兴趣可以看下,或者直接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协议,特地抓包看了下:

Image

最终,引入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下:

Image

4G网络下:

Image

从表格可以看出,wifi网络和4G网络下,QUIC协议下载的总耗时比Http2要小,相对于Http2,wifi下,QUIC在下载总耗时上提升了14%左右,4G下提升18%左右。当然,这只是针对一张图片进行的测试,可能不具有代表性,但可以大致看出QUIC在下载耗时方面还是有所提升的。

目前只是对QUIC进行初步研究,后续将会继续熟悉Chromium源代码。


如果您觉得我们的内容还不错,就请转发到朋友圈,和小伙伴一起分享吧~

Image

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK