各位观众老爷,大家好!今天咱们来聊聊HTTP/2的Frame解析和二进制协议分析,这玩意儿听起来高大上,其实扒开皮儿,也就是一堆0和1,外加一些精心设计的格式而已。别怕,我会尽量用大白话,加上一些代码示例,让大家都能听明白。
开场白:HTTP/2 是个啥?
HTTP/1.1用了这么多年,有些老态龙钟了。队头阻塞、头部冗余等等问题,让网页加载速度慢吞吞的。HTTP/2就是为了解决这些问题而生的。它最大的特点就是使用了二进制协议,并且引入了多路复用、头部压缩等机制。
啥是二进制协议?
简单来说,就是把原本的文本协议,变成了0和1组成的二进制流。 这样做的好处是:
- 更高效: 计算机处理二进制数据效率更高。
- 更紧凑: 二进制数据可以更节省空间。
- 更易解析: 结构化的二进制数据更容易解析。
Frame:HTTP/2 的基本单位
在HTTP/2中,所有的数据都被封装在Frame中传输。 Frame就像一个个小包裹,每个包裹都有自己的类型和数据。理解Frame的结构,是理解HTTP/2的关键。
Frame 的结构
每个Frame都由一个9字节的Header和一个Payload组成。
字段 | 长度(字节) | 描述 |
---|---|---|
Length | 3 | Payload的长度。不包括Header的9个字节。 |
Type | 1 | Frame的类型。例如DATA、HEADERS、PRIORITY、RST_STREAM、SETTINGS、PUSH_PROMISE、PING、GOAWAY、WINDOW_UPDATE、CONTINUATION等。 |
Flags | 1 | 与Frame类型相关的标志位。不同的Frame类型,Flags的含义也不同。 |
Stream Identifier | 4 | 流ID。用于标识Frame所属的流。Stream ID为0的Frame用于控制连接,不属于任何流。 |
Payload | Length | 实际的数据。Payload的内容取决于Frame的类型。 |
Frame 类型详解
HTTP/2 定义了多种 Frame 类型,每种类型都有不同的作用。 咱们挑几个常用的讲讲:
- DATA Frame: 用于传输HTTP消息的Body。
- HEADERS Frame: 用于传输HTTP消息的Header。 可以包含完整的header块,也可以只包含一部分,后续用CONTINUATION Frame继续传输。
- SETTINGS Frame: 用于协商连接级别的配置参数,例如最大并发流数、头部压缩算法等。
- WINDOW_UPDATE Frame: 用于流量控制。
- RST_STREAM Frame: 用于重置某个流。
- GOAWAY Frame: 用于关闭连接。
用 JavaScript 解析 Frame
理论讲完了,咱们来点实际的。用JavaScript写一个简单的Frame解析器。
// 定义一个BufferReader类,方便读取二进制数据
class BufferReader {
constructor(buffer) {
this.buffer = buffer;
this.offset = 0;
}
readUInt8() {
const value = this.buffer.readUInt8(this.offset);
this.offset += 1;
return value;
}
readUInt16BE() {
const value = this.buffer.readUInt16BE(this.offset);
this.offset += 2;
return value;
}
readUInt24BE() {
// JavaScript没有直接读取24位整数的方法,需要手动处理
return (this.readUInt8() << 16) | this.readUInt16BE();
}
readUInt32BE() {
const value = this.buffer.readUInt32BE(this.offset);
this.offset += 4;
return value;
}
readBytes(length) {
const value = this.buffer.slice(this.offset, this.offset + length);
this.offset += length;
return value;
}
eof() {
return this.offset >= this.buffer.length;
}
}
function parseFrame(buffer) {
const reader = new BufferReader(buffer);
const length = reader.readUInt24BE();
const type = reader.readUInt8();
const flags = reader.readUInt8();
const streamIdentifier = reader.readUInt32BE();
const payload = reader.readBytes(length);
return {
length,
type,
flags,
streamIdentifier,
payload,
};
}
// 示例:解析一个简单的DATA Frame
const dataFrameBuffer = Buffer.from([
0x00, 0x00, 0x0a, // Length: 10 bytes
0x00, // Type: DATA (0x00)
0x00, // Flags: 0x00
0x00, 0x00, 0x00, 0x01, // Stream Identifier: 1
0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, // Payload: "Hello World" (10 bytes)
]);
const frame = parseFrame(dataFrameBuffer);
console.log(frame);
// 输出:
// {
// length: 10,
// type: 0,
// flags: 0,
// streamIdentifier: 1,
// payload: <Buffer 48 65 6c 6c 6f 20 57 6f 72 6c>
// }
console.log(frame.payload.toString()); // 输出: Hello World
这段代码定义了一个 parseFrame
函数,它接收一个Buffer对象作为参数,然后按照Frame的结构,读取各个字段的值。 为了方便读取Buffer,还定义了一个BufferReader
类。
更深入的解析:针对不同Frame类型的处理
上面的代码只是一个简单的框架,只能解析Frame的通用头部。 实际上,不同的Frame类型,Payload的结构也不同,需要针对不同的类型进行不同的处理。
例如,对于SETTINGS
Frame,Payload包含一系列的设置参数,每个参数由一个ID和一个Value组成。
const SETTINGS_FRAME_TYPE = 0x04;
function parseSettingsFramePayload(payload) {
const reader = new BufferReader(payload);
const settings = [];
while (!reader.eof()) {
const identifier = reader.readUInt16BE();
const value = reader.readUInt32BE();
settings.push({ identifier, value });
}
return settings;
}
function parseFrame(buffer) {
const reader = new BufferReader(buffer);
const length = reader.readUInt24BE();
const type = reader.readUInt8();
const flags = reader.readUInt8();
const streamIdentifier = reader.readUInt32BE();
const payload = reader.readBytes(length);
let parsedPayload = payload; // 默认情况
if (type === SETTINGS_FRAME_TYPE) {
parsedPayload = parseSettingsFramePayload(payload);
}
return {
length,
type,
flags,
streamIdentifier,
payload: parsedPayload,
};
}
// 示例:解析一个SETTINGS Frame
const settingsFrameBuffer = Buffer.from([
0x00, 0x00, 0x06, // Length: 6 bytes
0x04, // Type: SETTINGS (0x04)
0x00, // Flags: 0x00
0x00, 0x00, 0x00, 0x00, // Stream Identifier: 0
0x00, 0x01, 0x00, 0x00, 0x10, 0x00 // Identifier: 1, Value: 4096
]);
const frame2 = parseFrame(settingsFrameBuffer);
console.log(frame2);
// 输出:
// {
// length: 6,
// type: 4,
// flags: 0,
// streamIdentifier: 0,
// payload: [ { identifier: 1, value: 4096 } ]
// }
这段代码添加了一个 parseSettingsFramePayload
函数,用于解析 SETTINGS
Frame 的 Payload。 在 parseFrame
函数中,根据Frame的类型,选择不同的解析函数。
HPACK:HTTP/2 的头部压缩
HTTP/2 使用 HPACK 算法来压缩头部,减少头部的大小。 HPACK 使用了静态表、动态表和Huffman编码等技术。
HPACK 的原理比较复杂,这里只简单介绍一下:
- 静态表: 预定义了一些常用的头部字段和值,例如
:method: GET
、:status: 200
等。 - 动态表: 客户端和服务器端各自维护一个动态表,用于存储最近使用的头部字段和值。
- Huffman编码: 使用Huffman编码对头部字段和值进行压缩。
解析 HPACK 编码的头部,需要使用专门的 HPACK 解码器。 这里就不写代码了,因为比较复杂,可以使用现成的库,例如 hpack.js
。
流量控制
HTTP/2 引入了流量控制机制,防止客户端或服务器端被大量数据淹没。 流量控制基于 Window Update 的概念。 每个流都有一个窗口大小,表示可以接收的数据量。 当接收方处理完数据后,会发送 WINDOW_UPDATE
Frame,增加窗口大小。
多路复用
多路复用是 HTTP/2 最重要的特性之一。 它允许在一个TCP连接上同时传输多个HTTP请求和响应。 每个请求和响应都被分配一个唯一的 Stream ID。 客户端和服务器端可以通过 Stream ID 来区分不同的请求和响应。
总结
HTTP/2 是一个复杂的协议,但只要理解了Frame的结构、HPACK 头部压缩、流量控制和多路复用等核心概念,就能更好地理解HTTP/2的工作原理。 希望今天的讲解对大家有所帮助。
彩蛋:如何抓包分析 HTTP/2 流量
可以使用 Wireshark 等抓包工具来分析 HTTP/2 流量。 Wireshark 提供了 HTTP/2 的协议解析器,可以方便地查看 Frame 的内容。 还可以使用 Chrome 浏览器的开发者工具,查看 HTTP/2 的请求和响应头部。
今天的讲座就到这里,感谢大家的观看! 下次有机会再和大家聊聊 HTTP/3 (QUIC) 的相关知识。