为什么不能在浏览器上发送 UDP 数据包

前言

在2017年有一个非常流行的Web游戏出现在网络上,他的名字叫 agar.io,这是一个休闲类的IO类游戏,这个游戏使用的网络协议通过 TCP 实现的 WebSockets。而近年来,随着 HTML5 强势崛起,各个小游戏平台的如雨后春笋的冒出来,越来越多的联网的小游戏出现在了我们的面前。

对于很多联网的游戏,特别是很多休闲的实时对战类游戏,不用下载点击就可游玩,很适合发布到小游戏平台,但这些游戏通常对网络的传输的实时性有很高的要求。如果我们使用 UDP 协议就能极大地改善网络连接,基于此原因,那有没有基于 UDP 的等效 WebSockets 协议能在浏览器中使用呢?

背景

我们都知道 Web 浏览器构建在 HTTP 协议之上,HTTP 最初开始设计的时候只是为了访问静态网页的无状态请求/响应而设计的。而 HTTP 协议又是建立在 TCP 协议基础上的,而 TCP 是一种更低层次的协议,可以保证互联网的数据按发送的顺序可靠的送达到目的地。

但是随着网站业务的逐渐复杂化,对用户的交互实时性要求越来越高,HTTP 请求/响应的协议规范逐渐难以适应。当然,对于这样的跳转,随之而来的出现了一系列的现在 Web 协议:WebSockets、WebRTC、QUIC、HTTP/2 等等。

不幸的是,虽然很多新的协议被开发出来,但并没有一个协议由针对性的提供多人游戏所需的内容,或者说,如果你要开发多人游戏而使用上面的某些协议,它会让你的应用变得非常复杂。

问题

几乎每一个多人游戏在某种程度上都对网络的可靠性有所要求,游戏开发的早期阶段要做的一个重要决定就是在 TCP 和 UDP 协议之间做出选择。

TCP 优点是提供一个经得起时间考验、稳定的可靠协议。并包只所有数据不仅能到达目的地,而且按发送顺序到达。此外,其还提供了复杂的拥塞控制,以求不会阻塞中间路由器的发送数据的速率来限制数据包丢失。

在状态瞬间万变的游戏中,在三种情况下,这种强制的、统一的可靠传输会带来一些列的问题:

  • 低优先级数据丢失干扰高优先级数据的接收

    TCP按顺序处理所有数据包,当实时游戏中,比如玩家攻击指令这样高优先级的数据包需要优先处理,但在遇到低优先级的数据包丢包的情况下,恰巧这种数据包最先发送,在触发丢包重传机制后,即使高优先级的数据包到达了服务器,服务器也不会处理,而是等待丢失的数据包重传成功。

    假设玩家 A、玩家 B相互攻击,最开始服务器产生了一个远处爆炸音效发送给客户端 A,随后玩家 B 开始对玩家 A 进行攻击,但是由于网络波动,第一个数据包丢失,但玩家B的攻击数据包没有丢,对玩家 A 来说,爆炸的特效有没有丢失其并不关心,而是关心自己有没有被攻击,然后再还手。如果等待 TCP 把所有丢失的底优先级数据包重传完成,那么玩家可能已经被人攻击致死了,可想而知这样的游戏体验有多糟糕。

  • 两个单独的可靠有序数据流的相互干扰
    即使游戏不存在优先级高低的数据,在所有数据必须靠靠传输的情况下,也是可能造成干扰的。假设系统向玩家 A 发送了一个聊天数据包,聊天信息至关重要,必须保证客户端接受,但紧随其后的又来了一个被攻击的数据包,第一个聊天数据包丢失,但第二个攻击数据包并没有丢失,如果聊条数据包妨碍了被攻击数据包的处理,显然不是玩家 A 愿意看到的。但是如果你的游戏使用 TCP,这种情况就很容发生。

  • 过时的游戏状态重传

    玩家 B 穿越整个地图去攻击玩家 A,服务器会定时同步玩家 B 的坐标信息给 A,刚开始是位置是 x = 0,移动5秒后变成 x = 100,假设玩家第一个数据包就丢失,触发重传,这意味着当玩家 B 接近 x = 100 时,服务器可能还在重传 x = 0 附近的数据。这导致玩家 A 看到玩家 B 的位置是非常过时的,在收到玩家 B 的靠近的信息之前他已经被击杀了,这是玩家 A 不能接受的用户体验。

上面的种种问题,在过去20年的游戏行业标准解决方案是通过 UDP 发送游戏数据。这在实践中需要每个游戏在 UDP 之上开发自己的自定义协议,根据自定义协议实现可靠性,并保证数据包尽快到达,且无需等待重新发送丢失的数据包。

而在网页游戏中面临的主要问题是,游戏开发者无法在浏览器中遵循上面的行业最佳实践。相反,网页游戏只能通过 TCP 来发送游戏数据,由于网络阻塞、重传等会导致游戏故障且无响应。

新的 Web 协议可以用在游戏领域吗?

WebSockets 怎么样?

WebSockets,基于 TCP 实现全双工通信协议,依赖于 HTTP,利用 HTTP 握手进行 Upgrade 完成握手后建立持久性的连接,并可进行双向数据传输。

很不幸的是 WebSockets 实现在 TCP 之上,显然会受到数据阻塞带来的影响。

WebRTC 呢?

WebRTC,即网页即时通信(Web Real-Time Communication)协议,基于 UDP 实现,是一个支持网页浏览器进行实时语音和视频对话的协议。

WebRTC 支持可以在不可靠的模式下配置数据通道,并提供了一种从浏览器发送和接受不可靠无须数据的方法。

看起来 WebRTC 比较符合我们的期望,但为什么我们没有看到它在游戏中的应用呢?原因在于,多人游戏存在着从点对点到客户端/服务器发展的趋势,尽管 WebRTC 可以在一个浏览器中向另外一个浏览器发送不可靠的无序数据,但当需要浏览器与专用服务器之间发送数据时,它就会变得无效。

另外一个原因是因为 WebRTC 本身就非常复杂,它需要提供 STUN、ICE 和 TURN 来支持 NAT 穿透,这种复杂性是可以理解的,它的目的是为了支持浏览器之间的对等协议而设计的。

而对于游戏开发人员来说,寄托于复杂性非常高的 WebRTC 来开发出简单好玩的游戏显然不太现实,WebRTC 是一个庞然大物,如果游戏服务器只是为了其中一小部分特性而引入它的依赖,这是不靠谱的,即使需要使用它,那也得等一个只是用来处理数据通道的精简版本。

QUIC 是否可行?

QUIC,由谷歌发布的一种快速UDP网络连接(Quick UDP Internet Connections)协议。其也使用 UDP 协议,但目前看起来只有 Google Chrome 浏览器支持它。

QUIC 的一个特性是支持多个数据流传输,客户端和服务器可以通过建立多个通道来隐式的创建多个数据流。

不幸的是,虽然不相关的数据流消除了数据传输的队头阻塞(Head-of-line blocking)问题,但队头阻塞以然存在于每一个流当中。

为什么不让我们可以直接发送 UDP 数据?

如果让用户可以直接从浏览器发送和接收 UDP 数据包,将是一个可怕的想法,有很多理由证明为什么不允许这样做?

  • 从浏览器中通过 UDP 数据包可以轻易的向网站发起 DDos 攻击。
  • 可以通过 Web 页面的 JavaScript 编写恶意的 UDP 数据包来探测企业内部的网络结构,并通过 HTTPS 报告,这样新的安全漏洞将会产生。
  • UDP 数据包并未加密,通过浏览器发送的任何数据包都可以被黑客嗅探和读取,甚至在传输中被修改。对于 Web 安全来说,在浏览器中发送未加密的数据将是一个巨大的退步。
  • 没有身份验证,所以服务器必须为了验证有效客户端发送过来的数据包而自己实现一套方法,这个远远超过了游戏开发者愿意为解决这个问题而付出的努力。

所以,让 JavaScript 在浏览器中创建 UDP 套接字显然是不可行的。

有没有解决方案?

有一个解决方案,那就是 netcode.io,这是一个简单的基于 UDP 并允许客户端安全地连接到专用服务器的通信协议。它是面向连接的,可以对数据包进行加密和签名,并提供身份验证支持,因此,只有通过身份验证才能连接到专用的服务器。

netcode.io 胜过 WebRTC 的地方很简单,它仅仅是消除了对 ICE, STUN 和 TURN 的依赖。并通过 libsodium 来实现加密、签名和身份验证。它避免了复杂性,同时也提供了不错的安全性。

总结

agar.io 这样的游戏依然通过 TCP 上的 WebSockets 进行联网,是因为 WebRTC 实在很难应用到专用的游戏服务器环境中使用。

我们可以寄托于 Google,在后面的开发中提供一个更加简单的 WebRTC 实现。或者你可以看看 netcode.io,它提供了一个更加简单的 “WebSockets for UDP” 的实现,具体如何在浏览器中使用 netcode.io,你可以参考 netcode.io-browser 项目。

最后,本文大部分内容翻译自 Glenn Fiedler 博客文章 Why can’t I send UDP packets from a browser?,并结合了我最近对在浏览器中使用 UDP 的思考内容进行的总结概述。