i007.cc

i007.cc

优先队列-降维打击

05.价值资料

gRPC websocket HTTP3 quic TCP/UDP 与Claude的讨论

标准 gRPC 是强绑定 HTTP/2 的,不是随便什么 HTTP 版本都行,也不用 WebSocket——但浏览器场景下确实有例外,值得展开说说。

标准 gRPC:只能是 HTTP/2,不能是 HTTP/1.1

我们前面聊的那套机制——一次调用对应一个 HTTP/2 stream、用 DATA 帧的长度前缀分帧、用 trailers 传最终状态码——全都是建立在 HTTP/2 特有能力上的,HTTP/1.1 根本没法支撑这些。具体来说:HTTP/1.1 没有真正意义上的多路复用(一条连接同一时间只能处理一个请求,或者靠 pipelining 勉强凑合但有队头阻塞问题);HTTP/1.1 的 trailers 支持非常有限、几乎没有被广泛实现和信任,而 gRPC 恰恰依赖 trailers 来传最终的成功/失败状态;HTTP/1.1 也没有 HTTP/2 那种基于 stream 的独立流控。所以严格来说,gRPC 客户端和服务端建连时,是通过 TLS 握手阶段的 ALPN(应用层协议协商)明确要求走 h2,如果对方不支持 HTTP/2,gRPC 连接直接就建立不起来,不存在”退化成 HTTP/1.1 兼容跑”这种说法。

为什么不用 WebSocket

WebSocket 确实也能做到全双工、长连接、双向持续收发数据,表面上看好像能满足 gRPC 的流式需求,但 Google 设计 gRPC 时特意没选它,原因主要是:WebSocket 本质上只是一条裸的双向字节流通道,协议本身不提供多路复用——如果你想在一条 WebSocket 连接上同时跑多个独立的”调用”,得自己在应用层再发明一套多路复用、流量控制、消息分帧的机制,而这些 HTTP/2 全都原生提供,直接复用现成的、经过大量生产验证的能力显然更划算。另外 HTTP/2 还有 HPACK 头部压缩这种 WebSocket 没有的优化,而且 HTTP/2 作为标准 web 协议,和现有的负载均衡、反向代理、CDN 生态兼容性也更好——虽然实际上 HTTP/2 的这类基础设施支持也是这几年才逐渐成熟的,早期不少 L7 负载均衡对 HTTP/2 透传支持得并不好,这也是 gRPC 落地时一个常见的运维坑。

例外场景:浏览器里的 grpc-web,某些实现会用到 WebSocket

这里就呼应到我之前提过的一点——浏览器的 fetch/XHR API 没法让 JS 代码直接操控底层 HTTP/2 帧,没法自己发起真正的 gRPC 调用。所以浏览器要用 gRPC,走的是 grpc-web 这条路,而 grpc-web 的官方实现(Google 自己那个)基于 XHR/fetch,天生只支持 Unary 和 Server streaming 两种模式——因为 XHR/fetch 没法很好地支持”客户端持续往外发送数据”这种语义,Client streaming 和 Bidirectional streaming 做不出来。

为了在浏览器里也能做真正的双向流,社区里有些 grpc-web 实现(比较知名的是 improbable-eng 维护的那个版本)提供了一个基于 WebSocket 的传输层选项:因为 WebSocket 天然支持全双工、两边都能随时发消息,拿它当传输载体,可以在浏览器端模拟出 Client streaming 和 Bidirectional streaming 的效果,弥补 fetch/XHR 做不到的部分。所以准确地说,不是”gRPC 本身用 WebSocket”,而是”某些 grpc-web 的浏览器兼容层,为了绕开浏览器限制,选择性地用 WebSocket 作为传输层”——这是一个专门为了适配浏览器环境而存在的变通方案,不是 gRPC 的主流或推荐路径。

还需要一层 proxy 做转译

不管是标准 grpc-web 还是基于 WebSocket 的变体,浏览器发出的请求格式(grpc-web 协议)和后端真正的 gRPC 服务(标准 HTTP/2 gRPC 协议)在字节层面并不完全一致,通常需要一个代理(最常见的是 Envoy,配置了 grpc-web filter)在中间做协议转译——不过现在情况有所改善,像 grpc-go、grpc-node、ASP.NET Core 的 gRPC 实现,已经可以在服务端进程内直接原生支持 grpc-web,不一定非要额外部署一层 Envoy。

顺带一提未来方向

业界也有一些实验性工作在探索让 gRPC 跑在 HTTP/3(基于 QUIC)之上,理论上能进一步解决我们前面提到的 TCP 层队头阻塞问题,但截至目前这还不是生产环境的主流形态,绝大多数 gRPC 部署依然是标准的 HTTP/2。

对于一些采用长连接的游戏服务器,是否能用http方式来实现?

可以做,但要看你说的”游戏服务器长连接”对延迟有多敏感,以及”HTTP 方式”具体指的是 HTTP 家族里的哪一种——这几种选择在实际游戏行业里的适用场景差别很大。

先说为什么传统强实时游戏大多不用 HTTP/TCP

竞技类、动作类游戏(FPS、格斗、MOBA 里对操作精度要求高的部分)对延迟极度敏感,通常要求几十毫秒级的往返时间,而且是高频率的小数据包(玩家位置、输入指令,可能一秒钟要发几十次)。这类游戏传统上几乎都是自己在 UDP 上实现协议,而不是用 TCP(更不用说 HTTP 了)。核心原因就是我们前面提到过的”传输层队头阻塞”:TCP 保证可靠有序交付,一旦丢了一个包,后面所有数据都得等这个包重传成功才能继续处理,哪怕后面的数据早就到了。对游戏来说这是灾难性的——你根本不关心 3 帧之前那个已经过时的玩家位置有没有送达,你只想要最新的位置,宁可丢弃旧数据也不要卡顿等待。这种”选择性可靠”的语义,TCP 天生做不到,只能自己在 UDP 上造轮子(常见的做法是给不同类型的消息分别定义可靠性策略:位置同步用不可靠不保序,伤害/击杀这类关键事件用自己实现的可靠重传)。

如果非要用 HTTP 家族协议,有这几条路径

HTTP/1.1 长轮询(long polling):客户端发一个请求,服务端一直不回,等有新数据了才回,客户端收到后立刻发起下一次请求。这本质上是在用请求-响应模型硬凑”服务端推送”的效果,延迟高、开销大,基本上是被淘汰的老办法,现在很少有游戏真的这么用。

WebSocket:这是目前”用 HTTP 起步做游戏长连接”最常见的选择。它的握手走的是 HTTP/1.1 的 Upgrade 机制,握手成功后就切换成一条独立的全双工帧协议,不再是 HTTP 语义了,但因为握手阶段是标准 HTTP,兼容性好、浏览器原生支持、防火墙也认。适合对延迟没那么苛刻的游戏——回合制卡牌、棋牌、H5 小游戏、一部分 MOBA/放置类游戏,这些场景两三百毫秒的延迟通常不影响体验。但 WebSocket 底层还是跑在 TCP 上,一样有队头阻塞问题,不适合要求几十毫秒级响应的强实时对战。

HTTP/2 流式(包括我们前面聊的 gRPC bidirectional streaming):可以做出一条持久、双向、多路复用的连接,架构上比 HTTP/1.1 更贴近游戏需要的模式,但它同样是构建在 TCP 之上的,一样继承 TCP 的队头阻塞问题——这条连接上如果丢了一个包,同一条连接里其他 stream 的数据也会跟着卡住。所以 HTTP/2 更适合游戏里”重要但没那么实时”的部分,比如聊天、状态同步、非战斗数据,而不是核心战斗帧同步。

HTTP/3(基于 QUIC):这是理论上最有前景的方向,因为 QUIC 直接构建在 UDP 之上,解决了 TCP 那种”一个连接内所有流互相拖累”的问题——每个 stream 的丢包重传是独立的,一个 stream 卡了不影响其他 stream。不过这里有个细节要区分:游戏行业如果用 QUIC,更多是把 QUIC 当作一个”自带加密、支持多路复用的传输层”来用,自己在上面定义游戏协议,而不是严格遵循 HTTP/3 那套请求-响应语义。换句话说,是”借用 QUIC 这个底层技术”而不是”真的用 HTTP 语义来传游戏数据”。这几年确实有一些游戏引擎和网络库在往这个方向探索,但还没有成为行业主流标准做法。

一个经常被忽略、但很现实的理由:防火墙穿透

有意思的是,不少厂商即便技术上更倾向于自研 UDP 协议,也会额外做一条 WebSocket 或 HTTP/2 的”备用通道”,原因不是性能,而是连通性——很多公司网络、机场酒店 Wi-Fi、移动运营商的 NAT 环境会封锁或限制任意 UDP 端口,但几乎从不封锁标准的 443(HTTPS)端口。所以有些游戏会做成”优先用自研 UDP,UDP 连不通就自动降级到 WebSocket/HTTPS”这种双通道策略,用 HTTP-like 协议换取”至少能连上、体验差一点”,而不是完全连不上。

总结一下怎么选

如果你的游戏是强实时竞技类,核心战斗同步这部分不建议用任何 HTTP 家族协议,自研 UDP(或者用 ENet、KCP 这类现成的可靠 UDP 库)依然是业界主流做法。如果是回合制、卡牌、放置、轻度联机这类对延迟没那么敏感的游戏,WebSocket 是性价比很高的选择,开发和运维都简单很多。如果你的游戏架构里既有强实时的战斗部分,又有大量非实时的周边功能(聊天、商城、好友、匹配、排行榜),比较常见的做法是核心战斗走自研 UDP,周边系统走 HTTP/gRPC——这其实也是我们前面聊 Crossplane/Terraform 分层思路时提到过的类似原则:不同的需求用最适合它的工具,而不是指望一个协议包打天下。

先把分层关系理清楚

网络分层从下到上大致是:链路层(以太网/WiFi)→ 网络层(IP)→ 传输层(TCP 或 UDP,二选一,互相独立)→ 应用层(HTTP、DNS、gRPC 等)。TCP 和 UDP 是同一层级的两个”兄弟协议”,各自定义了自己的报文头格式、各自实现了完全不同的语义——TCP 有三次握手、确认应答、重传、拥塞控制这一整套复杂机制来保证可靠有序;UDP 则完全没有这些,发出去就不管了,是”尽力而为”的无连接协议。它们唯一的共同点是都直接封装在 IP 报文里传输,没有谁依赖谁、谁包含谁的关系。你可能是把”TCP/IP”这个统称理解成了”TCP 建立在 IP 之上,而 IP 又建立在 UDP 之上”这种链条,但实际上 IP 之上是 TCP 和 UDP 两条并行的路,不是一条串联的路。

防火墙怎么区分 TCP 和 UDP

这个区分能力其实是天生就有的,不需要什么特殊手段。IP 报文头里有一个字段叫 Protocol(IPv4)或者 Next Header(IPv6),是一个 8 位的数字,专门用来标识”这个 IP 包里装的到底是什么协议的数据”——TCP 对应数字 6,UDP 对应数字 17,ICMP(比如 ping 用的)对应 1,等等。防火墙、路由器只要读一下这个字段,立刻就知道这个包是 TCP 还是 UDP,完全不需要深入解析后面的内容,想单独放行 TCP、单独封锁 UDP 是非常直接、非常轻量的一个过滤规则。

为什么很多环境倾向于”放行 TCP、限制 UDP”

这背后主要是几个实际考量:

一是 TCP 有明确的握手过程(SYN → SYN-ACK → ACK),防火墙/NAT 设备可以借助这个握手来做”状态追踪”(stateful filtering)——只允许由内部发起、外部正常应答的连接,能比较可靠地判断”这是不是一次合法的双向会话”。而 UDP 是无连接的,没有握手可供追踪,防火墙只能靠简单的超时和启发式规则去猜”这算不算一个会话”,天然就更难做精细化的安全控制,所以很多安全策略干脆采取保守做法:默认拒绝大部分 UDP,只放行少数明确需要的(比如 DNS 用的 UDP 53)。

二是历史上 UDP 被大量用于反射放大型 DDoS 攻击(比如 DNS 放大攻击、NTP 放大攻击),这类攻击利用的正是 UDP 无连接、可以伪造源地址的特性,导致很多网络管理员对开放任意 UDP 端口有天然的警惕。

三是业务必要性不对等——TCP 的 80/443 端口承载了几乎所有网页浏览、邮件、文件传输等基础网络需求,公司网络、机场酒店 Wi-Fi 不可能封这两个端口,不然整个网络就没法用了;但任意的 UDP 端口对绝大多数办公/公共网络场景来说不是必需品(游戏、P2P、语音视频这些是”额外”的流量),索性作为默认安全基线直接限制掉,漏掉的合法应用只能自己想办法(比如走 TCP 兜底)。

四是 NAT 对 UDP 的处理本身就比较脆弱,UDP 没有连接的概念,NAT 设备只能靠一个映射表加超时机制来维持”内网 IP:端口 ↔ 外网映射”的对应关系,超时时间通常比较短(几十秒到几分钟),稍微空闲一下映射就失效了,导致依赖 UDP 的应用经常出现”连接莫名断掉”的现象,这也是很多 UDP 应用(包括游戏)天生就比 TCP 应用更容易在复杂网络环境下出问题,不完全是”被故意封”,有时候是 NAT/防火墙的默认行为造成的连带影响。

一个很切题的延伸:HTTP/3 也会踩到这个坑

这也是为什么我们前面聊到的 HTTP/3/QUIC(虽然理论上性能更好)在一些受限网络环境里反而会水土不服——因为 QUIC 底层用的是 UDP,如果所在网络对 UDP 限制严格,QUIC 连接可能直接建立失败或者被限速。所以几乎所有支持 HTTP/3 的浏览器和客户端都内置了”回退机制”:先尝试 QUIC/UDP,如果不通就自动降级回 HTTP/2(基于 TCP)。这跟游戏客户端”优先 UDP、连不通就切 WebSocket/HTTPS”的思路是同一个道理,本质上都是在应对”网络环境对 UDP 不友好”这个现实问题。

那如果现在要开发一个面向未来的游戏服务器网络架构,网络协议选型该怎么做?

这个问题正好把我们前面聊的所有点(TCP/UDP、HTTP/2、WebSocket、QUIC、防火墙穿透)串起来做一次系统性的选型决策。核心思路不是”选一个最好的协议”,而是按流量特征分层,每一层用最合适的协议——下面按这个框架展开。

第一步:先把流量按延迟敏感度分类,不要指望一个协议包打天下

一个游戏的网络流量大致可以分成三类:强实时的核心玩法数据(位置同步、输入指令、战斗事件),对延迟要求几十毫秒级,而且很多数据”旧的就没用了”(比如上一帧的位置);次实时的辅助数据(聊天、非关键状态同步、观战数据流),能容忍一两百毫秒;完全非实时的外围服务(登录鉴权、商城、排行榜、资源下载、埋点上报),用标准 Web 请求完全够用。这三类流量的最优协议选择是不一样的,面向未来的架构应该从一开始就按这个维度分层设计,而不是试图找一个协议满足所有场景。

核心实时通道:QUIC 是目前最面向未来的选择

传统做法是自研 UDP 协议(ENet、RakNet、KCP 这类),原因我们前面聊过——TCP 的队头阻塞对实时同步是灾难。但现在有一个更标准化、更值得投入的方向:QUIC。QUIC 相比自研 UDP 协议有几个关键优势:一是默认强制 TLS 1.3 加密,不用自己额外操心传输层安全,这对防作弊、防篡改、满足平台合规要求(尤其主机平台、部分应用商店现在对网络安全有要求)很重要;二是连接内的多个 stream 相互独立,某个 stream 丢包不会拖累其他 stream(解决了我们前面说的 TCP 队头阻塞问题),同时 QUIC 还有一个专门的扩展叫 unreliable datagram(不可靠数据报,对应 RFC 9221),让你可以在同一条连接里,既用可靠有序的 stream 传关键事件(击杀、拾取、聊天),又用不可靠数据报传高频、可丢弃的状态(位置同步)——这基本上是”自研 UDP 协议”想要达到的效果,但标准化、经过审计、有成熟库支持(Google 的 quiche、Meta 的 mvfst、Microsoft 的 msquic、Cloudflare 的 quiche、Go 的 quic-go 等)。三是连接迁移(connection migration):QUIC 连接是靠 connection ID 标识的,不像 TCP 靠”源 IP+源端口+目的 IP+目的端口”这个四元组,所以玩家从 WiFi 切换到蜂窝网络、IP 地址变了,QUIC 连接可以无缝延续,不需要重连——这对移动端游戏体验提升很明显。

浏览器端:WebTransport 是 QUIC 在浏览器里的落地方式

如果你的游戏有 Web 端(浏览器直接玩,不装客户端),以前浏览器完全没法直接用 UDP/QUIC,只能退而求其次用 WebSocket。但现在有一个新的浏览器 API 叫 WebTransport,它是构建在 HTTP/3(也就是 QUIC)之上的,专门为低延迟场景设计,同时提供可靠 stream 和不可靠 datagram 两种语义——本质上就是把 QUIC 的能力开放给了浏览器 JS 代码,不再需要 WebSocket 那种”只能可靠有序传输”的妥协。这个 API 现在已经到了 Baseline 阶段(2026 年 3 月 Safari 26.4 加入原生支持后,Chrome、Firefox、Edge、Safari 四大浏览器已经全部支持,不需要开发者标志或 polyfill),可以说是目前浏览器里最贴近游戏实时通信需求的标准 API,值得作为 Web 端的首选。

兜底层:WebSocket/TCP 443 仍然必不可少

不管核心通道用自研 UDP 还是 QUIC,都必须假设一部分玩家的网络环境会限制或封锁 UDP(公司网络、部分移动运营商、某些受限地区网络),这是我们前面讨论过的现实问题。所以面向未来的架构依然需要一条基于 TCP/TLS 443 端口的兜底通道(WebSocket,或者退化成轮询),客户端在建连时先尝试 QUIC,超时或失败就自动降级到这条兜底通道,接受延迟变高但保证能连上。建议提前做好连接质量的埋点统计,实际测一下你的用户群体里有多大比例最终走的是兜底通道,这个数字会直接影响你要在兜底体验上投入多少优化精力。

后端微服务层:gRPC over HTTP/2

匹配系统、库存系统、好友系统、结算系统这类后端服务间通信,不直接面对客户端延迟敏感的场景,gRPC 是很合适的选择——我们前面详细聊过它的优势:强类型接口、多语言代码生成、流式能力(可以用来做匹配状态推送、观战数据分发这类次实时场景)、以及成熟的服务网格生态。

外围服务:标准 HTTPS/REST 就够

登录鉴权、内购商城、资源热更下载、日志上报这些完全不需要专门的实时协议,用标准 HTTPS(HTTP/2 或者未来的 HTTP/3 语义都行)就足够,好处是能直接享受现成的 CDN、负载均衡、监控告警等 Web 基础设施,不需要为这些场景单独定制。

几个容易被忽视的运维细节

QUIC/UDP 流量的负载均衡比 TCP 复杂——因为连接不再绑定固定的四元组(前面提到的连接迁移特性),传统基于四元组做一致性哈希的 L4 负载均衡器不适用了,需要能识别 QUIC connection ID 的负载均衡方案来保证同一个连接的包始终转发到同一台后端;另外自建 UDP 服务要特别小心被利用做反射放大攻击,QUIC 协议本身在握手阶段有专门的防放大攻击设计,如果是自研 UDP 协议则要自己实现类似保护。

最后一句务实的话

“面向未来”不等于”上最新技术”。如果你的游戏本身对延迟没那么敏感(回合制、卡牌、放置类、慢节奏 MOBA),直接用 WebSocket + gRPC 的组合完全够用,架构简单、招人容易、调试方便,不需要为了赶技术潮流去啃 QUIC 这种更复杂的自建成本。只有当你的核心玩法确实是强实时同步、且预期要长期投入维护的时候,才值得把 QUIC 作为核心通道认真投入——这也是为什么建议先做第一步的流量分类,用真实的延迟需求倒推协议选型,而不是反过来。

Sources:

发表回复