HTTP 1.0

HTTP 1.0的问题

HTTP协议的基本特点是“一来一回”。这样的协议有两个问题:

  1. 性能问题。连接的建立和关闭都是耗时操作,短连接的性能很差。
  2. 服务器推送问题。服务器无法在客户端没有请求的情况下主动向客户端推送消息。
Keep-Alive机制与Content-Length属性

为了解决第一个问题,HTTP 1.0设计了一个Keep-Alive机制来实现TCP连接的复用。具体来说就是客户端在发送请求时在头部加上一个Connection:Keep-Alive字段。服务器会有一个Keep-Alive timeout参数,如果客户端没有主动关闭连接,超时之后连接会被关闭。

在服务器不主动关闭连接的情况下,客户端怎么知道连接处理结束了呢?答案是在HTTP Response头部返回了一个Content-Length:xxx的字段,这个字段告诉客户端响应的body共有多少个字节。

HTTP 1.1

连接复用与Chunk机制

HTTP 1.1把连接复用变成了一个默认属性。除非在请求头部显式加上Connection:Close属性,服务器才会在处理完后主动关闭连接。

对于Content-Length字段有个问题,如果服务器返回的数据是动态语言生成的内容,计算返回内容长度则比较困难,也会比较耗时。为此HTTP 1.1引入了Chunk机制(HTTP Streaming)。具体来说,就是在响应的头部加上Transfer-Encoding:chunked属性,目的是告诉客户端,响应的body被分成了一块块的,块与块之间有间隔符,所有块的结尾也有个特殊标记。这样客户端就能不通过Content-Length字段判断出响应的末尾。

Pipeline与Head-of-line Blocking问题

有了连接复用之后,在同一个连接上,请求是串行的,收到响应之后才能发送下一个请求。为此,HTTP 1.1引入了Pipeline机制。在同一个连接上面,可以在一个请求发出去之后,响应没有回来之前,就可以发送下一个、再下一个请求,这样就提高了连接处理请求的效率。

但是Pipeline有个致命问题,就是Head-of-line Blocking,翻译成中文就是“队头阻塞”。具体就是,虽然服务器可以并发处理很多请求,但是客户端接收响应的顺序必须和请求的顺序一样,如此才能把响应和请求成功配对,跟队列一样,先进先出。一旦请求队列前面的请求发生延迟,客户端迟迟收不到响应,则该请求后面的请求的响应都会被阻塞,即使他们早已处理完毕。

也正因为如此,为了避免Pipeline带来的副作用,很多浏览器默认把Pipeline关闭了。

HTTP 2出现之前的性能提升方法

一方面,Pipeline不能用,在同一个连接上面,请求是串行的;另一方面,对于同一个域名,浏览器限制只能开6 ~ 8个连接。但一个网页可能要发几十个HTTP请求,那么如何提高并发度,或者说提高网页渲染的性能呢?

  1. Spriting技术。这种技术主要针对小图片,可以在服务器把小图片拼成一张大图,到了浏览器再通过JS或CSS从大图中截取一小块显示。
  2. 内联。内联是针对小图片的另一种技术,将图片的原始数据嵌入在CSS文件里面。
  3. JS拼接。把大量小的JS文件合并成一个文件并压缩,让浏览器在一个请求里面下载完。
  4. 请求的分片技术。可以多做几个域名,绕开浏览器的限制。尤其是现在CDN用得很广泛,网站的静态资源可能都在CDN上面,可以做一批CDN的域名,这样浏览器就可以为每个域名都建立6 ~ 8个连接,从而提高页面加载的并发度。
“一来多回”问题

服务器主动推送的解决方法:

  1. 客户端定期轮询。比如客户端每隔5s向服务器发送一个HTTP请求,服务器如有有新消息就返回。这种方式既低效,又增加服务器压力,现在已很少采用。
  2. FlashSocket/WebSocket。直接基于TCP,但也有一定局限性。
  3. HTTP长轮询。客户端发送一个HTTP请求,如果服务器有新消息就返回;如果没有,保持该连接,客户端一直等待。然后过一个约定的时间后,服务器还没有新消息,就返回一个约定好的消息。客户端接收该消息后关闭连接,再发起一个新的连接,重复此过程。这相当于变相用HTTP实现了TCP的长连接效果,也是目前Web最常用的服务器端推送方法。
  4. HTTP Streaming。服务器端利用Transfer-Encoding:chunked机制,发送一个没有结束标记的chunk流。
断点续传

HTTP 1.1还有一个很实用的特性是断点续传。当客户端从服务器下载文件时,如果下载到一半连接中断了,再新建连接后,客户端可以从上次断开的地方继续下载。具体实现也很简单,客户端一边下载一边记录下载的数据量大小,一旦连接中断了,重新建立连接之后,在请求的头部加上Range:first offset - last offset字段,指定从某个offset下载到某个offset,服务器就可以只返回该部分的数据。

HTTP 2

与HTTP 1.1的兼容
二进制分帧
头部压缩

延伸阅读
HTTP 协议入门
都9102年了,还问GET和POST的区别

SSL/TLS

SSL/TLS处在TCP层的上面,它不仅可以支撑HTTP协议,也能支撑FTP/IMAP等其他各种应用层的协议。

对称加密的问题

对称加密的想法很简单,客户端和服务器知道同一个密钥,客户端给服务器发消息时用此密钥加密,服务器用此密钥解密,反之亦然。

图片.png

这种加密方式有个问题就是如何传输密钥,密钥A的传输需要另外一个密钥B,密钥B的传输又需要密钥C···如此循环无解。

双向非对称加密

如图,客户端和服务器都为自己准备一对公私钥。公私钥有个关键特性:公钥是通过私钥计算出来的,但反过来不行。

图片.png

客户端和服务器把自己的公钥公开出去,自己保留私钥。当客户端给服务器发送消息时,就用自己的私钥签名,再用服务器的公钥加密。所谓的签名,相当于自己盖了章,证明这个信息是客户端发送的,客户端不能抵赖;用服务器的公钥加密,意味着只有服务器可以用自己的私钥解密。服务器收到消息后,先用自己的私钥解密,在用客户端的公钥验签(证明信息是客户端发出的)。反向过程同理。

在这个过程中,存在签名和验签与加密和解密两个过程:

  1. 签名和验签。私钥签名,公钥验签,目的是防篡改。同时也防抵赖,既然没有人可以篡改,只可能是发送方自己发出的。
  2. 加密和解密。公钥加密,私钥解密,目的是防窃听。第三方即便能截获到信息,但如果没有私钥,也解密不了。

在双向非对称加密中,客户端和服务器都需要提前知道对方的公钥,同样面临着公钥如何传输的问题。

单向非对称加密

在互联网上,网站对外是完全公开的,网站的提供者没有办法去验证每个客户端的合法性,只有客户端可以验证网站的合法性。比如用户访问百度的网站,需要验证所访问的是不是真的百度网站,防止被钓鱼。

在这种情况下,客户端并不需要公钥和私钥对,只有服务器需要。如图,服务器把公钥给到客户端,客户端给服务器发送消息时,用公钥加密,然后服务器用私钥解密。反过来,服务器给客户端发送的消息采用明文发送。

图片.png

当然,对于安全性要求很高的场景,比如银行的个人网银,不仅客户端要验证服务器的合法性,服务器也要验证每个访问的客户端的合法性。对于这种场景,往往会给用户发一个U盘,里面装的就是客户端一方的公钥和私钥对,用的是双向非对称加密。

对于单向非对称加密,只有客户端到服务器的单向传输是加密的,服务器的返回是明文方式,怎么保证安全呢?

假设PubB的传输过程是安全的,客户端知道了服务器的公钥。客户端就可以利用加密通道向服务器发送一个对称加密的密钥,如图。

图片.png

接下来,双方就可以基于对称加密的密钥进行通信了,这个密钥存储在内存里面,所以也不会存在被窃取的问题,这就是SSL/TLS的原型。

中间人攻击

通过上面分析可以发现,我们不需要双向的非对称加密,而用单向的非对称加密就可以达到传输的目的。

但无论双向还是单向,都存在着公钥如何安全传输的问题。下面以一个典型的“中间人攻击”的案例为例,来看一下这个问题是怎样被解决的。

本来客户端要把自己的公钥发给服务器,但是被中间人劫持了,中间人用自己的公钥替换客户端的公钥,然后发给服务器。服务器发送给客户端的公钥也被同样的方式劫持和篡改。结果就是客户端和服务器都在和中间人通信。

这个问题为什么会出现呢?是因为公钥的传输过程是不安全的。客户端和服务器是在网络上,互相又没见过对方,怎么知道收到的公钥就是对方发出,而不是被中间人篡改过的呢?

这里就需要想个办法来证明服务器收到的公钥的确就是客户端发出的,反过来也是一样。这就是数字证书。

数字证书与证书认证中心

在客户端和服务器之间,引入一个中间机构CA。当服务器把公钥发给客户端时,不是直接发送公钥,而是发送公钥对应的证书。

从组织上来讲,CA类似现实中的公证处,从技术来讲就是一个服务器。服务器先把自己的公钥发给CA,CA给服务器颁发一个数字证书,这个证书相当于服务器的身份证。之后,服务器把证书发给客户端,客户端可以验证证书是否为服务器下发的。反过来对客户端而言同理。当然,对于通常的互联网应用,只需要客户端验证服务器,不需要服务器验证客户端。

具体的验证过程如下:CA有一对公钥和私钥对,私钥只有CA知道,公钥在网络上,谁都可以知道。服务器把个人信息和服务器的公钥发给CA,CA用自己的私钥为服务器生成一个数字证书。通俗地讲,服务器把自己的公钥发给CA,让CA加盖公章,之后别人就不能再伪造公钥了。如果被中间人伪造了,客户端拿着CA的公钥去验证这个证书,验证将无法通过。

根证书与CA信任链

但又会出现问题:如果CA是个假的怎么办?CA的公钥如何被安全地在网络上传输?CA也需要证明,自己的公钥是由自己发出去的,不是被伪造的。
答案是给CA颁发证书,CA的证书由CA的上一级CA颁发,最终形成一条证书信任链。

证书信任链的验证过程

客户端要验证服务器的合法性,需要拿着服务器的证书C3,到CA2处去验证(C3是CA2颁发的,验证方法是拿着CA2的公钥,去验证C3的有效性);客户端要验证CA2的合法性,需要拿着CA2的证书C2,到CA1处去验证;客户端要验证CA1的合法性,需要拿着CA1的证书C1,到CA0处去验证。而CA0只能无条件信任。这里的CA0指的是Root CA机构,都是一些世界上公认的机构,在用户的操作系统、浏览器发布的时候,里面就已经嵌入了这些机构的Root证书。你信任这个操作系统,信任这个浏览器,也就信任了这些Root证书。

颁发过程与验证过程刚好是逆向的,最终,证书就成为了网络上每个通信实体的身份证,在网络上传输的都是证书,而不再是原始的公钥。

SSL/TLS协议:四次握手

如图所示:

图片.png

在建立TCP连接之后,数据发送之前,SSL/TLS协议通过四次握手、两个来回,协商出客户端和服务器之间的对称加密密钥。当然,为了携赏处对称加密的密钥,SSL/TLS协议引入了几个随机数,具体细节不再展开。

HTTPS

HTTPS = HTTP + SSL/TLS。整个HTTPS的传输过程大致可以分成三个阶段,如图:

图片.png

其中第一阶段和第二阶段只在连接建立的时候执行一次,之后只要连接不关闭,每个请求只经过第三阶段,因此相比HTTP,性能没有太大损失。
最后,分析一下HTTP 2和HTTPS的关系:HTTP 2主要是解决性能问题,HTTPS主要解决安全问题。从理论上讲,两者没有必然的关系,HTTP 2可以不依赖于HTTPS,反之亦然。两者都是HTTP 1.1和TCP之间的中间层。
但在实践层面,目前主流的浏览器都要求如果要支持HTTP 2则必须先支持HTTPS,这也是因为整个互联网都在推动HTTPS的普及。

延伸阅读SSL/TLS协议运行机制的概述

TCP/UDP

可靠与不可靠

众所周知,UDP是不可靠的,而TCP是可靠的。什么是不可靠呢?

  • 数据包丢失
  • 数据包重复
  • 数据包时序错乱

TCP是如何做到可靠的:

1.解决数据包丢失问题

网络丢包是一定会出现的,解决办法只有一个,重发。服务器每次收到一个包,就要对客户端进行确认,反馈给客户端已经收到了数据包。如果客户端在约定时间内没有收到ACK,则重发数据。

服务器还会使用滑动窗口机制来更有效地管理ACK号,并且在发送ACK号和窗口更新时,会等待一段时间,在这个过程中如果有其他的发送ACK号和更新窗口的操作,可以把这些通知操作合并到一起去发送。这样可以提高传输的效率。

2.解决数据包重复问题

解决这个问题的方法很简单,如果服务器给客户端回复ACK = 4381,意思是小于4381(字节)的数据包都已经收到了,之后凡是再接收到这个范围内的数据包,直接丢弃即可。

3.解决数据包时序错乱问题

假设服务器要接收数据包1、2、3,已经接收了2和3,1却迟迟没有收到。这个时候服务器会把2、3暂时存放在接收缓冲区中,直到数据包1的到来,再给客户端回复ACK号;如果数据包接收不到,服务器的ACK进度会一直停在那,直到客户端超时,会把所有的三个包全部重新发送,服务器接收到了数据包1就会回复ACK号,同时2、3被丢弃。

总的来说,TCP是通过网络包顺序编号 + 重发 + 服务器顺序ACK,实现了所谓的可靠性的。

三次握手

下图展示了TCP建立连接的三次握手过程,以及对应的客户端和服务器的状态。

图片.png

为什么是三次握手呢?

因为三次握手恰好可以保证客户端和服务器对自己的发送、接收能力做了一次确认。第一次,客户端给服务器发了seq = x,无法知道对方是否收到;第二次,服务器回复了seq = y, ACK = x + 1,这时客户端知道自己的发送能力和接收能力都没有问题,但服务器只知道自己的接收能力没有问题;第三次,客户端发送了ACK = y + 1,服务器收到后知道自己的发送能力也没有问题。

四次挥手

图片.png

为什么是四次挥手呢?因为TCP是全双工的,可以处于半关闭状态。如果客户端和服务器只发生了第一次和第二次挥手,意味着该连接处于半关闭状态,客户端通往服务器的点通道关闭了,但服务器通往客户端的通道还未关闭。等到第三次和第四次挥手,连接才会处于完全的CLOSE状态。
这里有一个问题,关闭连接的时候,为何不直接进入CLOSE状态,而要做一个TIME_WAIT状态,非要等待一段时间才能进入CLOSE状态呢?

如果双方都直接进入CLOSE状态,但此时仍可能有数据包在网络上“闲逛”。连接在关闭之后可能会重开,此时如果收到了这些闲逛的数据包,会产生问题。

一个连接是由(客户端IP、客户端Port、服务器IP、服务器Port)四元组唯一标识的,连接关闭之后再重开,应该是一个新的连接,但四元组无法区分是新连接还是旧连接。这会导致,之前闲逛的数据包会被当成新的数据包,也就是旧连接上的数据包会“串”到新连接上面。

在整个TCP/IP网络上,定义了一个值叫做MSL(Maximum Segmeent Lifetime),任何一个数据包在网络上逗留的最长时间是MSL,默认为120s。一个数据包必须最多在MSL时间内从源点传输到目的地,如果超出了这个时间,中间的路由节点就会把该数据包丢弃。

有了这个限定之后,一个连接保持在TIME_WAIT状态,再等待2 * MSL的时间进入CLOSE状态,就能避免旧连接上面闲逛的数据包串到新连接上。那为什么是2倍的MSL的时间呢?原因如下。

第四次挥手时客户端发送的数据包,服务器是否收到是不确定的。服务器采取的方法是无法收到的情况下重发第三次挥手的数据包,客户端重新收到后再次发送第四次的数据包。这里对应着是两个数据包的来回,最长时间是2倍的MSL,所以要让客户端在TIME_WAIT状态等待2 * MSL的时间。
还有一个问题,客户端处于TIME_WAIT状态,要等待2 * MSL的时间进入CLOSED;但服务器收到第四次的ACK后,立即进入了CLOSE状态。为什么不让服务器也进入TIME_WAIT状态呢?原因是没有必要。任何一个连接都是一个四元组,同时关联了客户端和服务器,客户端处于TIME_WAIT状态后,意味着这个连接至少要等到2倍的MSL时间之后才能重新启用,服务器端即使想使用也没法实现。

如果频繁地创建连接然后关闭,最后可能导致大量的连接处于TIME_WAIT状态,耗光所有的连接资源。为了避免出现这种问题,可以采取如下措施:

  • 不要让服务器主动关闭连接。这样服务器的连接就不会处于TIME_WAIT状态。
  • 客户端做连接池,复用连接。这其实也是HTTP 1.1和HTTP 2采用的思路。

延伸阅读
TCP 协议简介
跟着动画来学习TCP三次握手和四次挥手
WebSocket:5分钟从入门到精通

QUIC


与昊
225 声望636 粉丝

IT民工,主要从事web方向,喜欢研究技术和投资之道