写点什么

探索 http1.0 到 http3.0 的发展史,详解 http2.0

作者:ike潮
  • 2022 年 9 月 20 日
    广东
  • 本文字数:6465 字

    阅读完需:约 21 分钟

探索http1.0到http3.0的发展史,详解http2.0

标题:探索 http1.0 到 http3.0 的发展史,详解 http2.0

1、http1.0

1.1、特点

  • 短链接性能拉垮,每次发送请求都必须重新建立 TCP 连接,每次响应结束后都会断开 TCP 连接,http 客户端容易端口占用太多;

  • 无 host 头域(请求头信息中无需 host),http1.0 时代认为每台服务器都绑定一个唯一的 IP,所以请求消息中并没有传递服务器的主机名;

  • 不允许断点续传,每次只能传输完整的对象,不能只传输一部分,对文件的上传下载极度不友好;

  • 非管道化的缺陷,规定下一个请求必须在前一个请求响应到达之前才能发送,若前一个请求响应一直不到达,下一个请求就不发送;

  • 由于请求排队的原因,http1.0 的队头阻塞始终发生在客户端,如下所示:


1.2、测试

由于现在 http1.0 几乎已经被弃用了,只能通过更改源码中的 http 配置参数来实现,以 node 为例:

1.2.1、搭建一个 httpServer

const http = require('http')
/** * 构造http1.0 * @type {http.ServerResponse._storeHeader} */const serverResponseStoreHeader = http.ServerResponse.prototype._storeHeader;http.ServerResponse.prototype._storeHeader = function () { this.httpVersion = '1.0'; arguments[0] = 'HTTP/1.0' + arguments[0].slice(8); serverResponseStoreHeader.apply(this, arguments);};
http.createServer(((req, res) => { console.log(req.url) res.end('Hello , this is http 1.0 version')})).listen( 3000, err => { if (err) { return console.log('something bad happened', err) } console.log('server is listening on 3000')});
复制代码

1.2.2、搭建一个 httpClient

let http = require('http');
/** * 构造http1.0,删除头信息的host * @type {http.ClientRequest._storeHeader} */const clientRequestStoreHeader = http.ClientRequest.prototype._storeHeader;http.ClientRequest.prototype._storeHeader = function () { this.httpVersion = '1.0'; arguments[0] = arguments[0].slice(0, -3) + '0\r\n'; delete arguments[1]; clientRequestStoreHeader.apply(this, arguments);};
let options = { hostname: '127.0.0.1', port: 3000, path: '/ikejcwang/syaHello?method=sayHello', method: 'POST'};
let req = http.request(options, function (res) { console.log('STATUS: ' + res.statusCode); console.dir(res.headers);
res.on('data', function (chunk) { console.log('BODY: ' + chunk); });});req.on('error', function (e) { console.log('problem with request_test: ' + e.message);});
req.end('hello http1.0 version');
复制代码

1.2.3、启动测试

先开启 httpServer,打开 Wireshark 抓包,再启动 httpClient,


抓包记录如下:



抓包请求 &响应的报文如下:


POST /ebus/ikejcwang/syaHello?method=sayHello HTTP/1.0Connection: closeTransfer-Encoding: chunked
4haha0
HTTP/1.0 200 OKDate: Sun, 11 Sep 2022 09:37:13 GMTConnection: close
Hello Its Your Node.js Server!
复制代码

2、http1.1

2.1、特点

  • 启用长连接,支持在一个 TCP 连接上传输多个 http 的请求与响应,减少了建立和关闭连接的消耗和延迟。通过头信息的connection:keep-alive来控制,默认开启,可以设置close关闭关闭长连接;

  • 节省带宽,http1.1 支持只传 headers(不带任何 body),可以通过相应状态码来决定后续传输的报文。http1.0 每次都需要传输一个完整的对象,极大程度上浪费了带宽;

  • host 域,http1.1 强制要求请求头信息中必须携带 host 域,否则会报错(400 Bad Request),随着虚拟主机技术的发展,一台物理机上可以有多个虚拟主机,共享一个 IP 地址,host 域更好的适应了虚拟主机环境;

  • 丰富缓存策略,在 http1.0 中主要使用 headers 里的 If-Modified-Since,Expires 来做为缓存判断的标准,http1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since,If-Match,If-None-Match 等更多可供选择的缓存头来控制缓存策略。

  • 相对新增了 24 个响应状态码;

  • 引入管道化技术(pipeline),支持将多条请求放入队列,不必等待之前的请求是否有响应,后续的请求都可以陆续发送,在高延时网络条件下,可以降低网络回环时间;

  • 队头阻塞发生在服务端,客户端可以允许发送多个请求,但 http1.1 规定,服务端的响应报文必须根据收到请求的顺序排队依次应答,造成问题是,如果第一个请求的处理逻辑复杂,执行周期长,生成响应自然慢,直接阻塞了后续请求的响应应答;如下所示:


2.1.1、队头阻塞解决方案

  1. 并发 TCP 连接,对于一个独立域名,是允许分配多个 TCP 长连接,将请求均分到 TCP 连接上,主动避开队头阻塞的产生,在 RFC 规范中规定客户端最多并发 2 个连接,实际情况不然,Chrome 中是 6 个,说明浏览器一个域名采用 6 个 TCP 连接,并发 HTTP 请求;

  2. 域名分片,在一个域名下分出多个子域名,使其最终指向同一台服务器,其原理同上,还是为了增加并发;

2.2、测试

现在绝大多数应用使用的是 http1.1,也默认的是长连接,此处在请求头中主动设置connection:close来测试短链接

2.2.1、搭建一个 httpServer

就是当下一个标准的 httpServer:


const http = require('http');
http.createServer().on("request", (req, res) => {
console.log("接收请求") console.log("url:"+req.url) console.log("headers:"+JSON.stringify(req.headers))
let d = []; req.addListener("data",chunk=>{ d.push(chunk) }) req.addListener("end", ()=>{ console.log("接收完报文:") console.log(d.toString()) })
res.statusCode = 200 res.end(JSON.stringify({name: 'ike'}));
}).listen(9000, "0.0.0.0");
复制代码

2.2.2、搭建一个 httpClient

let http = require('http');
let options = { hostname: '127.0.0.1', port: 9000, path: '/ikejcwang/syaHello?method=sayHello', method: 'POST', headers: { 'connection': 'close' // 测试短链接 }};
let req = http.request(options, function (res) { console.log('STATUS: ' + res.statusCode); console.log('HEADERS: ' + JSON.stringify(res.headers));
let body = [] res.addListener('data', function (chunk) { body.push(chunk); }); res.addListener('end', () => { console.log('response body:' + body.toString()); })});req.on('error', function (e) { console.log('problem with request_test: ' + e.message);});req.end('hello http1.1 version');
复制代码

2.2.3、启动测试

先开启 httpServer,打开 Wireshark 抓包,再启动 httpClient,


抓包记录如下:标准的三次握手建立连接,http 报文传输,四次挥手断开连接



请求响应报文如下:


POST /ikejcwang/syaHello?method=sayHello HTTP/1.1connection: closeHost: 127.0.0.1:9000Content-Length: 21
hello http1.1 versionHTTP/1.1 200 OKDate: Mon, 12 Sep 2022 08:29:47 GMTConnection: closeContent-Length: 14
{"name":"ike"}
复制代码

3、http2.0

3.1、特点

3.1.1、二进制分帧

http1.x 在应用层是以纯文本的方式通信,注定了每次请求 &响应的数据包特别大,这是第一个影响其通信效率的原因。http2.0 针对此做了改造,将所有的传输信息分割为更小的帧和消息,并对此采用二进制编码,所以 http2.0 的客户端和服务端都需要引进新的二进制编解码机制。


http2.0 并没有改写 http1.x 之前的各种在应用层上的语义,只是用分帧的技术将原来的数据包重新封装了一下,比方说:http1.x 的传输信息主要为 headers+body,http2.0 的传输信息就是 headers 帧和 body 帧;

3.1.2、帧

最小的传输单位,http2.0 定义了帧的模板(跟协议模板类似),也有头部,头部标明了帧长度,类型,标志位……其中帧类型如下所示:


  • DATA:用于传输 http 消息体;

  • HEADERS:用于传输首部字段;

  • SETTINGS:用于约定客户端和服务端的配置数据。比如设置初识的双向流量控制窗口大小;

  • WINDOW_UPDATE:用于调整个别流或个别连接的流量

  • PRIORITY: 用于指定或重新指定引用资源的优先级。

  • RST_STREAM: 用于通知流的非正常终止。

  • PUSH_ PROMISE: 服务端推送许可。

  • PING: 用于计算往返时间,执行“ 活性” 检活。

  • GOAWAY: 用于通知对端停止在当前连接中创建流。


标志位用于不同的帧类型定义特定的消息标志。比如 DATA 帧就可以使用End Stream: true表示该条消息通信完毕。流标识位表示帧所属的流 ID。优先值用于 HEADERS 帧,表示请求优先级。R 表示保留位



3.1.2、消息

就是逻辑上 HTTP 的数据包(请求 &响应)。一系列数据帧组成了一个完整的消息。比如一系列 DATA 帧和一个 HEADERS 帧组成了请求消息;

3.1.3、流

http1.x 是一种半双工的通信协议,规定在某一时刻数据只能在一个方向上传递(要么是轻轻,要么是响应),这是第二个影响其通信效率的原因,http2.0 定义了一种虚拟信道,可以承载双向数据传输,模拟全双工模式,被称作。每个流有唯一整数标识符 ID,为了防止两端流冲突,客户端发起的流具有奇数 ID,服务器端发起的流具有偶数 ID。


所有的流 Stream 都是建立在一个 TCP 连接之上的,每个数据流以消息的形式发送, 每条消息由多个帧组成,帧可以乱序发送,对端根据每个帧首部的标识符重新组装。如下所示:



3.1.4、多路复用

如上所述:基于流 Stream 的设计,http2.0 可以在一个共享 TCP 连接的基础上,同时发送请求和响应。http 消息被分解为独立的帧,每一帧都拥有着自己的标识符,保证交错发送出去后,对端能够根据流 ID 和首部将它们重新组合起来。


由于 http1.x 的队头阻塞问题一直存在,不管是 http1.0 的客户端队头阻塞,还是 http1.1 的服务端队头阻塞,都直接影响了网页 &图片 &流媒体……的渲染时间,这是第三个影响其通信效率的原因=


http 2.0 建立一条 TCP 连接后,可以并行传输着多个数据流,客户端向服务端乱序发送 stream1~n 的一系列的 DATA 帧,双向通信的模式下,服务端已经在依次返回 stream n 的 DATA 帧了,单个 TCP 下做到极致传输,无需并发连接对于服务器的性能也有很大提升;

3.1.5、请求优先级

流可以带有一个 31 字节的优先级。当客户端明确指定优先级时,服务端可以根据这个优先级作为依据。


例如:客户端优先级设置为.css>.js>.jpg,服务端按优先级返回结果有利于高效利用底层连接,提高用户体验。 然而,也不能过分迷信请求优先级,仍然要注意以下问题:


  • 服务端是否支持请求优先级

  • 会否引起队首阻塞问题,比如高优先级的慢响应请求会阻塞其他资源的交互

3.1.6、服务端主动推送

HTTP 2.0 增加了服务端推送功能,服务端可以根据客户端的请求,提前返回多个响应,推送额外的资源给客户端,主要应对 html5 页面的加载。例如:客户端请求 stream 1(/page.html)。服务端在返回 stream 1 消息的同时推送了 stream 2(/script.js)和 stream 4(/style.css)……


PUSH_PROMISE 帧是服务端向客户端有意推送资源的信号。


  • 如果客户端不需要服务端 Push,可在 SETTINGS 帧中设定服务端流的值为 0,禁用此功能

  • PUSH_PROMISE 帧中只包含预推送资源的首部。如果客户端对 PUSH_PROMISE 帧没有意见,服务端在 PUSH_PROMISE 帧后发送响应的 DATA 帧开始推送资源。如果客户端已经缓存该资源,不需要再推送,可以选择拒绝 PUSH_PROMISE 帧。

  • PUSH_PROMISE 必须遵循请求-响应原则,只能借着对请求的响应推送资源。

3.1.7、headers 压缩

http1.x 每一次的请求 &响应,都会携带 header 信息用于描述资源属性,有的 header 信息庞大,且来回反复传递。http2.0 在客户端和服务端之间使用内存来管理“头部表”,通过头部表来跟踪和存储之前发送的键-值对,头部表在连接过程中始终存在,新增的键-值对会更新到表尾,因此,后续的通信中直接使用 k-v 的方式代替原来 header 中的明文,实现压缩的效果。



3.2、测试

3.2.1、搭建一个 httpServer

引用 http2 的依赖包,其余跟 http1 一样。


const http2 = require("http2")
// 开启http2服务http2.createServer().on('request', (request, response) => {
console.log("http2 request_test")
let d = [] request.addListener("data", chunk => { d.push(chunk) }) request.addListener("end", () => { console.log(d.toString()) })
response.end('hello http2 server')
}).listen(8080, '0.0.0.0');
复制代码

3.2.2、搭建一个 httpClient

通过http2.connect能够看出,http2 的 client 应该是个类似全双工的通信模式,建立连接,写数据,接收数据


const http2 = require('http2')
const reqBody = JSON.stringify({'name': 'ikejcwang'})const client = http2.connect('http://127.0.0.1:8080');const req = client.request({ ':method': 'POST', ':path': '/hello', 'content-type': 'application/json', 'content-length': Buffer.byteLength(reqBody),});
let resBody = [];req.on('response', (headers, flags) => { console.dir(headers)});req.on('data', chunk => { resBody.push(chunk);});req.on('end', () => { console.log("resBody: " + resBody.toString())
// client需要主动关闭,否则一直是连接状态 // client.close()});
req.end(reqBody)
复制代码

3.2.3、启动测试

先开启 httpServer,打开 Wireshark 抓包,再启动 httpClient,


client 的终端始终在连接态中,没有断开,印证了 stream。



抓包记录如下:标准的三次握手建立连接,http2 的多路复用,流式报文传输



一次完整请求 &响应的数据报文如下:


.........PRI * HTTP/2.0
SM
...........'.......A...\..p.x....br.A.._..u.b&=LtA...20.........{"name":"ikejcwang"}............................a..i~....Z...%...?p.S............hello http2 server.........
复制代码


可以发现,headers 被压缩简化,采用编码替代了。

3.3、传输层的瓶颈

启用 http2.0 之后,应用整体的性能必然提升,但是所有的流 Stream 都集中在传输层的一个 TCP 连接之上,然而 TCP 本身的可靠性机制带来的性能瓶颈必然无法避免,例如:TCP 分组的队首阻塞问题,单个 TCP 数据包丢失导致整个连接的阻塞无法逃避,此时所有 http2 的 Stream 都会受到影响。

4、http3.0

4.1、历史问题

4.1.1、tcp 的缺点:

  • tcp 协议是有序的,存在对头阻塞的问题,如果有一个数据包丢失了,后面的数据包都需要排队等待,效率比较低

  • tcp 协议和 TLS 的握手是分开进行的,增加了握手的延时,对 https 不太友好;

  • tcp 协议是基于客户端 IP,客户端端口,服务端 IP,服务端端口,这四元组确定一个连接的,在有线网络中没有问题。但是在无线网络中,客户端的 IP 经常会发生变化,导致 tcp 连接经常性需要重连。很明显的 i 个案例:手机从流量数据切换到 Wi-Fi,正在刷的视频会卡顿重载一下的。

4.2、变革

Google 公司在 http1.x,http2.0 的发展历史中破旧迎新,做出决定:http3.0 正式抛弃 tcp,基于 udp 协议开发了一个能规避 tcp 缺陷,又能兼容 udp 优点的叫做 quic 协议,http3.0 正是运行在 quic 之上。

4.3、特点

  • 实现了并发无序的字节流式传输,解决了对头阻塞的问题;

  • http3.0 重新定义了 tls 的加密方式,降低了建立连接的延时;

  • 兼容了 udp 的高效特性,满足了连接迁移不断开的功能,在无线网络切换 IP 时,无需重新建立连接;

4.4、测试

暂无测试案例,各类语言还有待封装实现。

5、结尾

tcp 丢包后队头阻塞的解释:


阻塞发生在用户态,内核态的报文接收正常,这也抵消了一个疑问,为啥抓包抓不出来队头阻塞的现象。


在内核态并不会堵塞,后续报文会进入 linux 的 ooo 队列(out-of-order),等待着丢失的报文。 一旦丢失的报文再次出现(重传机制),才开始响应用户态,用户就能正常接受到数据。


即,对于 Linux 内核来说一切收发报文都是正常的,不会堵塞。但是对于用户态确实是堵塞的,因为 tcp 的数据包是基于 seq(序列号) 而丢包导致 seq 不是连续的,所以必然不能返回到用户态,用户态也就堵塞了。


关于 tcp&udp 的详解,可以看这里:https://xie.infoq.cn/article/3334dc73de331eb4f020eb7a8

发布于: 2022 年 09 月 20 日阅读数: 62
用户头像

ike潮

关注

还未添加个人签名 2020.08.21 加入

全栈程序员

评论

发布
暂无评论
探索http1.0到http3.0的发展史,详解http2.0_HTTP_ike潮_InfoQ写作社区