概述
网络协议有很多种,但对互联网来说,用的最多的就是HTTP协议。HTTP主要有1.0、1.1、2三个版本,在HTTP之上有HTTPS。 1996年,HTTP1.0协议规范RFC 1945发布; 1999年,HTTP1.1协议规范RFC 2616发布。 2015年,HTTP/2协议规范RFC 7540/7541发布。 HTTP/2还比较新,目前远没有达到普及的程度。在过去的近20年间,主流的协议一直是http1.1。接下来将对HTTP协议的发展脉络进行梳理。
HTTP1.0
HTTP1.0的问题
HTTP协议的基本特点是“一来一回”。什么意思呢?客户端发起一个TCP连接,在连接上面发一个HTTP Request到服务器,服务器返回一个HTTP Response,然后连接关闭。每来一个请求,就要开一个连接,请求完了,连接关闭。 这样的协议有两个问题:
- 性能问题。连接的建立、关闭都是耗时操作。对应一个网页来说除了网页本身的HTML请求,页面里面的JS、CSS、img资源,都是一个个的HTTP请求。现在的互联网上的页面,一个页面上有几十个资源文件是很常见的事。每来一个请求就开一个TCP连接时非常耗时的。虽然可以同时开多个连接,并发的发送请求,但连接数毕竟是有限的。
- 服务器推送问题。不支持“一来多回”,服务器无法在客户端没有请求的情况下主动向客户端推送消息。但很多的应用恰恰都需要服务器在某些事情完成后主动通知客户端。
针对这两个问题,来看HTTP在发展过程中是怎么解决的。
Keep-Alive机制与Content-Length属性
为了解决上面提及的第一个问题,HTTP1.0设计了一个Keep-Alive机制来实现TCP连接的复用。具体来说,就是客户端在HTTP请求的头部加上一个字段Connection:Keep-Alive。服务器收到带有这样字段的请求,在处理完请求之后不会关闭连接,同时在HTTP的Response里面也和加上该字段,然后等待客户端在该连接上发送下一个请求。 当然,这会给服务器带来一个问题:连接数有限。如果每个连接都不关闭的话,一段时间之后,服务器的连接数就耗光了。因此,服务器会有一个Keep-Alive timeout参数,过一段时间之后,如果该链接上上没有新的请求进来,则连接就会关闭。 连接复用之后又产生了一个新问题:以前一个连接就只发送一个请求,返回一个响应,服务器处理完毕,把连接关闭,这个时候客户端就知道连接的请求处理结束了。但现在,即使一个请求处理完了,连接也不关闭,那么客户端怎么知道连接处理结束了呢?或者说,客户端怎么知道接收回来的数据包是完整的呢? 答案是在HTTP Response的头部,返回一个Content-Length:xxx的字段,这个字段可以告诉客户端HTTP Response的Body共有多少个字节,客户端接收到这么多字节之后,就知道响应成功接收完毕。
HTTP 1.1
连接复用与Chunk机制
从上面的分析可以看出,连接复用非常有必要,所以到了HTTP 1.1之后,就把连接复用变成了一个默认属性。即使不加Connection:Keep-Alive属性,服务器也会在请求处理完毕之后不关闭连接。除非在请求头部显示地加上Connection:Close属性,服务器才会在请求处理完毕之后主动关闭连接。 在HTTP 1.0里面可以利用Content-Length字段,让客户端判断一个请求的响应成功是否接收完毕。但Content-Length有个问题,如果服务器返回的数据时动态语言生成的内容,则要计算Content-Length,这点对服务器来说比较困难。即使能够计算,也需要服务器在内存中渲染出整个页面,然后计算长度,非常耗时。 为此,在HTTP1.1中引用了Chunk机制(Http Streaming)。具体来说,就是在响应的头部加上Transfer-Encoding:chunked属性,其目的是告诉客户端,响应的Body是分成一块块的,块与块之间有分隔符,所有块的结尾也有一个特殊标记。这样,即使没有Content-Length字段,也能方便客户判断出响应的末尾。 下面显示了一个简单的具体Chunk机制的HTTP响应,头部没有Content-Length字段,而是Transfer-Encoding:chunked字段。该响应包含4个chunk,数字25(16机制)表示第一个chunk的字节数,1C(16进制)表示第二个chunk的字节数…最后的数字0表示整个响应的末尾。
HTTP/1.1 200 OK
Content-Type:text/plain
Transfer-Encoding:chunked
25
This is the data in the first chunk
1C
and this is the second one
3
con
8
sequence
0
Pipeline与Head-of-line Blocking问题
有了“连接复用”之后,减少了建立连接、关闭连接的开销。但还存在一个问题,在同一个连接上,请求是串行的,客户端发送一个请求,收到响应,然后发送下一个请求,再收到响应。这种串行的方式导致并发度不够。 为此,HTTP1.1引入了Pipeline机制。在同一个TCP连接上面,可以在一个请求发出去之后、响应没有回来之前,就可以发送下一个、再下一个,这样就提高了在同一个TCP连接上面的处理请求的效率。如下图所示,展示了在同一个TCP连接上面,串行和Pipeline的对比。 从上图可以明显看出,Pipeline提高了请求的处理效率。但Pipeline有个致命问题,就是Head-of-Line Blocking翻译成中文叫做“对头阻塞”。什么意思呢? 客户端发送的请求顺序是1,2,3,虽然服务器是并发处理的,但客户端端接收响应的顺序必须是1,2,3,如此才能把响应和请求成功匹对,跟队列一样,先进先出。一旦队列头部请求1发生延迟,客户端迟迟收不到请求1的响应,则请求2、请求3响应也会被阻塞。如果请求2、请求3不和请求3在一个TCP连接上面,而是在其他的TCP连接上面发出去的话,说不定早返回了,现在因为请求1处理的慢,也影响了请求2、请求3。 也正因为如此,为了避免Pipeline带来的副作用,很多浏览器默认把Pipeline关闭了。
HTTP/2出现之前的性能提升方法
HTTP/2
TCP协议是先进先出的协议。 报文段在一个TCP连接上,如果按照seq=1,seq=2,seq=3的顺序发送到服务器,服务器端必定是按照seq=1,seq=2,seq=3的顺序接收报文段。如果过程中seq=1的数据包延迟或者丢包,服务端会一直等待seq=1的包。 因为报文有seq和ack序号,比如服务端发送一个包ack=1,那么客户端发送的包seq=1必须先到服务端,之后的seq=2的包才能被服务端接收,这就是TCP协议的报文先进先出规则,这注意是为了放置丢包等问题。
SSL/TLS
在介绍HTTPS之前,需要先深入探讨SSL/TLS,因为HTTPS是构建在这个基础上的。
背景
SSL/TLS的历史几乎互联网历史一样长:SSL(Secure Socket Layer)的中文名词为安全套接层,TLS(Transport Layer Security)的中文名词为传输层安全协议。 1994年,网景(NetScape)公司设计了SSL1.0; 1995年,网景公司发布SSL2.0,但很快发现存在严重漏洞; 1996年,SSL3.0问世,得到大规模应用; 1999年,互联网标准化组织IETF对SSL进行标准化,发布了TLS1.0; 2006年和2008年,TLS进行了两次升级,分别为TLS1.1和TLS1.2。 所以,TLS1.0相当于SSL3.1;TLS1.1、TLS1.2相当于SSL3.2、SSL3.3。在应用层里,习惯将两者并称为SSL/TLS。 如下图,SSL/TLS处在TCP层的上面,它不仅可以支撑HTTP协议,也能支撑FTP、IMAP等其他各种应用层的协议。 接下来从最基础的对称加密讲起,一步步分析SSL/TLS背后的原理和协议本身。
对称加密的问题
对称加密的想法很简单,如下图所示。客户端和服务器知道同一个密钥,客户端给服务器发消息,客户端用此密钥加密,服务器用此密钥解密;反过来,服务器给客户端发消息时,是相反的过程。 这种加密方式在互联网上有两个问题:
- 密钥如何传输?
密钥A的传输也需要另外一个密钥B,密钥B的传输有需要密钥C…,如此循环,无解。 - 如何存储密钥?
对应浏览器的网页来说,都是明文的,肯定存储不了密钥;对应Android/iOS客户端,即使能把密钥藏在安装包的某个位置,也很容易被破解。 当然,这两个问题其实是一个问题。因为如果解决了密钥的传输问题,就可以在建立好连接之后获取到密钥,然后只存在内存中,当连接断开之后,密钥在内存中就被销毁了,也就解决了存储的问题。
双向非对称加密
|