8. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP改造篇之HPACK原理

rust,手把手,编写,一个,wmproxy,代理,内网,穿透,http,改造,hpack,原理 · 浏览次数 : 24

小编点评

**索引** * **H** - 字符长度 (7+) * **Value** - 字符值 (长度) **字符串** * **H** - 字符长度 (7+) * **Value** - 字符值 (长度) **动态表** * **Index** - 索引值 (7+) * **Value** - 索引值的值 (长度) **请求方法** * **H** - 字符长度 (7+) * **Value** - GET 方法 **索引的两个前缀** * **01** - 表示不添加到动态表中 **请求的索引** * **0001** - 表示不添加到动态表中

正文

用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP改造篇之HPACK原理

项目 ++wmproxy++

gite: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

HTTP/2的简介

HTTP/1.1发表于1999年,该协议持续被使用到了至今
HTTP/2标准于2015年5月以RFC7540正式发表。由于HTTP2对1.1协议保持有高度的兼容,并且主要以字节传输,相比于1.1有更好的传输效率和更强大的传输能力,所以他快速流行起来

  • 在2017年5月,全球排名前1000万的网站中,有13.7%支持了HTTP/2。
  • 在2019年6月,全球有36.5%的网站支持了HTTP/2

而http/3的标准基于UDP协议的制定,而UDP在很多场合受限相对严重,由此可以得出http/1.1和http/2将会并存非常长的一段时间。所以我们需要对两种协议进行完全支持

库的选择

在这里我们选择自研的解析库 webparse和HTTP服务框架wenmeng,可以将服务转化成可以支持多协议的结构。此过程可能涉及到重写头信息,也可能添加头信息,会涉及到HTTP2的头部编码问题,所以必须将HTTP2的代码做完整的解析。

HTTP2的定义

HTTP2的RFC主要由7540和补充的7541,因为少量数据的请求如GET请求头占比100%,头部流量的优化变成了极为重要的性能提升点,所以很大的一部分把重点放在头部(7541)的优化。

HPACK: HTTP2的头部压缩

头部信息主要以下部分组成:
Header Field: 一个名称-值对。无论是名称还是值都由8位的字节组成,但名称限定为常见的编码(可解析成String),值可以为纯二进制。
Dynamic Table: 动态表是一个将存储的头部字段与索引值相关联的表。这个表是动态的,特定于一个编码或解码上下文。
Static Table: 静态表是一个静态地将频繁出现的头部字段与索引值相关联的表。这个表是有序的、只读的、始终可访问的,并且可以在所有编码或解码上下文之间共享。定义了一个常用的键值对以更好的压缩如(:method, GET)可由一个字节0x82表示
Header List: 一个头部列表是有序的头字段集合,这些头字段被联合编码并可以包含重复的头字段。一个完整的HTTP/2头部块中的头字段列表就是一个头部列表。如返回:status必须在其它的头前面,请求:method必须在其它前面,另一个因为需要动态添加到动态表里,如果前后顺序不一致的话,可能会导致动态表的映射值不一致,所以必须用列表的形式保证顺序。
Header Field Representation: 一个头字段可以在编码形式中表示为字面量或索引
Header Block: 一个有序的头字段表示列表,当解码时,会产生一个完整的头部列表。

动态表索引值

静态表和动态表如何结合成一个单独的索引地址空间。
在HPACK中,静态表和动态表都被结合到一个单独的索引地址空间中。
索引值在1和静态表长度之间(包括两端)指的是静态表中的元素。
索引值严格大于静态表长度指的是动态表中的元素。需要从索引中减去静态表的长度以找到动态表中的索引。
严格大于两个表长度之和的索引值必须被视为解码错误。
对于一个大小为s的静态表和大小为k的动态表,下面的图表展示了整个有效的索引地址空间。

<----------  索引地址空间 ---------->
<--      静态表   -->  <--    动态表     -->
+---+-----------+---+  +---+-----------+---+
| 1 |    ...    | s |  |s+1|    ...    |s+k|
+---+-----------+---+  +---+-----------+---+
                       ^                   |
                       |                   V
                新值插入位置      超出大小删除位置

如何索引

编码的头部字段可以表示为索引或文字。
索引表示将头部字段定义为静态表或动态表中的条目的引用。
文字表示通过指定其名称和值来定义头部字段。头部字段名称可以文字地表示或作为静态表或动态表中的条目的引用。头部字段值以文字形式表示。
定义了三种不同的文字表示:
将头部字段作为新条目添加到动态表开头的文字表示,如(custom-key, custom-value)。
不将头部字段添加到动态表的文字表示,如(:path, /wmproxy)。
不将头部字段添加到动态表,同时附加规定,此头部字段始终使用文字表示,尤其是当由中间人重新编码时。此表示旨在保护不因压缩而处于危险之中的头部字段值这些文字表示的选择可以通过安全考虑来指导,以保护敏感的头部字段值(如Authorization: xxxx),不能做任何的数据索引缓存。

头部字段名称或头部字段值的文字表示可以直接或使用静态Huffman编码对八位字节序列进行编码。

长度编码

整数被用于表示名称索引、头部字段索引或字符串长度。整数的表示可以在一个八位字节内的任何位置开始。为了允许优化的处理,整数的表示始终在八位字节的末尾完成。
整数由两部分表示:一个前缀,该前缀填充当前八位字节,以及一个可选的八位字节列表,当整数值不适合前缀时使用。前缀的位数(称为N)是整数表示的参数。

如果整数值足够小,即严格小于2^N-1,则在前缀的N位中对其进行编码。
可表示0-31的值,前面三位被用来做别的用途,如string的长度前缀为1,如索引头前缀为2

  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| ? | ? | ? |       Value       |
+---+---+---+-------------------+

图:前缀中编码的整数值(以N=5为例),动态表长度更新即N=5

否则,前缀的所有位均设置为1,并且使用一个或多个八位字节列表对值进行编码,该值减去2^N-1。每个八位字节的最高有效位用作续表标记:其值设置为1,列表中的最后一个八位字节除外。八位字节的其余位用于编码减小后的值。

  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| ? | ? | ? | 1   1   1   1   1 |
+---+---+---+-------------------+
| 1 |    Value-(2^N-1) LSB      |
+---+---------------------------+
               ...
+---+---------------------------+
| 0 |    Value-(2^N-1) MSB      |
+---+---------------------------+

图:前缀后编码的整数值(以N=5为例)

从八位字节列表解码整数值首先从列表中反转八位字节的顺序。然后,对于每个八位字节,删除其最高有效位。将八位字节的剩余位连接起来,并增加2N-1以获得整数值。

前缀大小N始终介于1和8位之间。以八位字节边界开始的整数具有8位前缀。

表示整数I的伪代码如下:

if I < 2^N - 1, encode I on N bits
else
    encode (2^N - 1) on N bits #即N位的1
    I = I - (2^N - 1)
    while I >= 128
         encode (I % 128 + 128) on 8 bits
         I = I / 128
    encode I on 8 bits

解码整数I的伪代码如下:

decode I from the next N bits
if I < 2^N - 1, return I
else
    M = 0
    repeat
        B = next octet
        I = I + (B & 127) * 2^M
        M = M + 7
    while B & 128 == 128
    return I

在HPACK中跟长度相关的编解码都用如上方式

字符串的表示

标题字段名称和标题字段值可以表示为字符串文字。字符串文字编码为一系列八进制数字,通过直接编码字符串文字的八进制数字或使用霍夫曼编码(参见[HUFFMAN])。

  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| H |    String Length (7+)     |
+---+---------------------------+
|  String Data (Length octets)  |
+-------------------------------+

图:字符串文字表示

字符串文字表示包含以下字段:

  • H:一个1位的标记H,指示字符串的八进制数字是否使用霍夫曼编码。如果1则表示用HUFFMAN编码,如果为0则普通编码
  • 字符串长度: 用于编码字符串文字的八进制数字的数量,编码为具有7位前缀的整数。
  • 字符串数据:字符串文字的编码数据。如果H为“0”,则编码数据是字符串文字的原始八进制数字。如果H为“1”,则编码数据是字符串文字的霍夫曼编码。

霍夫曼编码的优势:
在头信息中,可读信息出现的频率远大于不可读字节出现的概率,也就是把0-255字节按照频率出现在概率进行数据的重编码,如‘0’则编码成‘00000’,按照ASCII的方式如果表示0需要8位的编码,这里就可以节省(8-5)/8=37.5%的数据大小。

头信息索引

因为HTTP2是多次复用同一个连接,头基本上会相同,如Cookie,HTTP2会将常用的(静态编码)和常用的(动态编码)将其缓存下来,在下次发送的时候,只要发送一个字节接收方就可以知道对方发送的头文件信息。

  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 1 |        Index (7+)         |
+---+---------------------------+

图: 头信息索引,Index为长度编码,遵循前缀1的长度编码

增量索引的文字头部字段

增量索引文字头部字段表示法将一个头部字段附加到解码后的头部列表,并将其作为新条目插入到动态表中。

  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 1 |      Index (6+)       |
+---+---+-----------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+

表示名字是索引,即Name命中,如:path命中,值为新的值,添加到动态表里

  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 1 |           0           |
+---+---+-----------------------+
| H |     Name Length (7+)      |
+---+---------------------------+
|  Name String (Length octets)  |
+---+---------------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+

表示名字和值均没有在原有的表里,现将name和value添加到动态表里。

索引的两个前缀必须为01,如01000010十六进制写法则是0x82,<61,查静态表可得(:method, GET),即我们通过一字节表示请求方法为GET。

以下两种情况前缀为0001开头的则表示不添加到动态表中。

  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 |  Index (4+)   |
+---+---+-----------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+

不进行索引,但是从表中查Name,如authorization。

  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 |       0       |
+---+---+-----------------------+
| H |     Name Length (7+)      |
+---+---------------------------+
|  Name String (Length octets)  |
+---+---------------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+

从不进行索引

更新动态表的长度

动态表大小更新表示动态表大小发生了变化。

  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 1 |   Max size (5+)   |
+---+---------------------------+

可以设置新的最大的动态表长度。

至此HTTP2的头部压缩协议已基本了解完成,下一章将进行具体示例的分析。

与8. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP改造篇之HPACK原理相似的内容:

8. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP改造篇之HPACK原理

用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP改造篇之HPACK原理 项目 ++wmproxy++ gite: https://gitee.com/tickbh/wmproxy github: https://github.com/tickbh/wmproxy HTTP/2的

7. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP及TCP内网穿透原理及运行篇

用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP及TCP内网穿透原理及运行篇 项目 ++wmproxy++ gite: https://gitee.com/tickbh/wmproxy github: https://github.com/tickbh/wmproxy 内网、公

Dubbo3应用开发—XML形式的Dubbo应用开发和SpringBoot整合Dubbo开发

Dubbo3程序的初步开发 Dubbo3升级的核心内容 易⽤性 开箱即⽤,易⽤性⾼,如 Java 版本的⾯向接⼝代理特性能实现本地透明调⽤功能丰富,基于原⽣库或轻量扩展即可实现绝⼤多数的 微服务治理能⼒。更加完善了多语言支持(GO PYTHON RUST) 超⼤规模微服务实践 ⾼性能通信(Tripl

桌面版vscode用免费的微软4核8G服务器做远程开发(编译运行都在云上,还能自由创建docker服务)

GitHub的Codespaces为个人用户提供了免费的4核8G服务器资源,今天就来实战如何用桌面版vscode连接codespace服务器做远程开发,把编译运行下载等耗时耗资源的操作都转移到云端进行,还能为应用创建各种docker服务,这都不要钱!

看我是如何用C#编写一个小于8KB的贪吃蛇游戏的

译者注:这是Michal Strehovský大佬的一篇文章,他目前在微软.NET Runtime团队工作,主要是负责.NET NativeAOT功能的开发。我在前几天看到这篇文章,非常喜欢,虽然它的内容稍微有点过时(还是使用的.NET Core 3.0),不过其中的一些编程技巧和思维方式很受用,特

[转帖]阿里规范 - 五、MySQL 数据库 - (一)建表规约 - 8 - 【强制】varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长 度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索 引效率。

字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索 引效率。 1、因为mysql 是行存储模式,所以会把整行读取出来。text 储存了大量的数据。读取时,占了大量的io。所以会十分的慢。 2、每行的数据过大 行溢出 InnoDB 会将一些大对象数据存放在数据页之外的 BLOB 页

2024-04-27:用go语言,在一个下标从 1 开始的 8 x 8 棋盘上,有三个棋子,分别是白色车、白色象和黑色皇后。 给定这三个棋子的位置,请计算出要捕获黑色皇后所需的最少移动次数。 需要注意

2024-04-27:用go语言,在一个下标从 1 开始的 8 x 8 棋盘上,有三个棋子,分别是白色车、白色象和黑色皇后。 给定这三个棋子的位置,请计算出要捕获黑色皇后所需的最少移动次数。 需要注意的是,白色车可以垂直或水平移动,而白色象可以沿对角线移动,它们不能跳过其他棋子。 如果白色车或白色象

基于SpringBoot应⽤的logback⽇志配置

SpringBoot默认整合了logback-classic⽇志框架,我们需要对logback⽇志框架进⾏配置 以⾃定义⽇志输出格式、⽇志⽂件配置、⽇志⽂件保存策略等信息

Typora 最新中文版安装破解V1.4.8

Typora中文破解版是一款好用极简免费的跨平台Markdown编辑器,软件使用这款软件能够帮助用户轻松将文本转换到HTML,软件从底层向上设计,软件支持markdown的标准语法,同时这款软件还支持动态预览功能,一键预览,让一切都变得如此干净、纯粹,是一款不可多得的优质markdown编辑器。 T

文心一言 VS 讯飞星火 VS chatgpt (102)-- 算法导论9.3 8题

八、用go语言,设 X[1..n]和 Y[1..n]为两个数组,每个都包含n个有序的元素。请设计一个 O(lgn)时间的算法来找出数组 X和Y中所有 2n 个元素的中位数。 文心一言: 要在 O(lgn) 时间内找出两个有序数组 X 和 Y 中所有元素的中位数,可以使用二分查找算法。以下是用 Go