第5章 网络
5.1 HTTP 1.0
5.1.1 HTTP 1.0的问题
http协议的特点是"一来一回"。这样的协议有2个问题:
1.性能问题
2.服务端推送问题
5.1.2 Keep-Alive机制与Content-Length属性
为了解决第一个问题,http 1.0 设计了一个 Keep-Alive 机制来实现tcp连接的复用。具体来说,就是客户端在http请求的头部加上一个字段
Connection:Keep-Alive。服务端接收到这样字段的请求,在处理完请求后不会关闭连接。,同时在http 的 Response 里面也会加上该字段,然后
等待客户端在该连接上发送下一个请求。
当然,这会给服务器带来一个问题:连接数有限。如果每个连接都不关闭的话,一段时间后,服务器连接数就被耗光了。因此,服务器会有一个
Keep-Alive timeout 的参数,过一段时间后,如果该连接上没有新的请求进来,则连接就会关闭。
连接复用又产生一个新的问题:以前一个连接只发送一个请求,返回一个响应,服务器处理完毕,把连接关闭,这个时候客户端就知道连接的请求处理
结束了。但现在,即使一个请求处理完了,连接也不关闭,那么客户端怎么知道连接处理结束了呢?或者说,客户端怎么知道接收回来的数据是完整的?
答案是在http response 的头部,返回了一个 Content-Length:xxx 的字段,告诉客户端 http response 的 Body 共有多少个字节,
客户端接收到这么多个字节后就知道响应成功接收完毕。
5.2 HTTP 1.1
5.2.1 连接复用与Chunk机制
连接的复用很有必要,所以到了http 1.1 后,就把连接复用变成默认属性了。除非在请求头显式的加上 Connection:close 属性。
在http 1.0 里面可以利用 Content-Length 字段,让客户端判断一个请求的响应成功是否接收完毕。但Content-Length 有个问题,如果
服务器返回的数据是动态语言生成的内容,则要计算Content-Length,这点对服务器来说比较困难。即使能够计算,也需要服务器在内存中渲染出整个
页面,然后计算长度,非常耗时。
为了,在http 1.1 中引入了 Chunk 机制。具体来说,就是在响应头加上 Transfer-Encoding:chunked 属性,其目的是告诉客户端,响应
的Body 分成了一块块的,块与块之间有间隔符,所有块的结尾也有个特殊标记。这样,即使没有Content-Length 字段,也能够方便客户端判断出响应
的尾部。
5.2.2 Pipeline与Head-of-line Blocking问题
有了连接复用后,减少了建立连接,关闭连接的开销。但还存在一个问题,在同一个连接上,请求是串行的,客户端发送一个请求,收到响应,然后
发送下一个请求,再接收响应。这种串行的方式,导致并发度不够。
为了,http 1.1 引入了 Pipeline 机制。在同一个tcp连接上,可以在一个请求发出去之后,响应没回来之前,就可以发送下一个,再下一个
请求,这样就提高了同一个tcp连接上面的处理请求的效率。
但Pipeline有个致命的问题,就是 Head-of-Line Blocking 翻译成中文 "队头阻塞"。什么意思呢?
客户端发送的请求顺序是1,2,3,虽然服务器是并发处理的,但客户端接收到的响应顺序也必须是1,2,3,如此才能把响应和请求成功配对,跟
队列一样,先进先出。一旦队列头部请求1发生延迟,客户端迟迟收不到请求1的响应,则请求2,请求3的响应也会被阻塞。
正因为如此,为了避免 Pipeline 带来的副作用,很多浏览器默认把 Pipeline 关闭了。
5.2.3 HTTP/2出现之前的性能提升方法
一方面,pipeline 不能用,在同一个tcp连接上面,请求是串行的;另一方面,对于同一个域名,浏览器限制只能开6~8个连接。如何提升?
1.Spriting技术(雪碧图)
这种技术专门针对小图片,假设在一个网页里,要从服务器加载很多小图片(比如小图标),可以在服务器里把小图片拼成一个大图片,到了
浏览器,再通过js或者css,从大图片截取一块显示。之前要发送很多小图片的http请求,现在只要发送一个请求就可以了。
2.内联(Inlining)
内联是另外一种针对小图片的技术,它将图片的原始数据嵌入在css文件里面。如:
.icon1 {
background:url(data:image/png;base64,<data>) no-repeat;
}
3.JS拼接
把大量小的js文件合并成一个文件并压缩,让浏览器在一个请求里面下载完。
4.请求的分片技术
对于一个域名,浏览器会限制只能开6~8个连接。可以多做几个域名,这样就相当于绕开了浏览器的限制。
如把静态资源放在cdn上,做一批cdn域名,这样浏览器可以为每个域名都建立6~8个连接,从而提高并发度。
5.2.4 “一来多回”问题
无论是http 1.0 还是 http 1.1,都无法直接做到服务器主动推送。常见的解决方案有:
1.客户端定期轮询
比如客户端每5s向服务器发送一个http请求,服务器如果有新消息,就返回。
定期轮询效率比较低下,又增加了服务器的压力。
2.FlashSocket/WebSocket
不再是http,而是直接基于tcp,但也有一定的局限性。
3.Http长轮询
客户端发送一个http请求,如果服务器有新消息,就立即返回;如果没有,则服务器夯住此连接,客户端一直等待该请求返回。然后经过
一个约定的时间后,如果服务器还没有新消息,服务器就返回一个空的消息(客户端和服务端约定好的一个消息)。客户端收到空消息后关闭连接,
再发起一个新连接,重复此过程。
这就相当于利用http实现了tcp长连接的效果,这是目前web最常用的服务器端推送的方法。
4.HTTP Streaming
服务端利用 Transfer-Encoding:chunked 机制,发送一个"没完没了"的chunk流,就一个连接,但其response永远接收不完。
与长轮询的区别在于,这里只有一个http请求,不存在http header 不断重复的问题,但实现时没有长轮询简单。
5.2.5 断点续传
http 1.1 还有一个很实用的特性:"断点续传"。当客户端从服务端下载文件时,如果下载到一半连接中断了,再建立新连接之后,客户端可以从
上次断开的地方继续下载。具体实现也很简单,客户端一边下载一边记录下载的数据量大小,一旦连接中断了,重建立连接后,在请求的头部加上
Range:first offset - last offer 字段,指定从某个 offset 下载到某个offset,服务器就可以只返回(first offset, last offset)
之间的数据。
这里需要补充的是,http 1.1 的这种特性只适用于端点下载。要实现断点上传,就需要自己实现了。
5.3 HTTP/2
因为http 1.1 的pipeline不够完善,web开发者们想出的各种办法去提高http1.1的效率,但这些方法都是从应用层面去解决的,没有普适性。
因此有人想在协议层面去解决这个问题,这就是google公司的 SPDY 协议的初衷。
吸取SPDY的经验和教训,在此基础上制定了 http/2协议。可以看出,http/2 在一开始就有很好的业界实践基础,之所以叫http/2,没有叫
http 2.0,也是因为工作组认为该协议已经很完善了,后续不会再有小版本,如果有的话,也是下一个版本 http/3。
5.3.1 与HTTP 1.1的兼容
http 1.1 已经是当今互联网的主流,因此 http/2 在设计过程中,首先要考虑的是与http 1.1 的兼容,意味着:
1.不能改变 http://, https:// 这样的url规范;
2.不能改变http Request / http Response 的报文结构。
如何做到不改变Request/Response报文结构的情况下,发明http/2? 这是理解 http/2 的关键。
http/2 和 http 1.1 并不是处在平级的位置,而是处在 http1.1 和 tcp 之间。以前http 1.1 直接构建在 tcp 之上;现在相当于在
http 1.1 和 tcp 之间多了一个转换层,这个转换成就是 SPDY,也就是现在的 http/2。
5.3.2 二进制分帧
二进制分帧是 http/2 为了解决 http 1.1 的 "队头阻塞"问题所涉及的核心特性,是转换层所做的核心工作。
http 1.1 本身是明文的字符格式,所谓的二进制分帧,是指在把这个字符格式的报文发送给tcp之前转换成二进制,并且分成多个帧(多个数据块)
发送。
对于每个域名,在浏览器和服务器之间,只维护一个tcp连接。因为tcp是全双工的,即来回两个通道。
这里的请求1,2,3,响应1,2,3 是 http 1.1 的明文字符报文。每个请求在发送之前被转换成二进制,然后分成多个帧发送。每个响应在回复
之前,也被转成了二进制,然后分成多个帧发送。如 请求1被分成F1,F2,F3 三个帧;请求2被分成F4,F5 2个帧;请求3被分成F6,F7 2个帧;F1~F7
是被乱序发出去的,到了服务器被重新组装。
这里有一个关键问题:请求和响应都是被打散后分成多个帧乱序的发送出去的,请求和响应都需要重新组装起来,同时请求和响应还要一一配对。
那么组装和配对如何实现的呢?原理也很简单,每个请求和响应实际上组成了一个逻辑上的"流",为每一条流分配一个ID,把这个ID作为标签,打到
每一个帧上。如上,有三个流,三个流ID,分别打到三条流里面每一个帧上。
有了这个二进制分帧之后,在tcp层面,虽然是并行的;但从http层面来看,请求就是并发的发送出去,并发的接收的。\
有了二进制分帧,是不是彻底解决了Pipeline的"队头阻塞"问题呢?其实还没有,只是把"队头阻塞"问题从http request 粒度细化到了"帧"
的粒度。
只要用tcp协议,就绕不开"队头阻塞"的问题,因为tcp协议是先进先出的。如,F3帧(队头的第一个帧)在网络上被阻塞了,则服务器会一直等待
F3,如果F3不来,后面的包都不会成功被接收。反向队列FF1也是同样的道理。
当然,虽然 http/2 的二进制分帧没有完全解决队头阻塞的问题,但降低了其发生的可能性,为什么这么说?下面具体分析下:
原因1:服务器对请求1处理的慢;
原因2:服务器对请求1处理的很及时,但网络传输慢了;
对于原因2,如果刚好请求1的第一帧又在队头,则即使二进制分帧也解决不了队头阻塞的问题;但对于原因1,请求2,请求3的响应分帧之后,是先
于请求1的响应发出去的,那么请求2和请求3的响应就不会被请求1阻塞,从而避免了队头阻塞问题。
如果要彻底解决"队头阻塞"问题怎么解决呢?不用tcp。这正是google公司的 QUIC 协议要做的事情。
5.3.3 头部压缩
除了二进制分帧,http/2 另外一个提示效率的地方是头部压缩。在http 1.1 里面,对于报文的报文体,已经有相应的压缩,尤其对于图片,本来
就是压缩过的;但对于报文的头部,一直没做压缩。
随着互利网的发展,应用场景越来越复杂,很多时候报文的头部也变得很大,这时候对头部进行压缩就很有必要了。为此,http/2 专门设计了一个
HPACK 协议和对应的算法。
为解决http 1.1 的效率问题,除引入的这2个关键特性之外,http/2 还有一些其他特性,比如 服务器推送,流重置等。
5.4 SSL/TLS
5.4.1 背景
SSL(Secure Sockets Layer)安全套接层,TLS(Transport Layer Security)传输层安全协议。
TLS 1.0 相当于 SSL 3.1;
TLS 1.1 相当于 SSL 3.2;
TLS 1.2 相当于 SSL 3.3;
SSL/TLS 处在 tcp 层面的上层,它不仅可以支撑 http 协议,还可以支撑 ftp,imap 等其他各种应用层协议。
5.4.2 对称加密的问题
客户端和服务端用同一个密钥。
这种加密方式存在2个问题:
1.密钥如何传输?密钥A的传输也需要另外一个密钥B,密钥B的传输也需要密钥C... 如此循环,无解。
2.如何存储密钥?对于浏览器网页来说,都是明文的,肯定存储不了;对于Android/iOS客户端,即使能把密钥存储在安装包的某个位置,
也很容易被破解。
当然,这2个问题其实是一个问题。因为如果把密钥的传输问题解决了,就可以在建立连接之后获取密钥,然后只存储在内存中,因为当连接断开后,
密钥就在内存中销毁了,也就解决了存储问题。
5.4.3 双向非对称加密
客户端为自己准备一对公私钥(PubA,PriA),服务器为自己准备一对公私钥(PubB,PriB)。公私钥有2个特性:公钥PubA是通过私钥PriA计算出来
的,但反过来不行,不能根据PubA推算出PriA。
客户端和服务端把自己的公钥公开出去,自己保留私钥。
当客户端给服务端发送信息时,就用自己的私钥PriA签名,再用服务端的公钥PubB加密。所谓的"签名",相当于自己盖了章,或者说签了字,证明
这个信息是客户端自己发的,客户端不能抵赖;用服务器的公钥PubB加密,意味着只有服务器B可以用自己的私钥PriB解密。即使这个信息被C截获了,
C没有B的私钥,也无法解密这个信息。
服务器收到这个信息后,先用自己的私钥PriB解密,再用客户端的公钥验签(证明信息是客户端发出的)。反向过程同理:服务器发送给客户端信息时,
先用自己的私钥PriB签名,然后用PubA加密,客户端收到服务器的信息后,先用自己的私钥PriA解密,再用PubB验签。
在这个过程中,存在着签名和验签与加密和解密两个过程:
1.签名和验签
私钥签名,公钥验签,目的是防篡改。如果第三方截取到信息之后篡改,则接收方验签肯定过不了,同时也是防抵赖,既然没有人可以
篡改,只可能是发送方自己发出的。
2.加密和解密
公钥加密,私钥解密。目的是防止信息被第三方拦截和偷听。第三方即使能获取信息,但没有私钥,也解密不了。
在双向非对称加密中,客户端需要提前知道服务器的公钥,服务器需要知道客户端的公钥,和对称加密一样,同样面临公钥如何传输的问题。
5.4.4 单向非对称加密
在互联网上,网站是完全对外公开的,网站的提供者没有办法验证每个客户端的合法性,只有客户端可以验证网站的合法性,是否钓鱼网站等。
在这种情况下,客户端并不需要公钥和私钥对,只有服务器有一对公钥和私钥。服务器把公钥给到客户端,客户端给服务器发送信息时,用公钥加密,
然后服务器用私钥解密。反过来,服务器给客户端发送的消息,采用明文发送。
当然,对于安全场景要求很高的地方,比如银行的个人网银,不仅客户端要验证服务端的合法性,服务器也要验证每个访问的客户端的合法性。对于
这种场景,往往会给用户发一个U盘,里面装的就是客户端一方的公钥和私钥,用的就是上面的双向非对称加密。
对于单向非对称加密,只有客户端到服务器的单向传输是加密的,服务器返回的是明文的。
假设PubB的传输过程是安全的,客户端知道了服务器的公钥。客户端就可以利用加密通道给服务器发送一个对称加密的密钥。
客户端对服务器说,"hi,我们的对称密钥是xxx,接下来用xxx密钥通信。"这句话是通过PubB加密的,所以只有服务器用自己的PriB解密。然后
服务器回复了一句"好的,我知道了。"虽然是明文的,但没有任何密钥信息在里面,所以才有明文也没有关系。接下来,双方就可以基于对称加密进行通信
了。这个密钥在内存里,不会落地存储,所以也不会有盗取问题,而这就是 ssl/tls 的原型。
5.4.5 中间人攻击
上面的分析我们知道,我们并不需要双向的非对称加密,而用单向的非对称加密就能达到传输的目的了。
但无论是单向还是双向,都存在着公钥如何安全传输的问题。
中间人攻击的过程:
1.客户端本来要把自己的公钥发送给服务器:"hi,我是客户端1,我的公钥是PubA"
2.被中间人C劫持后,C用自己的公钥替换客户端的公钥,然后发送给服务器,"hi,我是客户端1,我的公钥是PubC"
3.反过来,服务器本来是要把自己的公钥发送给客户端的:"hi,我是服务器,我的公钥是PubB"
4.被C劫持后,C用自己的公钥替换服务器的公钥,然后发送给客户端:"hi,我是服务器,我的公钥是PubC"
最后的结果是:客户端和服务端都以为是在和对方通信,但其实他们都是在和中间人通信。接下来,客户端发给服务端的信息,会用PubC假面,
C当然可以解密这个信息;同样,服务器发送给客户端的信息也会被PubC加密,C也可以解密。
这个问题为什么会出现?因为公钥的传输是不安全的。
5.4.6 数字证书与证书认证中心
引入一个中间机构CA。当服务器把公钥发给客户端时,不是直接发送公钥,而是发送公钥对应的证书。
从组织上来说,CA类似于现实中的"公证处",从技术上来说,就是一个服务器。服务器先把自己的公钥PubB发给CA,CA给服务器颁发一个数字证书,
这个证书相当于服务器的身份。之后,服务器把证书给客户端,客户端可以验证证书是否为服务器下发的。
反过来同理,客户端用自己的公钥PubA给CA换取一个证书,相当于客户端的身份证,客户端把这个证书给了服务器,服务器就能验证证书是否为客户端
下发的。
当然,对于通常的互联网来说,只需要客户端验证服务器,不需要服务器验证客户端。
具体过程:
CA有一对公私钥,私钥只有CA自己知道,公钥在网上,谁都可以知道。服务器把 个人信息+服务器的公钥发给CA,CA用自己的私钥为服务器生成
一个数字证书。通俗地讲,服务器把自己的公钥发给CA,让CA改个公章,之后别人就不能再伪造公钥了。如果被中间人伪造了,客户端拿着CA的公钥
去验证这个证书,验证将无法通过。
5.4.7 根证书与CA信任链
CA 面临和客户端,服务器同样的问题:客户端和服务器需要证明公钥的确是自己发出去的,不是被伪造的。CA同样需要证明,自己的公钥是自己发
出去的,而不是被伪造的。
答案是给CA颁发证书,CA的证书由谁来颁发的呢?CA的上一级CA。最终形成证书信用链。
1.证书信任链的验证过程
客户端需要验证服务器的合法性,需要拿着服务器的证书C3,到CA2去验证(C3是CA2颁发的,验证方法是拿着CA2的公钥,去验证证书C3的
有效性)。
客户端验证CA2的合法性,需要拿着CA2的证书C2,到CA1去验证(C2是CA1颁发的)。
客户端要验证CA1的合法性,需要拿着CA1的证书C1,到CA0去验证(C1是CA0颁发的)。
而CA0呢,只能无条件信任。怎么做到无条件信任呢?Root CA 机构都是一些世界上公认的机构,在用户的操作系统,浏览器发布的时候,
里面就已经嵌入这些机构的Root 证书。信任这个操作系统,信任这个浏览器,也就信任了这个 Root 证书。
2.证书信任链的颁发过程
颁发的过程与验证的过程刚好是逆向的,上级CA给下级CA颁发证书。从根CA(CA0)开始,CA0给CA1颁发证书,CA1给CA2颁发证书,CA2给
服务器颁发证书。
最终,证书成为网络上每个通信实体的"身份证",在网络上传输的都是证书,而不再是原始的那个密钥。把这套体系标准化后,就是在网络
安全领域常见的一个词,PKI。
5.4.8 SSL/TLS协议:四次握手
在建立tcp连接之后,数据发送之前,ssl/tls 协议通过4次握手,2个来回,协商出客户端与服务器之间的对称加密密钥。第一个来回,是公钥
的传输与验证过程(通过数字证书);第二个来回基于第一个来回的公钥,协商出对称加密的密钥。接下来,就是正常的应用层数据包的发送操作了。
当然,为了协商出对称加密密钥,ssl/tls 协议引入了几个随机数。
5.5 HTTPS
https = http + ssl/tls 。整个https的传输过程大致可以分为三个阶段:
1.tcp连接的建立
2.ssl/tls 四次握手协商出对称加密的密钥
3.基于密钥,在tcp连接上对所有的http request/response 进行加密和解密。
其中阶段1 和阶段2 只在连接建立时,做1次,之后只要不关闭,每个请求都只需要经过阶段3,因此相比http,性能没太大损失。
最后分析下http2和https的关系:http2 主要是解决性能问题,https 是解决安全问题。从理论上来说,两者没有必然的联系,http2 可以不依赖于
https;反过来也是。
http 1.1
http/2 (SPDY) (可选)
SSL/TLS (可选)
TCP
5.6 TCP/UDP
5.6.1 可靠与不可靠
不可靠:
1.丢包
2.时序乱了
可靠:
1.数据包不丢
2.数据包不重
3.时序不乱
tcp 核心:
1.解决不丢问题:ACK + 重发
丢包是一定的,如何确保不丢包?只有一个办法:重发。收到一个包,就确认。如果超时没收到,就重发。
2.解决不重的问题
如何判重呢?就是顺序ACK。服务器回复给客户端 ACK=6,意思是所有小于或者等于6的数据包全部收到了,之后凡是再收到这个范围的
数据包,就判定为是重复的包,丢弃。
3.解决乱序的问题
服务器虽然收到的数据包是并发的,但数据包的ACK是按照序号从小到大逐一确认的,所以数据包的时序是有保证的。
最终通过 消息顺序编号+客户端重发+服务器顺序ACK,实现了客户端到服务器的数据包的不重,不漏,时序不乱。
5.6.2 TCP的“假”连接(状态机)
在物理层面,在客户端和服务器之间不存在一条可靠的"物理管道"。只是在逻辑层面,让tcp之上的应用层就像在客户端和服务器之间架起了一个"可靠
的连接"。但实际上这个连接是"假的",是通过数据包的 不重,不漏,时序不乱的机制,给上层应用一个假象。
每条连接用(客户端IP,客户端Port,服务器IP,服务器Port)4元组唯一确定,在代码中是一个个socket。其中有一个关键问题要解决,既然"连接
是假的",在物理层面不存在,但在逻辑层面是存在的,每条连接都要经历建立阶段,正常数据传输阶段,关闭阶段,要完整的维护在这三个阶段过程中连接
的每种可能的状态。
5.6.3 三次握手(网络2将军问题)
客户端的状态转移: closed => syn_send => established;
服务端的状态转移: closed => listen => syn_rcvd => established;
无论是两次握手,三次握手,还是四次握手,都绕不开网络的2将军问题。那为什么是三次?
因为三次握手刚好可以保证客户端和服务器对自己发送,接收能力做了一次确认。
5.6.4 四次挥手
客户端的状态转移:established => fin_wait_1 => fin_wait_2 => time_wait => closed;
服务端的状态转移:established => close_wait => last_ack => closed
为什么是四次挥手?
因为tcp是全双工的,有可能处于 half-close 状态。
为什么要等待 2MSL time_wait:
1.所谓的"连接"是假的,物理层面没有连接。这意味着当双方都进入close后,仍可能有数据包还在网络上"闲逛",如果此时收到了这些闲逛
的数据包,丢到即可,但问题是连接可能重开。
一个连接由(客户端ip,客户端port,服务端ip,服务端port)4个元组组成唯一标识,连接关闭后重新打开,应该是一个新的连接,但用
4元组无法区分新连接和老连接。这会导致,之前闲逛的数据包在新连接打开后被当做新的数据包,这样一来,老连接上的数据包会"串"到新
连接上。
怎么解决这个问题?
在整个tcp/ip网络上,定义了一个值叫做 MSL,任何一个ip数据包在网络上逗留的最长时间是msl,这个值默认是120s。意味着一个数据包
最多在msl时间内,从源点到目的地,如果超出了这个时间,中间的路由节点就会把该数据包丢弃。
有了这个限定后,一个连接保持在 time_wait 状态,再等待 2msl 的时间进入 close 状态,就会完全避免旧的连接数存在闲逛的数据
包串到新连接上。为什么是2msl呢?
2.因为网络2将军问题。第4次发送的包,服务器是否收到是不确定的。服务器采用的方法是在无法收到第4次的情况下,重新发送第3次的数据
包,客户端重新收到第3次数据包,再次发送第4次数据包。第4次包的传输时间+服务器重新发送第3次包的时间,最长2msl,所以要客户端在
time_wait 等待 2msl。
还有一个问题:客户端处于time_wait状态,要等到2msl进入closed;但是服务器收到第4次 ack后,立即进入了closed状态。为什么
服务器不等待呢?
原因是没有必要。任何一个连接都是4元组,同时关闭了客户端和服务器,客户端处于time_wait状态,意味着这个连接要等待2msl时间
之后才能重新启用,服务器想立即启用也无法实现。
通过分析会发现,一个连接并不是想关闭就能关闭的,关闭之后还要等待2msl时间才能重新打开,这就造成了一个问题,如果频繁的创建连接,
最后可能会导致大量的连接处于 time_wait 状态,最终耗光了所有资源。如何避免?
1.不要让服务器主动关闭连接,这样服务器就不会处于 time_wait 状态
2.客户端做连接池,复用连接,而不要频繁的创建和关闭,这其实也是http 1.1 和 http/2 的思路。
5.7 QUIC
QUIC(Quick UDP Internet Connection) 是由google 公司提出的基于udp协议的多路并发传输协议。
只要使用tcp,就没有办法完全解决队头阻塞问题,因为tcp是先发送后接收的,而udp没有这个限制。正因为如此,google提出了QUIC协议,想基于
udp构建上层的应用网络。
如下为quic协议在网络分层中的位置,首先,它取代了 tcp的部分功能(数据包的不丢);然后它实现了 ssl/tls 的所有功能;最后它还取代了http/2
的部分功能(多路复用)。
http 1.1 http 1.1
http/2 http/2
ssl/tls QUIC
TCP UDP
IP
5.7.1 不丢包(Raid5算法和Raid6算法)
虽然针对udp会丢包,tcp可以通过 "ack+重传"来解决,但很多时候重传的效率不够高。
除了重传,是否还有其他办法解决丢包?这就要用到磁盘存储领域经典的 Raid5和Raid6 算法。每发送5个包,就发送一个冗余包。冗余包是对5个
数据包做异或运算得到的。这样一来,服务器收到6个包,如果5个当中,有一个丢失了,可以通过其他几个包计算出来。这样就好比做简单的数学运算:
A+B+C+D+E=R,假设数据包D丢失了,可以通过 D=R-A-B-C-E 计算得到。
但这种丢包的恢复办法有一个限制条件:每5个包中只能丢失1个,如果把它改造成每发送10个数据包,再生成一个冗余包,就是每10个当中只能
丢失1个。
在raid5的基础上,把可靠性向上提一个级别,生成两个冗余块,就是raid6。每5个块生成2个冗余块,这样就允许每5个丢失2个。数据恢复的过程,
相当于解一个二元一次方程:
A+B+C+D+E=R1
A-B+C-D+E=R2
对于QUIC 来说,它采用的是 RAID5,目前是发送10个包,冗余1个包。允许丢1个包,如果丢失了2个。那就要重传了。通过设置合理的冗余比,
QUIC 减少了数据重传的概率。
5.7.2 更少的RTT
要建立一个 https 连接需要7次握手,tcp的3次握手加上 ssl/tls 的4次握手,是3个RTT。而造成网络延迟的原因,一个是带宽,另外一个是
RTT。因为RTT有个特点是同步阻塞,在数据包发送出去之后,必须要等待对方的确认回来,接着再发送下一个。
所以,无论是tcp,还是ssl/tls,都尝试优化协议,减少rtt的次数。基于QUIC协议,可以把前面的7次握手(3个RTT),减少为0次。
5.7.3 连接迁移
tcp 的连接是4元组组成的,这在pc端上问题不大。而在移动端上,客户端是wifi或者4g,客户端的ip一直是变化的,意味着频繁的建立和关闭
连接。有没有可能,在客户端的ip和端口的浮动下,连接仍然是可以维持的呢?
tcp的连接本来就是"假"的,一个逻辑上的概念而已,QUIC协议也可以创造一个逻辑上的连接。具体做法是,不再以4元组来标识连接,而是让
客户端生成一个64位的数字来标识连接,虽然客户端的ip和port在漂移,但64位的数字没有变化,这条连接就会一直存在。这样,对于应用层来说,
就感觉连接一直存在。