HTTP的历史和未来

Posted by HHP on December 2, 2019

前言

http是目前互联网应用层使用最广泛的协议,无论你是用浏览器打开一个网页,还是使用手机app来看电影玩游戏,绝大部分都是使用的http协议来传输数据。本文将带大家一块来了解一下http的发展历史和未来的发展方向。

HTTP 1.0

最早的http协议标准可以追溯到1996年五月,但实际上,早在1990年,http协议就开始在互联网里大规模的使用了,只是直到1996年五月,才将http规范化到rfc标准RFC1945,叫http 1.0。

HTTP连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。

下面来简单看一下http 1.0 的通信过程(以访问www.baidu.com为例),下面是我的访问过程:

大家先看到第一个红框,这是整个http请求的开始。

第一行 GET / HTTP/1.0是请求行,GET是请求方法,还有其他三种方法分别是deleteputpost。用这四种方法,就可以实现数据的增删改查功能。 /是客户端请求的url,这里请求的是www.baidu.com的根路径。HTTP/1.0是http协议的版本。

第二行到第四行是请求头部,携带了客户端的一些有用的信息。

第七行是服务器response的回复行,携带了协议版本和状态码信息。

第八行到最后一行是response的头部。

我们还可以根据自己的需要定制自己所需要的头部,最常用的就是像AUTH_TOKEN等等。

第二个红框就是百度服务器给我返回的消息体,就是我们浏览器看到的东西了。

HTTP 1.0 有一个很大的缺点,就是一个tcp连接只能发起一个http请求,大大的浪费了网络带宽和降低了通讯效率。

HTTP 1.1

之前说了, HTTP 1.0是很老的协议了。在1999年6月,IETF组织制定了RFC2616,公布了HTTP1.1协议的诞生。HTTP 1.1相对于上一代HTTP 1.0来说,改进了以下几个点:

  • 缓存处理,在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
  • 带宽优化及网络连接的使用,HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
  • 错误通知的管理,在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。

  • Host头处理,在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。
  • 长连接,HTTP 1.1支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟,在HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。

在这么多改进的点中,和性能最相关的,也是我认为HTTP 1.1中的最大亮点就是最后一个,支持长连接和流水线处理了。在HTTP 1.0中,每发一个http请求,都要建立一个tcp连接。要知道,建立tcp连接要三次握手,需要1.5个RTT,是非常的耗时的,而且作为客户端去访问,本身端口号最多也就只有65536个(当然客户端一般情况下是不会用完的),发起连接的数量也是有限的,所以HTTP1.0的数据传输效率和用户体验都会很差。在HTTP 1.1 中,由于默认开启keepalive,就是说在一个tcp连接中可以发多个请求。流水线(pipeline处理)则是说在发多个请求的的过程中,无需等待前面的请求对应的response回复完成再发下一个,而是可以一次性把请求都先发完(当然还是串行的,这也是pipeline的意思),再等服务器响应。pipeline可以节省大量等待相应的时间。当然,服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容

​ 从图中可以明显看到response的header带有connection: keepalive字段

HTTP2

HTTP 1.1 看起来已经很完善了,效率也大大有了提升,但是我们不满足于现状,还要精益求精。于是有了HTTP2协议。HTTP2是目前最快性能最好的HTTP协议了,它在2015年五月被规范进标准(RFC7540)。HTTP2前身是google开发的SPDY,HTTP2是在SPDY的基础上进行了一些小改动,成为了标准而已。

相比于HTTP1.1,HTTP2的改进都是关于性能的提升。主要有以下几点:

  • 多路复用

    在http1.1中,请求是pipeline形式发送的,服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容。假设现在有一种情况,第一个请求需要服务器处理很久才返回,那么后面的请求虽然是已经发送过去了,但服务器还在处理第一个请求,以至于即使后面的请求可能处理时间会很快,但也会被阻塞住。而多路复用则是让请求可以并行的传输(逻辑上并行,有点像单核多线程的意思)。 例如客户端要向服务器发送Hello、World两个单词,客户端 可以将数据拆成包,给每个包打上标签。发的时候是这样的①H ②W ①e ②o ①l ②r ①l ②l ①o ②d。这样到了服务器,服务器根据标签把两个单词区分开来。 这样服务器就可以并行地处理多个请求了,而且由于每一个包都是带标签的,所以服务端也不需要按顺序发response了,也是逻辑上并行地发送数据即可。http2解决了http层面的队头阻塞因为服务器在接受完一个请求的头部之后,其实就可以开始做处理了(不需要把包体全部接受完),所以这种逻辑上的并行是能够加快数据的处理的。最关键的是,http2 让所有数据流共用同一个连接,可以更有效地使用 TCP 连接,让高带宽也能真正的服务于 HTTP 的性能提升。 以往使用http1.1的话,浏览器一般会启用多个tcp连接,由于tcp连接的慢启动,让原本就具有突发性和短时性的 HTTP 连接变的十分低效。 使用HTTP2,由于 TCP 连接的减少而使网络拥塞状况得以改善,同时慢启动时间的减少,使拥塞和丢包恢复速度更快

    但是由于http2是所有数据流共用一个tcp连接,但网络环境很不好的情况下(发生大量丢包),由于tcp的丢包重传机制,会使http2发挥不出他的优良性能,这时可能效率比http1还低。

  • 头部压缩

    在 HTTP/1 中,HTTP 请求和响应都是由「状态行、请求 / 响应头部、消息主体」三部分组成。一般而言,消息主体都会经过 gzip 压缩,或者本身传输的就是压缩过后的二进制文件(例如图片、音频),但状态行和头部却没有经过任何压缩,直接以纯文本传输。 随着 Web 功能越来越复杂,每个页面产生的请求数也越来越多,导致消耗在头部的流量越来越多,尤其是每次都要传输 UserAgent、Cookie 这类不会频繁变动的内容,完全是一种浪费。

    部压缩需要在支持 HTTP/2 的浏览器和服务端之间:

    • 维护一份相同的静态字典(Static Table),包含常见的头部名称,以及特别常见的头部名称与值的组合;
    • 维护一份相同的动态字典(Dynamic Table),可以动态的添加内容;
    • 支持基于静态哈夫曼码表的哈夫曼编码(Huffman Coding);

    静态字典的作用有两个:

    • 对于完全匹配的头部键值对,例如 “:method :GET”,可以直接使用一个字符表示;
    • 对于头部名称可以匹配的键值对,例如 “cookie :xxxxxxx”,可以将名称使用一个字符表示。

    同时,浏览器和服务端都可以向动态字典中添加键值对,之后这个键值对就可以使用一个字符表示了。需要注意的是,动态字典上下文有关,需要为每个 HTTP/2 连接维护不同的字典。在传输过程中使用,使用字符代替键值对大大减少传输的数据量。

  • 服务器推送

    服务端推送是一种在客户端请求之前发送数据的机制。当代网页使用了许多资源:HTML、样式表、脚本、图片等等。在HTTP/1.x中这些资源每一个都必须明确地请求。这可能是一个很慢的过程。浏览器从获取HTML开始,然后在它解析和评估页面的时候,增量地获取更多的资源。因为服务器必须等待浏览器做每一个请求,网络经常是空闲的和未充分使用的。

    为了改善延迟,HTTP/2引入了server push,它允许服务端推送资源给浏览器,在浏览器明确地请求之前。一个服务器经常知道一个页面需要很多附加资源,在它响应浏览器第一个请求的时候,可以开始推送这些资源。这允许服务端去完全充分地利用一个可能空闲的网络,改善页面加载时间。

HTTP3

HTTP3 是http未来的协议,尚在标准化的路上,但估计很快就能跟大家见面了。HTTP3的前身是QUIC,QUIC也是google开发的,所以说google在http优化这方面做的贡献是十分的大的。HTTP3和QUIC大致相同,下面我们用QUIC来代替HTTP3进行讲解。

QUIC 全称 quick udp internet connection ,“快速 UDP 互联网连接”,(和英文 quick 谐音,简称“快”)。他直接放弃了我们一直使用的tcp协议,转而使用udp协议。之所以选择udp,是因为 TCP 是由操作系统在内核协议栈层面实现的,应用程序只能使用,不能直接修改。虽然应用程序的更新迭代非常快速和简单,但是 TCP 的迭代却非常缓慢,原因就是操作系统升级很麻烦。 这也就意味着即使 TCP 有比较好的特性更新,也很难快速推广。比如 TCP Fast Open。它虽然 2013 年就被提出了,但是 Windows 很多系统版本依然不支持它。 使用udp来构建quic,相当于在应用层层面重构一个新的类似tcp的可靠协议出来,而且由于协议在应用层,是可以升级迭代很快的。

Quic 相比现在广泛应用的 http2+tcp+tls 协议有如下优势:

  • 减少了 TCP 三次握手及 TLS 握手时间。

    我们知道,若使用http2,由于存在三次握手和tls握手,在真正传输数据之前,需要3个RTT,而quic则实现了0RTT握手。目前quic使用的加密协议是自定义的,但未来标准化之后应该就是使用TLS1.3来进行握手了,因为TLS1.3是支持0RTT握手的。

  • 改进的拥塞控制。

    TCP 的拥塞控制实际上包含了四个算法:慢启动,拥塞避免,快速重传,快速恢复 。

    QUIC 协议当前默认使用了 TCP 协议的 Cubic 拥塞控制算法 ,同时也支持 CubicBytes, Reno, RenoBytes, BBR, PCC 等拥塞控制算法。

    从拥塞算法本身来看,QUIC 只是按照 TCP 协议重新实现了一遍,但却有tcp不支持的可插拔特性。

    什么叫可插拔呢?就是能够非常灵活地生效,变更和停止。体现在如下方面:

    1. 应用程序层面就能实现不同的拥塞控制算法,不需要操作系统,不需要内核支持。这是一个飞跃,因为传统的 TCP 拥塞控制,必须要端到端的网络协议栈支持,才能实现控制效果。而内核和操作系统的部署成本非常高,升级周期很长,这在产品快速迭代,网络爆炸式增长的今天,显然有点满足不了需求。

    2. 即使是单个应用程序的不同连接也能支持配置不同的拥塞控制。就算是一台服务器,接入的用户网络环境也千差万别,结合大数据及人工智能处理,我们能为各个用户提供不同的但又更加精准更加有效的拥塞控制。比如 BBR 适合,Cubic 适合。

    3. 应用程序不需要停机和升级就能实现拥塞控制的变更,我们在服务端只需要修改一下配置,reload 一下,完全不需要停止服务就能实现拥塞控制的切换。

  • 避免队头阻塞的多路复用。

    多路复用是 HTTP2 最强大的特性,能够将多条请求在一条 TCP 连接上同时发出去。但也恶化了 TCP 的一个问题,那就是tcp的队头阻塞。

    假设HTTP2 在一个 TCP 连接上同时发送 4 个 Stream。其中 Stream1 已经正确到达,并被应用层读取。但是 Stream2 的第三个 tcp segment 丢失了,TCP 为了保证数据的可靠性,需要发送端重传第 3 个 segment 才能通知应用层读取接下去的数据,虽然这个时候 Stream3 和 Stream4 的全部数据已经到达了接收端,但都被阻塞住了。本质上这是由于tcp的可靠性保证引起的问题。

    不仅如此,由于 HTTP2 强制使用 TLS,还存在一个 TLS 协议层面的队头阻塞 。 Record 是 TLS 协议处理的最小单位,最大不能超过 16K,一些服务器比如 Nginx 默认的大小就是 16K。由于一个 record 必须经过数据一致性校验才能进行加解密,所以一个 16K 的 record,就算丢了一个字节,也会导致已经接收到的 15.99K 数据无法处理,因为它不完整。

    而QUIC 最基本的传输单元是 Packet,不会超过 MTU 的大小,整个加密和认证过程都是基于 Packet 的,不会跨越多个 Packet。这样就能避免 TLS 协议存在的队头阻塞。

    而且Stream 之间相互独立,比如 Stream2 丢了一个 Pakcet,不会影响 Stream3 和 Stream4。不存在 TCP 队头阻塞。

  • 连接迁移。

    一条 TCP 连接 是由四元组标识的(源 IP,源端口,目的 IP,目的端口)。什么叫连接迁移呢?就是当其中任何一个元素发生变化时,这条连接依然维持着,能够保持业务逻辑不中断。当然这里面主要关注的是客户端的变化,因为客户端不可控并且网络环境经常发生变化,而服务端的 IP 和端口一般都是固定的。 比如大家使用手机在 WIFI 和 4G 移动网络切换时,客户端的 IP 肯定会发生变化,需要重新建立和服务端的 TCP 连接。

    针对 TCP 的连接变化,MPTCP协议其实已经有了解决方案,但是由于 MPTCP 需要操作系统及网络协议栈支持,部署阻力非常大,目前并不是很适用。但实际上,目前ios也是有使用MPTCP的,当你从wifi切换到4g的时候,你的tcp流是不会断的。这个大家有兴趣可以去了解一下MPTCP。

    那 QUIC 是如何做到连接迁移呢?很简单,任何一条 QUIC 连接不再以 IP 及端口四元组标识,而是以一个 64 位的随机数作为 ID 来标识,这样就算 IP 或者端口发生变化时,只要 ID 不变,这条连接依然维持着,上层业务逻辑感知不到变化,不会中断,也就不需要重连。

    由于这个 ID 是客户端随机产生的,并且长度有 64 位,所以冲突概率非常低。

  • 前向冗余纠错。

    QUIC 还能实现前向冗余纠错,在重要的包比如握手消息发生丢失时,能够根据冗余信息还原出握手消息。

总结

个人认为,从性能的角度而言,http3的最大亮点是解决了http2的tcp队头阻塞,而HTTP2则是解决了http1的http层面的队头阻塞。http的优化还在不断的进行,不得不感概技术的进步是如此的快。

——END——