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

本文涉及的产品
.cn 域名,1个 12个月
简介: 实例代码探索http1.0到http3.0的发展史,详细拆解http2.0,队头阻塞的解决方案,分层帧,多路复用,传输层的伟大变革

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

引言:可能几年后,http1.1也将退出历史舞台,那就必须先了解全新的http2.0,http3.0的特性

1、http1.0

1.1、特点

  • 短链接性能拉垮,每次发送请求都必须重新建立TCP连接,每次响应结束后都会断开TCP连接,http客户端容易端口占用太多;
  • 无host头域(请求头信息中无需host),http1.0时代认为每台服务器都绑定一个唯一的IP,所以请求消息中并没有传递服务器的主机名;
  • 不允许断点续传,每次只能传输完整的对象,不能只传输一部分,对文件的上传下载极度不友好;
  • 非管道化的缺陷,规定下一个请求必须在前一个请求响应到达之前才能发送,若前一个请求响应一直不到达,下一个请求就不发送;
  • 由于请求排队的原因,http1.0的队头阻塞始终发生在客户端,如下所示:

    http1.0对头阻塞.png

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,

抓包记录如下:

http发展史http1.0.png

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

POST /ebus/ikejcwang/syaHello?method=sayHello HTTP/1.0
Connection: close
Transfer-Encoding: chunked

4
haha
0

HTTP/1.0 200 OK
Date: Sun, 11 Sep 2022 09:37:13 GMT
Connection: 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规定,服务端的响应报文必须根据收到请求的顺序排队依次应答,造成问题是,如果第一个请求的处理逻辑复杂,执行周期长,生成响应自然慢,直接阻塞了后续请求的响应应答;如下所示:

    http1.1对头阻塞.png

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报文传输,四次挥手断开连接

http发展史http1.1.png

请求响应报文如下:

POST /ikejcwang/syaHello?method=sayHello HTTP/1.1
connection: close
Host: 127.0.0.1:9000
Content-Length: 21

hello http1.1 versionHTTP/1.1 200 OK
Date: Mon, 12 Sep 2022 08:29:47 GMT
Connection: close
Content-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表示保留位

http2帧抓包.png

3.1.2、消息

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

3.1.3、流

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

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

http2流抓包.png

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中的明文,实现压缩的效果。

http2头部压缩.png

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。

http2client终端.png

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

http2请求抓包.png

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

.........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不是连续的,所以必然不能返回到用户态,用户态也就堵塞了。

目录
相关文章
|
6月前
|
缓存 网络协议 安全
Android网络面试题之Http基础和Http1.0的特点
**HTTP基础:GET和POST关键差异在于参数传递方式(GET在URL,POST在请求体),安全性(POST更安全),数据大小限制(POST无限制,GET有限制),速度(GET较快)及用途(GET用于获取,POST用于提交)。面试中常强调POST的安全性、数据量、数据类型支持及速度。HTTP 1.0引入了POST和HEAD方法,支持多种数据格式和缓存,但每个请求需新建TCP连接。**
57 5
|
6月前
|
缓存 网络协议 Android开发
Android网络面试题之Http1.1和Http2.0
HTTP/1.1 引入持久连接和管道机制提升效率,支持分块传输编码和更多请求方式如PUT、PATCH。Host字段指定服务器域名,RANGE用于断点续传。HTTP/2变为二进制协议,实现多工处理,头信息压缩和服务器推送,减少延迟并优化资源加载。HTTP不断发展,从早期的简单传输到后来的高效交互。
81 0
Android网络面试题之Http1.1和Http2.0
|
存储 缓存 网络协议
Http1.0和Http1.1的区别?Http1.1和Http2.0的区别
Http1.0和Http1.1的区别?Http1.1和Http2.0的区别
128 1
|
缓存 网络协议 开发者
HTTP1.0、HTTP1.1 、HTTP2.0和HTTP3.0 的区别【面试题】
HTTP1.0、HTTP1.1 、HTTP2.0和HTTP3.0 的区别【面试题】
1183 0
HTTP1.0、HTTP1.1 、HTTP2.0和HTTP3.0 的区别【面试题】
|
缓存 网络协议 C++
HTTP1.0 vs HTTP1.1 vs HTTP2.0
HTTP1.0 vs HTTP1.1 vs HTTP2.0
108 0
|
缓存 网络协议 前端开发
【计算机网络】HTTP1.0、HTTP1.1 和 HTTP2.0的详细分析
目录前言HTTP1.0HTTP1.1HTTP2.0总结 前言 在1.0时代中,html从Web服务器传送到客户端。 在2.0时代中,多了一些css,js等前端语言的更新。 影响http的两个主要因素有:带宽以及延迟 网络拨号更加影响带宽 延迟的话主要通过:一个主机中浏览器对同个域名的链接限制、dns的解析时长、三次握手的连接时长等 HTTP1.0 主要用在简单的网页和浏览器中 具体的一个模式为: 建立连接 发出请求信息 响应信息 关掉连接 存在一个带宽的浪费,比如传输一部分数据,却传输了整个对象进
455 1
【计算机网络】HTTP1.0、HTTP1.1 和 HTTP2.0的详细分析
|
存储 缓存 网络协议