什么是HTTP2
HTTP/2是HTTP协议的一个更新版本,是继HTTP/1.1之后的第二个正式版本。它在2015年发布,并且目前被广泛用于现代Web应用和网站。HTTP/2旨在提供更高效、更快速、更安全的网络传输协议,改进了HTTP/1.1中的一些性能瓶颈和安全性问题。
HTTP/2相较于HTTP/1.1引入了一些重要的特性:
- 多路复用(Multiplexing):HTTP/2使用二进制帧(Binary Frames)在同一个TCP连接上同时传输多个HTTP请求和响应,从而允许多个请求和响应并行传输,解决了HTTP/1.x中的队头阻塞问题。
- 头部压缩(Header Compression):HTTP/2使用HPACK算法对HTTP头部信息进行压缩,减小头部数据的大小,从而节省带宽和降低延迟。
- 服务器推送(Server Push):HTTP/2支持服务器推送功能,允许服务器在客户端发出请求后主动将与请求相关的资源推送给客户端,减少了客户端的请求次数,提高了页面加载速度。
- 流量控制(Flow Control):HTTP/2引入了流量控制机制,通过WINDOW_UPDATE帧来通知对方更新流或连接的流量控制窗口大小,确保数据传输的平衡和稳定。
- 优先级(Priority):HTTP/2支持请求的优先级管理,可以为不同的请求设置优先级,确保重要请求得到优先处理。
HTTP/2的目标是提高性能和效率,通过多路复用和头部压缩等技术,在相同的网络资源条件下实现更快的页面加载速度和更高的吞吐量。HTTP/2在现代Web应用和网站中得到了广泛的支持和应用,逐渐成为主流的HTTP协议版本。
why not HTTP/1.1?
- 冗余文本过多,导致传输体积很大作为一款经典的无状态协议,它使得Web后端可以灵活地转发、横向扩展,但其代价是每个请求都会带上冗余重复的Header,这些文本内容会消耗很多空间,和更快传输的目标相左。
- 并发能力差,网络资源利用率低HTTP1.1 是基于文本的协议,请求的内容打包在header/body中,内容通过\r\n来分割,同一个TCP连接中,无法区分request/response是属于哪个请求,所以无法通过一个TCP连接并发地发送多个请求,只能等上一个请求的response回来了,才能发送下一个请求,否则无法区分谁是谁。
区分不同数据是来自哪个请求是应用层应该做的事情,而传输层的职责为:传输层负责端到端的数据传输,提供可靠的数据传输服务。 这一点我们也可以从tcp报文段的格式中可以看出来。
TCP 报文段只是负责将数据进行可靠传输,并不负责对数据进行解析或区分。没有在 TCP 报文段头部添加专门用于标识请求的字段。
而 HTTP/1.x 设计时采用了简单的请求-响应模型,每个请求必须等待前一个请求的响应返回,所以导致HTTP/1.x并发能力差。
HTTP2
HTTP/2是HTTP协议的一个更新版本,它在数据传输方面引入了一些新的特性和改进。HTTP/2的传输数据结构主要建立在二进制帧(Binary Frame)的基础上。这些帧是HTTP/2中最小的数据传输单位,通过帧来传输数据和控制信息。
这些帧的二进制格式是HTTP/2中数据传输的基础,通过对这些帧的组合和解析,HTTP/2实现了多路复用(multiplexing)、流的优先级管理、头部压缩等功能。
HTTP/1.1引入了持久连接,允许在一个TCP连接上发送多个请求和响应,而不需要在每个请求之后关闭连接。这样可以减少连接建立和关闭的开销,提高性能。
HTTP/1.1的持久连接就是http2的多路复用吗
- HTTP/1.1的持久连接(persistent connection)允许在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。
- HTTP/2的多路复用(multiplexing)允许同时通过单一的HTTP/2连接发起多重的请求-响应消息。这意味着,在HTTP/2中,多个请求可以在同一个连接上并行执行,而不会像HTTP/1.1那样阻塞。
下图展示了两者之间的主要区别。
注意并发和并行的区别,这里就不再说明了。
HTTP2的帧结构
HTTP2帧结构,其中帧头为固定的9个字节,最重要的是加入了流标识符来区分不同的http请求。
这样在同一个tcp连接中,就可以通过流标识符唯一确定该帧是属于哪个HTTP请求,在到达目标端后再更具每个HTTP请求的流标识符的顺序重新组装数据,如此一来很大程度上解决了上文所说的HTTP/1并发度差、可能导致队头阻塞的问题。
HPACK算法
HPACK算法的主要概念包括:
- 静态字典(Static Table):HPACK定义了一个固定的静态字典表,包含了常见的HTTP头部字段和值,这些字典表在编码和解码过程中可以被重用。静态字典表是不可修改的,旨在提供一个共享的、预定义的标准头部信息集合。
- 动态表(Dynamic Table):HPACK还引入了一个动态表,用于存储请求和响应中额外的、特定于当前会话的头部字段和值。动态表可以在编码和解码过程中动态地添加和删除项目。
- 哈夫曼编码(Huffman Coding):哈夫曼编码是一种变长编码方式,它根据出现频率将不同的头部字段和值映射到不同长度的二进制编码,从而使得常用的头部信息占用较少的比特数,而不常用的头部信息占用更多的比特数。这样可以有效地减小头部数据的大小。
静态表
将高频使用的Header编成一个静态表,每个header对应一个数组索引,每次只用传这个索引,而不是冗长的文本。表总共有61项,下图是前30项
传 3 代表 "POST",这用一个字节表示了原来4个字节 传28代表content-length,这用一个字节表示了原来14个字节
动态表
动态表,用于存储请求和响应中额外的、特定于当前会话的头部字段和值。动态表可以在编码和解码过程中动态地添加和删除项目。
动态表会将每次重复发送的冗长的cookie或者服务端常见的Authorization字段也加入动态表中。
我们第一次访问时发送的字段长度和http/1是类似的,这是因为动态表中还没有将一些特定的字段缓存下来。 访问成功后,response中就会带着已缓存字段的名称和在动态表中的序号返回发送端,同样的在发送端也会维护一张动态表并把这些索引加入其中,当再次发送时只需要发送序号即可,这样就完成了动态字段的压缩。
由于动态表是客户端和服务器共同维护的,因此它们在传输过程中可以根据自己的需求来更新动态表,确保其中包含当前会话中所需的头部信息。这种共同维护的动态表能够进一步提高头部压缩的效率,减少了重复的头部信息传输,提高了HTTP/2的性能和效率。
哈夫曼编码
哈夫曼编码是一种变长编码方式,它根据头部字段和值的出现频率来构建一个最优的编码表,使得常用的头部信息用较少的比特表示,而不常用的信息则用较多的比特表示。
具体的哈夫曼编码过程如下:
- 统计出现频率:首先,对于HTTP头部字段和值,统计它们在一定范围内出现的频率。频率越高的字段和值,将被赋予较短的哈夫曼编码。
- 构建哈夫曼树:根据统计的频率构建哈夫曼树,哈夫曼树是一个二叉树结构,其中叶子节点表示头部字段和值,而非叶子节点表示编码的中间节点。
- 生成哈夫曼编码表:从哈夫曼树的根节点出发,沿着左子树走为0,沿着右子树走为1,依次遍历所有叶子节点,得到对应的哈夫曼编码表。
- 编码头部字段和值:使用生成的哈夫曼编码表,将头部字段和值编码为二进制序列。由于哈夫曼编码是变长编码,常用的头部信息会被编码为较短的比特序列,不常用的信息则会被编码为较长的比特序列。
- 解码头部字段和值:在解码时,根据生成的哈夫曼编码表,将二进制序列解码为原始的头部字段和值。
通过使用哈夫曼编码,HTTP/2头部压缩可以将头部信息压缩为更紧凑的二进制格式,从而减小了头部数据的大小,节省了带宽和降低了延迟。哈夫曼编码在HTTP/2的头部压缩中起到了至关重要的作用,使得头部压缩的效率得到了显著的提升。
抓包验证HPACK算法
func main() {
conn, err := grpc.Dial("localhost:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
logrus.WithError(err).Fatalln()
}
client := pb.NewIUserClient(conn)
ctx, cancelFunc := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancelFunc()
bob := &pb.User{
Id: 111,
Name: "Bob",
Addr: "bei jing road",
Age: 30,
}
userInfo, _ := client.GetUserInfo(ctx, bob)
fmt.Println(userInfo)
sam := &pb.User{
Id: 222,
Name: "sam",
Addr: "shanghai road",
Age: 40,
}
info, _ := client.GetUserInfo(ctx, sam)
fmt.Println(info)
}
在client端,我们调用GetUserInfo方法向localhost:8080重复发送了两次User对象。
func (m MyServer) GetUserInfo(ctx context.Context, user *pb.User) (*pb.UserInfo, error) {
userInfo := new(pb.UserInfo)
userInfo.Id = user.Id
userInfo.QqNumber = "2374895283"
userInfo.WechatNumber = "8529834598"
return userInfo, nil
}
在服务端,我们接受User对象并返回UserInfo对象
抓包工具:wireshark,调整为本地回环模式。
第一次header大小:86字节
后续header大小: 只有15字节
验证动态表的存在
已知header一共有8个
在第一次传输时,明显看到最后两个header并不存在Index字段:
在第二次传输时,Index字段出现:
而且为什么是从62开始呢?记得我上文说过静态表的长度只有61,会不会动态表的编号是紧接着静态表后开始呢?经查证,确实如此。 原文:RFC 7541: HPACK: Header Compression for HTTP/2 (rfc-editor.org)
2.3.3. Index Address Space
The static table and the dynamic table are combined into a single
index address space.
Indices between 1 and the length of the static table (inclusive)
refer to elements in the static table (see Section 2.3.1).
Indices strictly greater than the length of the static table refer to
elements in the dynamic table (see Section 2.3.2). The length of the
static table is subtracted to find the index into the dynamic table.
RFC文档中也说了静态表和动态表虽然在索引上是连续的但并不是共用同一块空间
动态表的存储结构大概类似于队列