# 【服务器深入探讨第 3 部分：使用 WebSockets】掌握在 Decentraland 中创建丰富的多人游戏体验的高级工具。

By [Decentraland 中文社区](https://paragraph.com/@decentraland-2) · 2022-03-30

---

![](https://storage.googleapis.com/papyrus_images/3d91fc1963dfa9ecffe3ffb37fd282e657b1e0fec304932e0484fc07481dd834.png)

_2020-8-25文章_

在本系列的[上一篇文章](https://mirror.xyz/0x85A07dC63fF774b86eEdB62563d05D72bcbE8854/FFTCpEe-MeN8l-MWLwUsi4ADcYTAilCNwBUu8kHngx0)中，我们介绍了如何启动您自己的服务器来处理 HTTP 请求以同步播放器并为场景存储持久状态。这对于您跟踪的更改不会连续发生并且不需要立即为每个人进行更新的场景来说效果很好。但是，如果您想跟踪不断移动的物体并确保场景中的所有玩家都在同一位置看到它，该怎么办？

如果您希望玩家彼此完美同步，那么使用 WebSockets 是最强大的解决方案，并且这是大多数 AAA 多人游戏使用的方法。在 WebSockets 连接中，只要有需要更新的内容，播放器或服务器可以随时发送数据“帧”。

WebSockets 连接首先通过播放器和服务器之间的握手建立，这打开了双向数据流以随时发送和接收更新，从而降低延迟并更好地利用资源。

举办 Decentraland 世界杯的 Moonshot 足球场是 WebSockets 实现可能性的完美例子。使用 WebSockets 对于为所有球员和观众持续不断地更新球的位置至关重要。玩家位置也通过 WebSockets 同步以减少延迟，他们的 WebSocket 同步位置由宇航员头像表示，以使它们在所有节点都可见。

![](https://storage.googleapis.com/papyrus_images/2fba41be0c97f2c629ca94f509e4246b83a74a063e603a22566441878a9aa874.jpg)

创建服务器
-----

不幸的是，我们在本系列的[上一篇博文](https://mirror.xyz/0x85A07dC63fF774b86eEdB62563d05D72bcbE8854/FFTCpEe-MeN8l-MWLwUsi4ADcYTAilCNwBUu8kHngx0)中使用的 Firebase 不是我们在这种情况下尝试构建的可行选择。 Firebase 提供的服务是“无服务器”的，这意味着我们部署的服务器只会在每次请求到达时真正被实例化，然后在完成时解散。在这种情况下，我们需要为场景周围的每个玩家保持打开的持久连接。

这意味着我们将不得不卷起袖子，从头开始建立一个更传统的服务器。有许多出租虚拟机的服务，在本教程中，我们将使用 DigitalOcean，但任何其他类似的服务也可以正常工作。如果您对这部分过程感到满意，请随时跳过本节。

要在 DigitalOcean 中创建服务器，请执行以下步骤：

1 访问 [DigitalOcean](https://www.digitalocean.com/) 并创建一个帐户。

> 提示：如果您还没有帐户，我们建议您通过[此推荐链接](https://m.do.co/c/f6ef5fc832ae)注册，以便您在 60 天内获得 100 美元的信用额度。

2 在左侧菜单中创建一个新项目。为您的项目命名和描述。当提示 Move Resources 时，选择 Skip for now。

3 单击 Get Started with a Droplet 以创建服务器。

4 配置服务器：

    i) Select **Debian v10** as the OS version. At the time of writing this, the exact version I’m selecting is **10.3 x 64**
    ii) Pick a pricing option. For this example the cheapest option should be fine, which costs 5 USD a month
    
    > TIP: You may need to scroll the menu left to find the cheaper options.
    
    iii) Choose a region for your hosting datacenter. Choose whichever is physically closest to you or your target audience
    iv) No need to set a VPC or any of the additional options
    v) For authentication a password is the simplest option and will work, but we highly recommend that you instead use an **SSH key**. This will enable you to connect to your server directly through your command line application, instead of through a mock console in a browser window, which has several limitations. To set up an SSH key, click **New SSH key** and follow the steps detailed in that window
    

5 完成后，单击 Create Droplet 让您的服务器实例化。

6 给服务器一分钟的时间来实例化。完成后，恭喜您，您是服务器的骄傲拥有者！

访问服务器的控制台：

i) 如果您将服务器配置为通过 SSH 使用身份验证，请在您的计算机上打开命令行并粘贴以下命令，使用您的服务器的 IP 地址。

    ssh root@<ip>
    

一旦实例化，您可以在服务器设置中轻松找到显示在 DigitalOcean 中的服务器 IP。

使用上面屏幕截图中的 IP，打开该服务器的命令如下：

    ssh root@64.225.45.232
    

然后将要求您编写在创建 SSH 密钥时设置的密码。您可以在 DigitalOcean 自己的文档中阅读有关此过程和故障排除的更多信息：[https://www.digitalocean.com/docs/droplets/how-to/connect-with-ssh/openssh/](https://www.digitalocean.com/docs/droplets/how-to/connect-with-ssh/openssh/)

ii) 如果您将服务器配置为通过密码访问，请单击服务器数据最右侧的三个点，然后选择 Access Console 以在浏览器窗口中打开模拟控制台。

测试你的服务器：

1 通过运行以下命令在服务器的控制台中安装 nginx：

    apt -y update
    apt install -y nginx
    

1 打开浏览器窗口并输入服务器的 IP 作为 http URL。例如 [http://64.225.45.232/](http://64.225.45.232/)

2 如果一切正常，您应该会看到一个页面显示“_Welcome to nginx!_”

获取 TLS 证书
---------

要安全地使用 WebSockets，您需要使用 WSS（WebSockets secure），它相当于 HTTPS 对 HTTP。与 HTTPS 一样，WSS 要求您获取特定于您的域的 TLS 证书以用作加密密钥。为了解决这个问题，我们将使用 [Certbot](https://certbot.eff.org/)，这是一种流行的服务，它可以自动执行获取这些证书所需的许多步骤，并使其变得非常简单。

在服务器的控制台中，运行以下命令来安装 Certbot：

    apt install -y certbot python-certbot-nginx
    

安装后，运行以下命令来创建您的证书：

    certbot --nginx
    

Certbot 将引导您完成几个步骤并提示您回答几个问题：

1 提供您的电子邮件。

2 同意服务条款，您无需同意与 EFF 共享您的电子邮件。

3 Certbot 会要求您提供域名。如果您只是按照本教程中的步骤进行操作，那么您可能不会有一个，但有一个简单的解决方法。我们可以依赖 [nip.io](http://nip.io)，该服务将所有可能的 IP 地址映射到它们的域并相应地路由到这些 IP。你不需要在他们的平台上订阅或做任何事情，他们应该已经有一个与你的 IP 地址匹配的域。如果您的服务器的 IP 是 64.225.45.232，那么以下 nip 域应该路由到您的服务器：[64-225-45-232.nip.io](http://64-225-45-232.nip.io)。您可以在此步骤中将此域提供给 Certbot。

4 当提示重定向 HTTPS 流量时，选择选项 2 将所有流量重定向到 HTTPS。

备选方案 1：将服务器下载为 Docker 映像
------------------------

您可以使用我们准备的通用广播服务器，而不是为服务器编写自己的代码。这个预制的 WebSockets 服务器将它收到的所有消息广播给订阅同一房间的所有其他玩家。您可以通过在调用服务器时将房间名称附加到 URL 的末尾来为玩家订阅特定房间。例如，这可以用于仅在同一领域中也可以看到彼此的玩家之间共享消息，或者您可以使用您想要将玩家分组到房间的任何其他标准。

在许多情况下，这个简单的示例服务器可能就是您所需要的。如果您计划构建自定义的东西，那么您可以跳到下一部分，我们将展示如何构建自己的服务器代码。

作为参考，您可以在此 github 存储库中找到服务器使用的代码：[github.com/decentraland-scenes/ws-broadcast](http://github.com/decentraland-scenes/ws-broadcast)

我们将把服务器安装为 [Docker 容器](https://www.docker.com/resources/what-container/)。 Docker 容器是独立的黑匣子，所有配置和依赖项安装都准备好运行开箱即用的东西。它们是设置此类内容的最简单方法。有关如何安装 Docker 的更完整说明，请参阅 Docker 自己的文档：[https://docs.docker.com/engine/install/debian/](https://docs.docker.com/engine/install/debian/)

要从 Docker 安装和运行通用 WebSockets 服务器应用程序：

1 安装以下依赖项：

    apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
    

**2** 安装 Curl 和 gnupg2

    sudo apt install curl apt-get update && apt-get install -y gnupg2
    

**3** 添加 docker 的官方 GPG 密钥：

    curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
    

**4** 配置 _apt_ 以使用最新稳定版本的 docker，然后更新您的 _apt_ 存储库：

    add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable" apt update
    

**5** 安装最新版本的 Docker Engine：

    apt install -y docker-ce docker-ce-cli containerd.io
    

**6** 安装 Docker Engine 后，下载并开始为我们的广播 WebSockets 服务器运行容器：

    docker run -d -p 8080:8080 decentraland-scenes/ws-broadcast
    

    >TIP: If you later need to stop running this server app, you can stop all active docker containers with the command `docker stop $(docker ps -aq)`.
    

**7** 广播服务现在在我们的服务器上运行，但我们需要通过路径公开它的端点，我们可以使用 nginx 来做到这一点。在基于控制台的编辑器上打开 nginx 的配置文件：

    nano /etc/nginx/sites-enabled/default
    

8 将以下几行粘贴到此文档中。这些应该放在定义 _server_ 对象的第二个括号部分中。这些部分中的第一部分是指端口 80，我们在这里不使用它。我们需要以下内容进入第二个 _server_ 对象定义，它指的是安全端口 443。

    location /broadcast {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
        #
        #  Extra headers needed for certain browsers
        #
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,if-range';
    
        if ($request_method = 'OPTIONS') {
        #
                # Tell client that this pre-flight info is valid for 20 days
                #
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain; charset=utf-8';
            add_header 'Content-Length' 0;
            return 204;
        }
        if ($request_method = 'GET') {
                add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
        }
    }
    

完成后，按 Ctrl + X 退出编辑器，并在询问时保存更改。

**9** 重新启动 nginx 以便它开始使用您刚刚设置的新配置。

**10** 瞧！您现在正在运行一个安全的 WebSocket 服务器。

您现在可以通过 _wss://yourIPwithDashs.nip.io/broadcast_ 将场景连接到服务器，在我们的例子中，就是 _wss://64-225-45-232.nip.io/broadcast_。

如果我们在浏览器窗口中输入它，我们不会看到任何响应，但您可以使用以下页面对其进行测试：[www.websocket.org](http://www.websocket.org)。只需将服务器的 wss 地址写入 _location_ 字段，单击连接，然后发送任何消息，以查看它的回应。

备选方案 2：为服务器编写自定义代码
------------------

上一节展示了从现有 Docker 容器设置 WebSockets 服务器的最快方法。这种方法的缺点是 Docker 容器是一个您无法篡改的黑匣子，您无法更改服务器使用的代码。如果您希望自由地更改服务器的代码以更好地满足您的需求，本节将描述另一种路径。

要构建一个全面的游戏，您可能需要向服务器添加一些自定义逻辑。例如，您可能需要验证玩家没有作弊，或者处理玩家发送冲突信息时发生的情况。您可能还希望让游戏循环和所有逻辑在服务器端运行，而不是在场景客户端运行。

> 注意：在本节中，我们假设您已经创建了服务器，安装了 nginx 并为其获取了 TLS 证书，如前几节所述。

在服务器中设置以下权限和依赖项：

**1** 创建新用户以更安全地运行应用程序：

请使用强密码，因为它可能用于登录服务器。随意跳过提供您的全名和其它个人信息。

        Note: When using the server downloaded from Docker, we worked directly with the default `root` user because running things inside a docker container is less risky. But running the app directly while using the root user exposes vulnerabilities that malicious users could exploit, so it’s always best to use a non-root user that has less permissions to change things.
    

**2** 切换到您创建的新用户：

    su - nodejs
    

您的命令行现在将以 _nodejs@_ 而不是 _root@_ 开头。

**3** 安装 Curl 和 gnupg2

    sudo apt install curl apt-get update && apt-get install -y gnupg2
    

**4** 安装 NVM（Node Version Manager），这个工具可以轻松安装不同的 nodejs 版本：

    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
    

**5** 安装完成后，重新启动终端（不要忘记将用户切换回 _nodejs_）或运行以下命令：

    export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
    

**6** 安装最新版本的 nodejs (v14)：

**7** 验证安装是否按预期工作：

    node -v
    

**8** 安装 _pm2_，这是一个管理nodejs进程的工具，可以在崩溃的情况下自动重启我们的应用程序：

    ```bash
    npm install -g pm2
    

    
    **9** 安装 *pm2 Typescript transpiler*，这样我们就可以直接用 *pm2* 运行我们的 typescript 应用了
    

pm2 install typescript

    
    我们现在已经准备好运行我们的服务器了。作为示例，我们将通过 Docker 容器下载并运行上一节使用的广播 WebSockets 服务器，该服务器可以在以下 repo 中找到：[github.com/decentraland-scenes/ws-broadcast](https://github.com/decentraland-scenes/ws-broadcast)。您可以分叉此存储库以进行自己的更改，然后下载该分叉的存储库。或者，如果您愿意，也可以从头开始构建自己的存储库。
    
    **1** 从 GitHub 下载示例 WebSocket 项目：
    

git clone [https://github.com/decentraland-scenes/ws-broadcast.git](https://github.com/decentraland-scenes/ws-broadcast.git)

    
    **2** 进入文件夹并安装项目的依赖项：
    

cd ws-broadcast npm install

    
    **3** 使用 *pm2* 开始运行应用程序，以获得更好的稳定性：
    

pm2 start src/index.ts --name ws-broadcast

    
    **4** 广播服务现在在我们的服务器上运行，但我们需要通过路径公开它的端点，我们可以使用 nginx 来做到这一点。在基于控制台的编辑器上打开 nginx 的配置文件：
    

nano /etc/nginx/sites-enabled/default

    
    **5** 将以下几行粘贴到此文档中。这些应该放在定义 server 对象的第二个括号部分中。这些部分中的第一部分是指端口 80，我们在这里不使用它。我们需要以下内容进入第二个服务器对象定义，它指的是安全端口 443。
    

location /broadcast { proxy\_pass [http://127.0.0.1:8080](http://127.0.0.1:8080); proxy\_http\_version 1.1; proxy\_set\_header Upgrade $http\_upgrade; proxy\_set\_header Connection "Upgrade"; proxy\_set\_header Host $host; add\_header 'Access-Control-Allow-Origin' '\*'; add\_header 'Access-Control-Allow-Methods' 'GET, OPTIONS'; # # Extra headers needed for certain browsers # add\_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,if-range';

    if ($request_method = 'OPTIONS') {
    #
            # Tell client that this pre-flight info is valid for 20 days
            #
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain; charset=utf-8';
        add_header 'Content-Length' 0;
        return 204;
    }
    if ($request_method = 'GET') {
            add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
    }
    

}

    
    完成后，按 Ctrl + X 保存并退出编辑器。
    
    **6** 重新启动 nginx 以使用您刚刚设置的新配置。
    

systemctl reload nginx

    
    **7** 瞧！您现在正在运行一个安全的 WebSocket 服务器。
    
    在调试自定义服务器代码时，您可能不想每次都部署新版本服务器的麻烦。您可以在调试场景时在本地运行服务器的副本。为此，只需在服务器文件夹上运行 *npm start*。然后，当该进程处于活动状态时，您可以使用 *ws://localhost:8080* 而不是完整的 URL 将您的 Decentraland 场景定向到服务器的本地副本。
    
    ## 将场景连接到服务器
    
    让我们构建一个超级简单的场景来试用我们的服务器。你不需要导入任何特殊的库来使用 WebSockets，因为 Typescript 提供了开箱即用的基本功能。运行 *dcl init* 后，你可以在一个干净的 Decentraland 项目上尝试一下。此示例应适用于您选择遵循的两种方法中的任何一种：Docker 容器或自定义代码。
    
    启动与我们的 WebSockets 服务器的连接的最低要求是这一行：
    

const socket = new WebSocket('wss://64-225-45-232.nip.io/broadcast\`)

const socket = new WebSocket(`ws://localhost:8080`)

    
    但是，如果我们想将不同领域的玩家分开，我们需要先获取玩家的领域，然后将领域名称作为我们发送到服务器的 url 的一部分。由于获取领域需要 *await*，我们需要在 *async* 函数中执行此操作：
    

import { getCurrentRealm } from '@decentraland/EnvironmentAPI'

let socket

joinSocketsServer()

export async function joinSocketsServer() { let realm = await getCurrentRealm() socket = new WebSocket( 'wss://64-225-45-232.nip.io/wsecho/' + realm.displayName ) }

socket.onmessage = function (event) { try { const parsed = JSON.parse(event.data) log(parsed) // DO SOMETHING WITH INPUT } catch (error) { log(error) } }

socket.send( JSON.stringify({ //MESSAGE DATA }) )

    
    这就是它真正需要的！
    
    这个非常简单的场景利用上面共享的片段来保持玩家同步。单击此场景中的任何立方体都会将所有玩家变为绿色。
    
    [github.com/decentraland-scenes/ws-example](http://github.com/decentraland-scenes/ws-example)
    
    ## 将 WS 与物理相结合
    
    这个可以踢球的场景使用 [cannon.js](https://schteppe.github.io/cannon.js/) 物理库来计算球的运动。每当一名球员踢球时，该踢球的向量都会通过 WebSockets 与其他球员共享。最后一个踢球的球员开始每五秒钟将所有球的位置分享给其他球员，以确保每个人都保持同步。
    
    由于该场景使用了我们在本教程中创建的广播 WebSockets 服务器，因此单个玩家一次共享同步位置非常重要，因为服务器无法处理冲突信息。在更高级的示例中，您可以让所有球员发送他们的球位置，并且服务器将具有一些内置逻辑来达成单一共识。
    
    > 提示：要在这个场景中使用 cannon.js 库，需要通过 *npm i "@types/cannon* 为这个库安装 Typescript 类型。还需要列出 *node_modules/cannon/build/cannon.min.js* 作为场景的 *tsconfig.json* 文件中的场景文件。
    
    在这里，我们在客户端计算所有物理，但在多人场景中处理物理也有不同的方法。相反，我们可以在服务器端计算所有物理，这将确保完美的一致性并消除任何作弊的风险，但也会导致大量延迟。可能最好的方法是在客户端和服务器端都计算物理，以便玩家看到即时响应，但会定期将玩家看到的内容调整为服务器计算的内容以确保一致性。
    
    [github.com/decentraland-scenes/websocket-bouncing-balls](http://github.com/decentraland-scenes/websocket-bouncing-balls)
    
    ## 服务器端逻辑
    
    这个场景在服务器端包含了更多的逻辑，将玩家组织成团队，并且只有在双方都有玩家的情况下才开始比赛。每个场景都运行自己的当前剩余时间计算和每个团队的得分，但服务器也是如此。最后，服务器对比赛何时结束以及最终结果有最终决定权。
    
    [github.com/decentraland-scenes/Land-Flipper-Game](http://github.com/decentraland-scenes/Land-Flipper-Game)
    
    ## 其它示例
    
    以下示例服务器能够使用 WebSockets 将场景集成到 Decentraland 的 Discord 聊天中，允许您在世界中读取 discord 消息，甚至将消息写入 Discord 服务器。
    
    [github.com/decentraland-scenes/discordWebsocket](http://github.com/decentraland-scenes/discordWebsocket)
    
    这个其他示例服务器将 Decentraland 场景连接到现实世界中的智能灯。它也使用 WebSockets 同步到处理这些灯的服务器：[github.com/HPrivakos/yeelightDecentraland](http://github.com/HPrivakos/yeelightDecentraland)
    
    ## 关于安全的说明
    
    WebSockets 提供了更强大的工具来确保玩家不会作弊，但您仍然需要进行所有与 REST 服务器一样的反作弊检查。一个好处是你只需要在握手阶段验证玩家的身份，你不必验证玩家发送的每个请求。另一个优点是，一旦会话打开，您就可以确定来自该会话的所有数据都来自同一来源。例如，作弊玩家将无法发送虚假请求来冒充其他玩家并与他们相处。与 REST 服务器一样，确保玩家不作弊的最安全方法是让游戏的逻辑在服务器端运行，并仅通过需要发生的特定操作序列来推进游戏
    
    **我们关于服务器的系列到此结束。我们希望这对您来说是一次宝贵的学习经历，并且您可以在不久的将来将这些想法带入令人惊叹的项目中。本系列中链接的所有示例都是开源的，旨在重新调整用途和重新混合，不要犹豫，将它们用作您自己工作的起点。**
    
    **如果您有任何疑问或问题，找到答案的最佳位置是我们在 Discord 上的** *support-sdk* **频道。编码快乐！**
    
    **选择您常用的频道加入与我们联系，关注Decentraland(MANA)的最新动态**
    
    **DCL基金会全球社区： 
     【[Official Website](https://decentraland.org)**】 
     【**[Telegram](https://t.me/decentralandTG)**】 
     【**[Blog](https://blog.decentraland.org)**】 
     【**[Twitter](https://twitter.com/decentraland)**】 
     【**[Discord](https://decentraland.org/discord/)**】
    
    **DCL中文社区： 
     【[电报群](https://t.me/+BtB90_SKDeQ4OGQ1)】 
     【[推特](https://twitter.com/decentralandcn)**】 
     【**[微博](https://weibo.com/decentralandcn)**】 
     【**微信群**】请加微信ID ChinWaan 
     【**微信公众号**】manalandcn

---

*Originally published on [Decentraland 中文社区](https://paragraph.com/@decentraland-2/3-websockets-decentraland)*
