http2模块
要学习关于啥是
http2
,主要还是得带着 几个问题 来熟悉熟悉该模块:
- 什么是http2协议?
- http2与之前习得的http1.0有什么区别?
- 为什么要使用http2模块?
- 如何来使用https模块?
引言
首先,想要学习什么是
http2
模块,需要先晓得关于http2
协议的相关知识点: 超文本传输协议第二版,http2.0主要基于SPDY
协议,通过对http头部字段进行数据压缩
,对数据传输采用多路复用
、增加服务端主动推送
措施,来减少网络延迟,提高页面的加载速度,它并没有修改http的应用语义,仍然使用HTTP的请求方法、状态码等规则,主要修改了报文传输格式,引用二进制分帧技术来实现性能的提升!!!
这里什么是SPDY
?对头部数据是如何压缩的?多路复用又是如何实现的?服务端主动推送是如何实现的?什么是二进制分帧?
SPDY协议
即
SPeeDY
协议,由谷歌开发的基于TCP
的会话协议,用以最小化网络延迟,提升网络速度,优化用户的网络体验,对HTTP协议的增强,包括有数据流的多路复用,请求优先级以及http报头压缩,增加服务器启动流等特性! 也就是说http2.0其实就是由这个SPDY
协议发展衍生而来的!!!
SPDY
强制使用
二进制分帧
作为能够突破http1.x标准的性能限制,改进传输性能,实现低延迟何高吞吐量的关键所在!!! 帧(frame)包含有:
- 类型Type;
- 长度Length;
- 标记Flags;
- 流标识Stream;
- 有效载荷Payload
消息(Message)则包含有一个完整的请求/响应,一个消息由一个或者多个帧组成。 流(Stream)是链接中的一个虚拟通道,可以进行双向数据的传递,每个流都有唯一的整数标识符,一般为了防止两端流ID冲突,客户端发起的流具有奇数ID,服务端发起的流具有偶数ID! 关于二进制分帧的构成与它在http2.0中的一个通讯如下所示:
在二进制分帧层中,http2.0会将所有的传输信息分割称为更小的消息与帧,并对他们采用二进制格式编码并将其封装,新增的二进制分帧层能够保证原有的http现状,而将原本http1.x中的header封装到Headers
帧中,而将请求/响应的body封装到Data
帧中!过程如下所示:
http2.0关键特性
由于是从
SPDY
中衍生而来的,因此也“继承”了SPDY的相关特性:
- 对http头部字段进行数据压缩
- 多路复用/连接共享
- 请求优先级
- 服务端主动推送
对http头部字段进行数据压缩
http1.x htt2.0 :每次的请求头中都带有大量信息,而且每次都要进行重复发送!http2.0则使用
encoder
来减少需要传输的header大小,通讯双方各自缓存一份头部字段表,既避免了重复header的传输,又减少了需要传输的大小! 对于相同的数据,不再通过每次请求和响应来发送,通讯期间几乎不会改变通用键值对,只需发送一次。
http2.0所关注的压缩仅仅是header的压缩,与gzip的压缩body完全不冲突,两者配合使用的话,能够达到更好的压缩效果!
关于头部字段静态表的内容 所示:
索引 | 头部名称 | 头部值 |
---|---|---|
1 | :authority | |
2 | :method | GET |
3 | :method | POST |
4 | :path | / |
5 | :path | /index.html |
6 | :scheme | http |
7 | :scheme | https |
8 | :status | 200 |
9 | :status | 204 |
10 | :status | 206 |
11 | :status | 304 |
12 | :status | 400 |
13 | :status | 404 |
14 | :status | 500 |
15 | accept-charset | |
16 | accept-encoding | gzip, deflate |
17 | accept-language | |
18 | accept-ranges | |
19 | accept | |
20 | access-control-allow-origin | |
21 | age | |
22 | allow | |
23 | authorization | |
24 | cache-control | |
25 | content-disposition | |
26 | content-encoding | |
27 | content-language | |
28 | content-length | |
29 | content-location | |
30 | content-range | |
31 | content-type | |
32 | cookie | |
33 | date | |
34 | etag | |
35 | expect | |
36 | expires | |
37 | from | |
38 | host | |
39 | if-match | |
40 | if-modified-since | |
41 | if-none-match | |
42 | if-range | |
43 | if-unmodified-since | |
44 | last-modified | |
45 | link | |
46 | location | |
47 | max-forwards | |
48 | proxy-authenticate | |
49 | proxy-authorization | |
50 | range | |
51 | referer | |
52 | refresh | |
53 | retry-after | |
54 | server | |
55 | set-cookie | |
56 | strict-transport-security | |
57 | transfer-encoding | |
58 | user-agent | |
59 | vary | |
60 | via | |
61 | www-authenticate |
多路复用
http1.x http2.0:在http1.x中,浏览器客户端在同一时间,针对同一域名下的请求 一定数量的限制,超过限制树木的请求将会被阻塞,:point_right: 这也就是为何一些站点会有多个静态的CDN域名的原因之一,而http2.0中的多路复用则优化了这一性能,它允许同时通过单一的
http/2
连接发起多重的请求-响应消息,得益于二进制分帧机制,每个数据流都可以拆分称为互补以来的帧,而且这些帧可以乱序发送,最后在另一段把他们进行重新组合起来!! http2.0连接是持久化的,而且客户端与服务器之间也需要一个连接即可,可以承载数十或者数百个流的复用,多路复用意味着来自很多流的数据包能够混合在一起通过同一个连接来进行传输,当达到终点的时候,再根据不同的帧Headers中的流标识符重新连接将不同的数据进行组装!!过程如下 所示:
中展示了一个连接上的多个传输数据流:客户端想服务端传输数据帧stream5,而服务端则向客户端乱序传输Stream1以及Stream3!!
请求优先级
把http消息分为很多独立的帧之后,就可以通过优化这些帧的交错和传输进一步优化性能,每个流都带有一个31byte的优先值:0代表最高优先级,-1表示最低优先级,关于资源的优先级如下:
html > css > js > 图片
,对应在浏览器中访问的优先级体现如下:
服务端主动推送
服务器可以对一个客户端请求发送多个响应,服务器向客户端推送资源无需客户端明确地请求。并且,服务端推送能把客户端所需要的资源伴随着index.html一起发送到客户端,省去了客户端重复请求的步骤。 正因为没有发起请求,建立连接等操作,所以静态资源通过服务端推送的方式可以极大地提升速度。Server Push 让 http1.x 时代使用内嵌资源的优化手段变得没有意义;如果一个请求是由你的主页发起的,服务器很可能会响应主页内容、logo 以及样式表,因为它知道客户端会用到这些东西,这相当于在一个 HTML 文档内集合了所有的资源。 当服务端需要主动推送某个资源时,便会发送一个 Frame Type 为 PUSH_PROMISE 的 Frame,里面带了 PUSH 需要新建的 Stream ID。意思是告诉客户端:接下来我要用这个 ID 向你发送东西,客户端准备好接着。客户端解析 Frame 时,发现它是一个 PUSH_PROMISE 类型,便会准备接收服务端要推送的流
实现http2.0 未实现http2.0
的两张图对比了自己个人博客网站基于nginx
配置实现了http2.0的一个效果对比图,发现其中一个比较明显的地方:
ConnectionID: 在使用了http2.0了之后,有些资源请求用的是同一个ConnectionID,而一个新的连接将(可能)需要通过所谓要建立TCP握手.这对于性能原因很重要,因为TCP握手会导致相对较大的网络开销.我们正在创建一个新连接,因此获取HTTP响应需要更长的时间,对于看到相同ID的后续时间,将不会产生此开销.也就是说,浏览器不需要执行TCP握手并将重用相同的连接.这里我们说TCP连接仍然是"开放"的.已建立的连接可以更快地获取HTTP响应数据.
使用了http2.0了之后,protocal协议这里都是展示的h2
http2.0在node.js中的模块
一切从
http2.createServer()
方法出发,其本质是调用的new Http2Server()
来创建一个server对象,那么new
出一个Http2Server
对象的过程是怎样的?Http2Server
对象是怎样的一个对象呢?
Http2Server服务对象
继承于
net.Server
,基于TCP上的一个Server
服务对象,然后调用其初始化方法,该方法中接收options
中的Http1Incoming与Http1ServerResponse参数,分别来自于http1.x
中的InComingMessage以及HttpServerResponse对象,这也就说明了创建出来的Server
服务基础信息来自于http1.x
红的信息,对其响应信息对象做了一层包装器,接着在其初始化方法中设置newListener
以及request
监听器,并且一旦有请求request事件发生了,则移除newListener
事件监听,也就是这个事件是一次性的,相当于once
动作!在new Server()
的时候,传递的第二个参数connectionListener
,用于监听每一个客户端连接的回调动作!:stars: 在该回调动作中,进行了ServerHttp2Session
会话的创建,也就是针对每一个连接进行了一个会话的创建,且针对这个会话对象,设置了stream
事件的监听,通过该事件,使得在底层接收到请求,可以监听到这个流的一系列操作!然后再触发session
动作,然后当我们在session
动作中进行了流的写入的时候,这个时候,就会自动的触发Server的stream
事件! 在Server中设置相关的监听器的同时,:u6709: 关键方法:onServerStream,它主要是移除原newListener
,并设置stream监听动作,当stream
事件发生时,触发该方法,关于该方法的定义如下:function onServerStream(ServerRequest, ServerResponse, stream, headers, flags, rawHeaders)
针对Http2Session
对象所发出的动作,对其请求以及响应进行一个包装,分别包装称为Http2ServerRequest
以及Http2ServerResponse
对象,然后出发request
事件,并将这两个参数抛出去!
这里分析了关于Http2Server
服务对象的一个工作过程,:point_down: 整理出关于这个对象它的一个组成结构
通过 的组成结构图我们可以看出这个Http2Server
对象就是主要提供服务监听的作用!
提及到的几个对象:ServerHttp2Session
、Http2ServerRequest
、Http2ServerResponse
对象,都是用来做甚的呢?
Http2ServerRequest
- 模块组成
由此可见,这里的
Http2ServerRequest
对象可以说是客户端请求的一个包装器,包装了客户端请求的所有信息 - 定义与使用
由
createServer
方法创建,并由request
监听器作为第一个参数响应返回执行,可用来访问请求资源的状态、请求头、请求的数据等等 - 源码实现
继承于
Readable
可读流对象,其核心成员变量为stream
,实现对流的监听,将所有相关的属性都放到kStream
唯一属性变量中,以便于后续过程中使用
Http2ServerResponse
模块组成
定义与使用
服务器响应对象描述,一般由 的
Http2Server
内部来创建的,且可借由request
回调方法第二个参数来回调出去,代表着对服务端响应资源的监听,可监听到请求源对象、响应头、响应内容等资源,也可直接做响应动作,比如writeHeader、write等操作!源码实现
继承于
Stream
对象,其构造方法以一流对象作为参数,来进行包裹,且其req属性变量以这个流来作为参照,拥有关键属性kStream、kHeader、kResponse等关键对象属性! 这里的kStream
变量,指向的是ServerHttp2Stream
,所有的与流相关的操作,都借由它来实现的!!关键方法与属性 writeHead: 往请求资源中写入响应头信息,一般可以是响应code、响应信息、响应头三者一起写,也可以是仅写响应code信息!:warning: 该方法必须是在
response.write/end
方法调用之前调用,代表告知流要以什么解析协议来解析所传输的资源!const body = 'hello world'; response.writeHead(200, { 'Content-Length': Buffer.byteLength(body), 'Content-Type': 'text/plain; charset=utf-8' });
关于该方法的一个伪代码描述如 :
根据传入的参数进行header的组装,并存储在当前对象中的kHeader变量中 调用其成员属性kStream
的response
方法,由这个response
方法来发起响应动作 调用底层AsyncWrap
对象的response
方法完成最终的写入header动作!
关于 中的kStream
对象,究竟是怎样的一个对象呢?通过调试我们可以得知,其实这里的kStream
对象是一个ServerHttp2Stream
,那么关于这个ServerHttp2Stream
它又是怎样的一个对象呢?具体可以看下方的介绍
write/end: 与http1.x
模块中的write/end
方法相类似,定义如下:
不管是write或者是end方法,其底层各自调用的都是stream.Duplex
的write/end方法,且都在调用对应方法之前,如果还没有设置header的话,则根据待写入的数据格式,来生成对应的header信息
createPushResponse: 往请求资源来创建推送资源信息,实现向请求客户端资源的主动推送功能,然后当请求的资源如果被关闭的话,那么这时主动推送将抛出ERR_HTTP2_INVALID_STREAM
异常,而且这里第二个参数callback都会在推送动作完成时回调到!这里主要是从请求原始会话session
中捞出源会话,借助于ServerHttp2Stream
对象来完成这个push动作!
这里所推送的资源是headers类型的资源!一般仅需要在对应的header中指明对应的:path
属性代表对应的资源文件即可,也就是说,我们可以将这个资源通过这个headers来实现服务端的推送,但是,:u6709: 一个关键点的地方需要注意的是,因为实现了服务端的推送,因此,假如推送的是与当前资源同源的情况下的话,需要针对这个客户端的请求进行资源的响应处理操作,否则就会出现忽略这个资源的响应处理动作!!
const http = require('node:http2');
const fs = require('node:fs');
const server = http.createSecureServer({
key: fs.readFileSync('localhost-privkey.pem'),
cert: fs.readFileSync('localhost-cert.pem')
});
server.on('stream', (stream, headers, flags, rowsHeader) => {
stream.respond({
'content-type': 'text/html',
':status': 200
});
stream.pushStream({ ':path': 'https://lib.baomitu.com/zepto/0.8/zepto.min.js' }, (error, pushStream) => {
if (error) throw error;
pushStream.respond({
'content-type': 'text/javascript',
':status': 200
})
pushStream.end(`alert('you win')`)
})
stream.end('<script src="https://lib.baomitu.com/zepto/0.8/zepto.min.js"><h1>Hello World</h1>');
});
server.listen(8443, () => {
console.info('server listen 8443 port');
});
从 的访问输出结果,我们可以看出客户端正常请求并无请求额外的js资源,但是,我们可以在响应的内容中,追加一js标签,实现服务端推送的机制,其实,这里与普通的html没有什么太多的区别,只不过是通过追加的标签,来让浏览器识别到对应的标签之后,自动发起资源的加载!!!
ServerHttp2Session
- 模块组成
继承于
Http2Session
对象,主要是利用继承而来的属性+api来实现会话的管理的! - 定义与使用
在分析Http2Server的时候,就提及到这个
ServerHttp2Session
对象,那么这个对象 怎样的一个作用呢? 代表着一个连接请求会话,每次有新的一个连接请求过来时,都在Http2Server
中创建一个session会话,代表每一个客户端连接会话,在会话中所产生的基于流的操作,都将会触发server的stream动作(本质上是调用的emit('stream')动作) - 源码实现
在每一个客户端连接监听中,都创建了一个会话session,并设置相应的监听器(stream、error、priority),并触发session事件,也就是
Http2Server
被触发的原因
Http2Session
- 模块组成
- 定义与使用
const session = new ServerHttp2Session(options, socket, this);
代表http2
的客户端与服务器之间的活动通信会话!每个Http2Session
实例都表现出不同的行为,这取决于它是作为服务器运行还是客户端运行的,这可凭借它的type
属性来确定! 每一个Http2Session
实例都与其对应的socket对象关联,因为它正是以这个socket对象来作为参数来创建的对象,所有的基于该会话上的操作,都是用socket来完成的,一旦创建时候,就触发对应的connect
方法,当销毁时,也跟着一通销毁。 由于spdy
协议特定序列化和处理要求,不建议往这个socket对象上来写入数据,因为这会导致不确定的因素发生! - 源码实现
- 关键方法与属性 goaway(code[, lastStreamID[, opaqueData]]): 启动连接关闭动作,GOAWAY允许端点正常停止接收新的流,同时仍然完成对先前建立的流的处理,这可以实现管理操作,比如服务器维护!
ServerHttp2Stream
- 模块组成
- 定义与使用
继承于
Http2Stream
- 源码实现
- 关键方法与属性
Http2Stream
- 模块组成
- 定义与使用
继承于
Duplex
,全双工的流对象,Http2Stream 类的每个实例都代表一个通过 Http2Session 实例的双向 HTTP/2 通信流。任何单个Http2Session
在其生命周期内最多可以有231-1
个 Http2Stream 实例!
用户不会直接构造 Http2Stream
实例。相反,这些是通过 Http2Session
实例创建、管理和提供给用户代码的。在服务器上,创建 Http2Stream 实例以响应传入的 HTTP 请求(并通过 'stream' 事件传递给用户),或响应对 http2stream.pushStream() 方法的调用。在客户端,当调用 http2session.request()
方法或响应传入的“推送”事件时,会创建并返回 Http2Stream 实例。
Http2Stream
作为ServerHttp2Stream
以及ClientHttp2Stream
的父类,默认文本字符编码是 UTF-8。使用 Http2Stream 发送文本时,使用 'content-type' 标头设置字符编码。
stream.respond({
'content-type': 'text/html; charset=utf-8',
':status': 200
});
- 源码实现
- 关键方法与属性
http2各个模块的协作流程
瞅着http2模块中这么多的模块,以及 一些隐藏性的模块其中,那么他们之间是如何协同工作的呢???
http2服务端的工作流程