深度剖析HTTP2原理以及 Golang 实战

HTTP/2 工作原理及 Go 语言实现

在当今的互联网时代,网络性能对于应用程序的成功至关重要。随着用户对快速响应和高效数据传输的期望不断提高,HTTP 协议也在持续演进。HTTP/2 作为 HTTP 协议的重大升级,带来了显著的性能提升。本文将深入探讨 HTTP/2 的工作原理,并通过 Go 语言实战,展示如何在实际项目中应用 HTTP/2。

HTTP/2优势

HTTP/2相比HTTP/1.1是一次重大升级,如今已经成为各处的默认标准。如果你曾用Chrome开发者工具查看网络请求,大概率已经见过HTTP/2连接的运行状态。

用Chrome检查HTTP/2连接

那么HTTP/2为什么如此重要?HTTP/1.1又有什么问题?

HTTP/1.1确实引入了管道机制,这个设计从理论上看很完美。

工作原理

  • 请求发送: 客户端按照顺序将多个请求报文发送到服务器,这些请求报文会在同一个 TCP 连接的输出缓冲区中排队等待发送。发送时不需要等待前一个请求的响应回来,就可以发送下一个请求,实现了请求的 “流水线” 式发送。
  • 服务器处理: 服务器接收到请求后,会按照请求到达的顺序进行处理。它会逐个解析请求,执行相应的操作,并生成响应。
  • 响应返回: 服务器将处理后的响应按照请求的顺序返回给客户端。客户端则按照发送请求的顺序来接收和处理响应,确保每个请求都能得到正确的响应。

HTTP/1.1管道机制:顺序处理请求

问题在于请求必须按顺序发出,响应也必须按相同顺序返回。如果某个响应延迟了——比如服务器需要额外时间处理——队列中的其他所有请求都得等着。

如果网络出现”小故障”导致某个请求延迟,也会出现这种情况。整个响应管道会停滞,直到那个延迟的请求处理完成。

存在的问题

  • 队头阻塞: 是 HTTP/1.1 管道机制的主要问题。如果一个请求在服务器端处理时间过长,或者由于网络等原因导致该请求的响应迟迟未返回,那么后续的请求即使已经处理完成,也需要等待前面的请求响应返回后才能被客户端接收和处理,这就导致了后续请求的阻塞,影响了整体的性能。
  • 依赖顺序: 请求和响应必须严格按照顺序进行处理,这在一定程度上限制了并行处理的能力,也增加了实现的复杂性和出错的可能性。如果某个请求的响应丢失或损坏,可能会导致后续的响应处理出现混乱。

HTTP/1.1中的队头阻塞

这个问题就是所谓的队头阻塞(Head-of-Line blocking)。

为了绕过这个限制,HTTP/1.1客户端(比如你的浏览器)开始对同一服务器建立多个TCP连接,让请求能够更自由、并发地流动。

虽然这个方法可行,但效率并不高:

  • 更多连接意味着客户端和服务器端都要消耗更多资源
  • 每个连接都要经过TCP握手过程,增加了额外延迟

“那么,HTTP/2解决这个问题了吗?”

大部分解决了,但是没有彻底解决.😂

HTTP/1.1的管道机制允许在同一个TCP连接中发送多个请求而无需等待每个请求的响应,但它存在队头阻塞等问题。HTTP/2通过以下几种方式解决了HTTP/1.1管道机制存在的问题:

  • 多路复用

    • 原理:HTTP/2允许在一个连接上同时发送多个请求和响应,这些请求和响应可以交错发送,并且可以乱序到达。它通过将每个请求和响应分割成多个帧,并为每个帧分配一个流ID,使得客户端和服务器能够根据流ID将帧重新组装成完整的请求和响应。
    • 解决问题:在HTTP/1.1管道机制中,如果一个请求阻塞了,后续请求即使不需要等待前面请求的资源也会被阻塞。而多路复用使得多个请求和响应能在同一个连接上并发进行,避免了队头阻塞问题,大大提高了连接的利用率和性能。
  • 二进制分帧

    • 原理:HTTP/2采用二进制格式传输数据,将数据分割为一个个的帧,每个帧都有特定的格式和标识。帧可以包含请求头、请求体、响应头、响应体等不同类型的数据。
    • 解决问题:相比HTTP/1.1基于文本的格式,二进制分帧更加紧凑和高效,能够更精确地控制数据的传输和处理。在管道机制中,基于文本的格式可能会导致数据解析的复杂性和不确定性,而二进制分帧使得数据的解析更加快速和准确,有助于实现多路复用和提高传输效率。
  • 首部压缩

    • 原理:HTTP/2使用HPACK算法对请求和响应的首部进行压缩。它会对首部字段进行编码,将重复的首部字段缓存起来,只发送一次,后续相同的字段通过索引来引用。
    • 解决问题:在HTTP/1.1管道机制下,每个请求都需要完整地发送首部信息,这会占用大量的带宽,尤其是在发送多个请求时,相同的首部字段会被重复传输。首部压缩减少了首部数据的传输量,提高了数据传输的效率,节省了带宽,也间接减少了因首部传输而可能导致的阻塞问题,使管道中的请求和响应能够更快速地进行。
  • 服务器推送

    • 原理:服务器可以根据客户端的请求,主动将客户端可能需要的资源提前推送给客户端,而不需要客户端再次发送请求。服务器通过分析页面的资源依赖关系,将相关的CSS、JavaScript、图片等资源主动推送到客户端缓存。
    • 解决问题:在HTTP/1.1管道机制中,客户端需要逐个请求所需的资源,这可能会导致多次往返延迟。服务器推送使得客户端能够提前获取到后续可能需要的资源,减少了客户端的请求次数,避免了因等待资源而造成的阻塞,提高了页面的加载速度和用户体验。

单一连接上的多流数据帧

虽然 HTTP/2 在性能优化方面取得了显著进步,大幅提升了网络传输效率,但由于其仍基于 TCP 协议运行,在某些情况下仍无法完全避免队头阻塞问题。

在传输层,TCP 协议严格遵循按序交付原则,即数据必须按发送顺序传递给应用层。如果某个数据包丢失或传输延迟,TCP 会暂停所有后续数据包的传输,直到该问题得到解决。只有当延迟的数据包成功到达后,TCP 才能按正确顺序将之前排队的数据包交付给 HTTP/2 层或应用层。

这种机制导致一个显著缺点,即使其他数据流已经准备就绪并可立即处理,由于延迟的数据流,服务器仍需等待,限制了数据处理的并行性和实时性,影响整体传输效率。

为了突破 TCP 协议的这些限制,实现更高效、更可靠的网络传输,我们需要关注基于 UDP(用户数据报协议)的新型协议,如 QUIC(快速 UDP 互联网连接)。QUIC 的出现是 HTTP/3 发展的核心驱动力,为解决 TCP 协议的固有问题提供了新的思路和解决方案。

当然,HTTP/2 的重要性不容忽视。它不仅成功解决了 HTTP/1.1 的诸多问题,如队头阻塞和首部传输效率低下,还为网络应用的发展开辟了新的可能性。接下来,让我们深入探讨 HTTP/2 的内部机制,了解其如何实现这些性能提升和功能优化。

HTTP/2 工作原理

在客户端开始建立 TLS 连接时,首先会发送一个 ClientHello 消息,其中包含了 ALPN(应用层协议协商)扩展。这个扩展的重要作用是列出客户端支持的协议列表。通常,该列表会包括 HTTP/2 的 “h2”(客户端优先选择的协议)、”http/1.1”(作为备用方案),以及其他可能使用的协议,以满足多样化的网络需求。

服务器端的 TLS 协议栈在接收到这个协议列表后,会迅速将其与自身支持的协议进行匹配。如果双方都支持 “h2” 协议,服务器将在 ServerHello 响应中确认这一选择,从而在协议层面达成一致,为后续的高效通信奠定基础。

接下来,TLS 握手按常规流程继续,包括设置加密密钥,通过加密算法生成用于数据加密和解密的密钥对,以确保数据传输的安全性。此外,还会验证数字证书,以确认通信双方的身份真实性和合法性,防止中间人攻击等安全威胁。

连接序言

TLS 握手完成后,客户端会发送一个连接序言,起始为一个特殊的 24 字节序列:PRI*HTTP/2.0\r\n\r\nSM\r\n\r\n。这一序列用于明确确认正在使用 HTTP/2 协议,类似于一个独特的身份标识,让服务器能迅速识别和响应。在此阶段,数据的压缩和分帧操作尚未开始,一切都在为高效的数据传输做准备。

在发送连接序言之后,客户端会立即发送一个 SETTINGS 帧。这不是普通的数据帧,而是与任何流无关的连接级控制帧。它相当于客户端发送给服务器的“偏好设置清单”,其中包括重要的配置信息,如流量控制选项,通过合理设置流量控制参数以确保数据传输的稳定性,避免网络拥塞;以及最大帧大小等配置,这些参数对于优化数据传输效率和提升网络性能至关重要。

服务端与客户端交换SETTINGS帧

当服务器准确理解客户端的意图后,会迅速做出响应,并发送自己的连接序言,其中包含一个 SETTINGS 帧。这一帧承载着服务器的配置信息,与客户端的 SETTINGS 帧相呼应,完成连接参数的协商。当双方完成 SETTINGS 帧的交换,连接就成功建立,客户端和服务器之间的通信通道正式搭建,为后续的数据交互做好了准备。

客户端准备发送请求时,会创建一个带有唯一标识符的新数据流,这个标识符称为流 ID。这里有一个有趣的设计规则:客户端发起的数据流 ID 始终为奇数,如 1、3、5 等。你可能会好奇,为什么流 ID 要采用奇数,而不是 1、2、3 这样的连续编号呢?这背后有着精妙的设计:

  • 奇数流 ID 专门分配给客户端发起的请求,这使得系统能快速识别请求的发起方,在复杂的网络通信中明确数据流向。
  • 偶数流 ID 则保留给服务器使用,通常用于服务器推送等由服务器端发起的功能。这种分配方式有效避免了客户端和服务器在数据流标识上的冲突,确保了数据传输的有序性。
  • 流 ID 0 是特殊的,仅用于连接级别(非流级别)的控制帧。这些控制帧对整个连接起到全局性控制作用,比如设置连接参数、管理连接状态等,在维护连接的稳定性和高效性方面发挥关键作用。

当数据流准备就绪,客户端会发送一个 HEADERS 帧。这个帧包含所有请求所需的头部信息,功能类似于 HTTP/1.1 中的请求行和头部信息(例如 GET / HTTP/1.1 及其后续内容)。然而,在 HTTP/2 中,头部的结构和传输方式有了显著变化:

  • 结构:HTTP/2 引入了伪头部字段,用于定义请求方法、路径和状态等关键信息,为请求提供更清晰的语义描述。在伪头部字段之后,是常规头部字段,如 User-AgentContent-Type 等,它们补充了请求的细节信息。
  • 传输:HTTP/2 使用 HPACK 算法对头部信息进行压缩,将冗长的文本格式头部转换为紧凑的二进制格式进行传输。这一改进大大减少了头部数据的传输量,提高了传输效率,降低了网络带宽的占用,使数据能更快速地在客户端和服务器之间传递。

“伪头部?HPACK 压缩?这都是些什么?” 许多人在看到这些专业术语时可能会感到困惑。别担心,接下来我们将揭开它们的神秘面纱,从理解伪头部字段开始。

如果你经常使用 Chrome 的开发者工具或其他网络检查工具来分析 HTTP 请求,那么伪头部字段可能会让你感到似曾相识。在 HTTP/2 的协议体系中,伪头部字段是一种独特的设计,它巧妙地将特殊头部与常规头部分离。像 :method(用于表示请求方法,如 GET、POST 等)、:path(指定请求路径)、:scheme(指明协议方案,如 http 或 https)以及 :status(表示响应状态码,如 200、404 等)这些特殊头部,在每次 HTTP/2 请求或响应中,总是排在最前面,起到“先导”的作用。待这些特殊头部“就位”后,才轮到我们熟悉的常规头部登场,如 Accept(用于告知服务器客户端能够接受的响应内容类型)、Host(指定目标服务器的域名或 IP 地址)和 Content-Type(说明发送或接收的数据类型)等。这些常规头部按标准格式排列,共同构成完整的 HTTP 头部信息,为客户端与服务器之间的准确通信提供了坚实的保障。

HTTP/1.1与HTTP/2头部格式对比

在 HTTP/1.1 的架构中,诸如请求方法、请求路径和协议类型等关键信息被分散在请求行和各个头部字段中。这种设计存在明显的缺陷,不仅缺乏结构上的严谨性,也不够优雅。在实际应用中,往往需要依赖一些约定俗成的规则或特定的上下文来补充缺失的信息,这增加了理解和解析 HTTP 请求的复杂性,也容易导致错误。具体来看:

  • 协议类型的判断:HTTP/1.1 没有明确的标识来指示协议类型是 HTTP 还是 HTTPS,通常只能通过连接方式来推断。例如,当客户端通过 443 端口建立 TLS 连接时,才能确定使用的是 HTTPS。这种依赖外部条件推断协议类型的方式,不仅增加了系统复杂性,还降低了信息传递的准确性和效率。

  • Host 请求头的定位:在 HTTP/1.1 中,为实现虚拟主机功能而引入的 Host 请求头,只是众多请求头中的一员,并未成为请求结构的核心元素。这导致在处理复杂请求场景时,难以快速准确地获取目标主机的关键信息,影响了请求的处理效率和准确性。

HTTP/2 则通过引入伪头部字段(以冒号开头,如 :method:path 等)解决了这些问题。通过将关键信息集中在伪头部字段中,HTTP/2 使请求结构更加清晰和规范,每个字段的含义和作用一目了然。无论是客户端还是服务器,在处理 HTTP 请求时,都能够更高效、准确地提取和理解关键信息,大大提升了 HTTP 协议的性能和易用性。

“那么,HPACK 压缩究竟是什么呢?” 很多人在接触 HTTP/2 时都会对这个概念感到好奇,因为它在提升 HTTP/2 性能方面发挥了关键作用。

在 HTTP/1.1 中,头部信息以换行符(\r\n)分隔的纯文本形式存在。虽然这种方式直观,但存在一些缺点,比如占用较大空间,导致网络传输效率较低。HTTP/2 则采用了二进制格式对头部进行编码,这为 HPACK 压缩算法的应用奠定了基础。HPACK 压缩算法是 HTTP/2 的“专属工具”,如同一位技艺精湛的“数据压缩大师”,能够显著减少数据传输所需的空间,并避免重复传输相同的头部数据,从而提升传输效率。

HPACK 的高效压缩得益于两个表格的巧妙运用:静态表和动态表。静态表类似于客户端和服务器间预先共享的一本“超级字典”,包含了 61 个最常用的 HTTP 头部。想象一下,当客户端和服务器交换数据时,对于这些常见的头部信息,只需通过静态表中的索引引用,而不必每次都完整传输。这不仅减少了数据传输量,还加快了数据处理速度。如果你对这 61 个常用 HTTP 头部的具体内容感兴趣,可以查看 net/http2 包中的 static_table.go 文件,那里详细记录了静态表的内容。

常用HTTP头部的静态表

假设你发起了一个包含 :method:GET 头部信息的 GET 请求。在 HTTP/2 中,通过 HPACK 压缩算法传输数据时,奇妙的事情发生了。HPACK 不需要传输整个头部内容,只需发送数字 2。这是因为数字 2 精确对应静态表中的 :method:GET 键值对。通信双方早已了解这个映射关系,所以接收到数字 2 时,便能解析出对应的头部信息。

进一步来说,对于像 etag:some-random-value 这样的头部信息,虽然 some-random-value 是变化的,但键 etag 可以与静态表中的某个键匹配。HPACK 能利用这个特性,重用静态表中该键的索引值(假设为 34),仅传输更新后的值 some-random-value。这样,省去了重复传输完整头部名称 etag 的需要,大大节省了带宽和时间。

“那么,some-random-value 会如何处理呢?” 你可能会好奇。实际上,some-random-value 会通过霍夫曼编码,被编码成诸如 34:huffman("some-random-value") 的形式(此为伪代码示意)。更妙的是,整个头部 etag:some-random-value 会被添加到 动态表 中。

动态表初始为空,就像一本空白笔记本。随着不在静态表中的新头部不断传输,动态表逐渐“充实”。这使 HPACK 成为一个有状态的协议,意味着在整个连接期间,客户端和服务器都需要维护各自的动态表,以确保数据的正确传输和解析。

每个新加入动态表的头部都会得到一个独特的索引值,索引值从 62 开始(因为 1 到 61 被静态表占用)。有了这个索引值,后续传输相同头部时,可以直接用索引值替代完整传输,极大提高传输效率。其设计特点包括:

  • 连接级别共享:动态表在同一连接的所有数据流之间共享。客户端和服务器各自持有一份动态表的副本,就像团队中每个人都有一本实时更新的手册,在任何工作环节都能获取最新信息。
  • 容量限制:动态表默认最大容量为 4 KB(4,096 字节),但可以通过 SETTINGS_HEADER_TABLE_SIZE 帧中的 SETTINGS 参数进行调整。当动态表容量达到上限时,为容纳新头部,旧头部会被移出动态表,以腾出空间。

数据帧详解

如果存在请求体,会通过 DATA 帧发送。当请求体大于最大帧大小(默认16KB)时,会被拆分成多个 DATA 帧,这些帧共用同一个流ID。

单个TCP连接承载多个数据流

“那么,帧中的流ID在哪里?” 问得好。我们还没有讨论帧的结构。

HTTP/2中的帧不仅仅是数据或头部的容器。每个帧都包含一个9字节的帧头。这与我们之前讨论的HTTP头部不同,这是一个 帧头部

HTTP/2帧头部结构解析

首先是长度字段,它精确标示帧负载的大小(不包括帧头本身)。这一信息对于接收方准确解析帧内容至关重要,就像包裹上的重量标识,使收件人提前了解包裹的大致重量。

接下来是类型字段,它如同一个身份标签,明确区分帧的种类。在 HTTP/2 中,常见帧类型包括 DATA(用于传输数据)、HEADERS(承载请求或响应的头部信息)、PRIORITY(处理优先级相关事宜)等。不同类型的帧在传输和处理过程中扮演着各自独特的角色,通过类型字段,接收方能迅速判断帧的用途,并采取相应的处理策略。

然后是标志位,它为帧提供了额外的关键信息。例如,END_STREAM 标志(值为 0x1)就像在信息末尾贴上了“结束”标签,表示当前流上不会再有后续帧,接收方可以据此及时结束对流的处理,避免不必要的等待和资源占用。

最后是流标识符,这是一个 32 位数值,用于唯一标识帧所属的流。需要注意的是,其最高有效位是保留位,必须设为 0。流标识符就像房间编号,每个流都有专属的“房间”,帧通过这个编号找到自己所属的“房间”,确保数据在复杂网络中准确无误地流向对应的数据流。

“那帧在流内的顺序怎么办?如果帧乱序到达呢?” 这是一个关键问题。虽然流标识符能明确帧所属的流,但不能直接指定帧的顺序。解决这一问题的关键在于 TCP 层。由于 HTTP/2 基于 TCP 运行,TCP 协议具备强大的按序传输保障机制。即便数据包在网络中因路由策略经由不同路径传输,TCP 也会确保接收方收到的数据包顺序与发送时一致,从而保证了流内帧的正确顺序。

这与我们之前讨论的队头阻塞问题息息相关。 TCP 的按序传输特性在一定程度上缓解了 HTTP/2 的队头阻塞问题,但由于 HTTP/2 依赖 TCP,无法完全摆脱队头阻塞的困扰。

当服务器接收到 HEADERS 帧时,会根据相同的流 ID 创建新的数据流。这就像在繁忙的物流中心,根据货物清单(HEADERS 帧),为即将到来的货物(后续的数据帧)开辟专门的通道(新的数据流)。

服务器创建新数据流后,会先发送自己的 HEADERS 帧,其中包含响应状态和头部信息(这些信息也通过 HPACK 压缩,以减少传输开销)。随后,响应主体通过 DATA 帧有序发送。得益于 HTTP/2 的多路复用技术,服务器可以将多个流的帧交错发送,在同一连接上高效传输不同响应的数据块,大大提高了连接的利用率和传输效率。

在客户端,响应帧会根据流 ID 精确排序。客户端像一个严格的分拣员,对收到的 HEADERS 帧进行解压处理,恢复原始头部信息,并按顺序处理 DATA 帧。这样,即使有多个活跃流,同时存在的内容依然能保持整齐有序,确保客户端能准确获取服务器发送的响应数据。

流量控制

在 HTTP/2 的通信中,当接收方收到一个帧时,会进行详细检查。如果帧头的标志位字段的第 1 位被设置为 1,即 END_STREAM 标志被激活,就像发出一个明确的信号,告诉接收方:“这是终点,此流上不会有更多帧。”一旦接收到这个信号,服务器可以开始发送请求的数据,并在响应帧中设置自己的 END_STREAM 标志,以宣告流的结束。

需要特别注意的是,流的结束不等于连接的关闭。在 HTTP/2 的设计中,连接保持开放状态,其他流仍可继续正常运作。这就像一条繁忙的高速公路,即使某条车道上的车辆已驶离,公路不会关闭,其他车道的车辆仍能畅通无阻。

如果服务器需要主动关闭连接,它会发送一个 GOAWAY 帧。这不是普通的数据帧,而是一个连接级别的控制帧,用于优雅地关闭连接。当服务器发送 GOAWAY 帧时,其中包含计划处理的最后一个流 ID。这条消息如同一份正式通知,告知客户端:“我已准备好结束工作,对 ID 更高的流将不再处理,但正在进行的流仍可完成数据传输。”这就是所谓的优雅关闭,确保数据传输的完整性和有序性。

发送 GOAWAY 帧后,发送方通常会稍作等待,给接收方足够时间处理消息并停止发送新流。这短暂的停顿虽然小,却意义重大,能有效避免突然的 TCP 重置(RST)。TCP 重置会立即终止所有流,导致传输混乱和数据丢失,而优雅关闭则避免了这些风险。

HTTP/2 的强大不仅于此,它还有许多实用功能。在连接期间,双方可以灵活发送 WINDOW_UPDATE 帧来控制数据流量,像调节水龙头一样,根据网络状况和处理能力调节数据速度;用 PING 帧检测连接是否活跃,就像定期发送“健康检查”信号,确保通信链路畅通;通过 PRIORITY 帧调整流的优先级,让重要数据优先处理,保障关键业务的高效运行。如遇数据传输问题,RST_STREAM 帧可以选择性关闭单个数据流,而不影响整个连接,就像在大厦中关闭某个房间的电源而不影响其他房间。

以上是 HTTP/2 的主要内容。接下来,让我们深入探索如何在 Go 语言中实现这些强大的功能,将 HTTP/2 的优势融入实际编程项目中。

Go语言中的HTTP/2实现

或许你未曾留意,Go语言的net/http包其实早已内置了对HTTP/2的支持。

“等等,这是不是意味着HTTP/2默认就启用了呢?”

这个问题的答案并非简单的“是”或“否”。

当你的服务基于HTTPS运行时,HTTP/2极有可能已经自动启用。但如果只是普通的HTTP服务,情况就另当别论了。在以下几种常见场景中,HTTP/2可能无法正常生效:

  • 若你的服务采用普通HTTP,并且仅使用了简单的ListenAndServe,这种情况下,HTTP/2不会自动启用。
  • 当你使用了Cloudflare代理时,虽然用户到Cloudflare的请求或许会采用HTTP/2,但从Cloudflare到你服务器(源站)的连接,通常还是使用HTTP/1.1。
  • 要是你的服务位于启用了HTTP/2的Nginx之后,Nginx作为TLS终结点,承担着解密请求和加密响应的任务,然而它与你的服务之间,依旧会采用HTTP/1.1进行通信。

混合协议:HTTP/2与HTTP/1.1

要直接使用 HTTP/2 服务,配置 SSL/TLS 是必不可少的一步。从技术上讲,HTTP/2 可以在没有 TLS 的情况下运行,但这并不常见于外部流量中。然而,对于微服务架构或私有网络等内部环境,不使用 TLS 运行 HTTP/2 是可行的。如果你对此感兴趣,不妨亲自尝试一下。

需要注意的是,即便不使用 TLS 运行 HTTP/2,客户端默认仍可能选择 HTTP/1.1 协议。因此,接下来提到的方法并不能保证外部服务客户端一定会使用 HTTP/2 与服务器通信。

让我们通过一个简单的例子来探讨。首先,在 8080 端口搭建一个基础的 HTTP 服务器:(此处省略具体代码,假设代码逻辑为常规的基础 HTTP 服务器搭建)

然后,编写一个基础的 HTTP 客户端进行测试:(同样省略具体代码,假设为普通的 HTTP 客户端测试代码)

为了突出核心概念,此处省略了错误处理部分。

从输出结果可以看到,请求和响应过程均采用 HTTP/1.1 协议,这与预期一致。在未配置 HTTPS 或特定设置的情况下,HTTP/2 不会被启用。

默认情况下,Go 语言的 HTTP 客户端使用 DefaultTransport,它支持 HTTP/1.1 和 HTTP/2。ForceAttemptHTTP2 字段在默认状态下是开启的。

“既然客户端和服务器都支持 HTTP/2,为什么它们没有使用 HTTP/2 进行通信呢?”

确实,客户端和服务器都支持 HTTP/2,但这种支持限于 HTTPS 场景。在普通 HTTP 环境下,缺乏对非加密 HTTP/2 的支持。不过,这个问题的解决并不复杂,只需简单调整即可启用非加密 HTTP/2:(假设存在相关代码设置,实现 protocols.SetUnencryptedHTTP2(true) 来启用非加密 HTTP/2)

通过设置 protocols.SetUnencryptedHTTP2(true) 成功启用非加密 HTTP/2 后,客户端和服务器便能直接使用 HTTP/2 进行通信,而无需依赖 HTTPS。如此小的改动,却能顺利切换到 HTTP/2,让流程自然流畅。

值得一提的是,Go 语言通过 golang.org/x/net/http2 包为 HTTP/2 提供支持,使开发者能够更好地控制 HTTP/2 的配置和运行机制。以下是一个配置示例:(此处给出具体的配置示例代码,展示如何使用 golang.org/x/net/http2 包进行配置)

这表明,HTTP/2 并不一定要依赖 TLS 才能运行,它本质上是基于 HTTP/1.1 的协议。然而,在大多数实际应用场景中,如果服务器启用了 TLS,Go 语言的默认 HTTP 客户端会自动使用 HTTP/2 进行通信,并在无法使用 HTTP/2 时自动回退到 HTTP/1.1,整个过程无需复杂配置。

文章来源: https://study.disign.me/article/202508/18.http2-golang-practice.md

发布时间: 2025-02-21

作者: 技术书栈编辑