# 【服务器深入探讨第 2 部分:启动服务器】通过学习如何构建 Decentraland 场景可以与之交互的服务器,将您的场景构建提升到全新的水平。 **Published by:** [Decentraland 中文社区](https://paragraph.com/@decentraland-2/) **Published on:** 2022-03-29 **URL:** https://paragraph.com/@decentraland-2/2-decentraland ## Content 2020-7-20文章 在本系列的第 1 部分中,我们看到了我们的场景如何从现有 API 中获取数据。当我们把事情提升到一个新的水平时,这将是非常有价值的知识。 问题:如果我们安装自己的 API,并使用它来存储影响场景如何显示给稍后进入的其他玩家的信息,该怎么办? 您可以使用许多服务——其中许多服务在一定数量内免费使用,在许多情况下可能足以在场景中使用。最终,如果您想处理大量用户,您最终可能需要付费。有些服务提供存储,有些提供处理能力,有些则同时提供。 Google Firebase 是一个不错的入门选择,因为它具有友好的界面,并在一定使用量下免费提供处理能力和存储空间。它还提供开箱即用的 https 加密,这是一件少担心的事情。 还有许多其他选择,以及关于哪些是最好的强烈意见。关于它的文章很多,其中很多内容同样适用于 Decentraland 场景和典型的 Web 应用程序。您可能想尝试的其他一些替代方案是 Heroku、Azure 和 Digital Ocean。 Vercel Now(以前称为 Zeit Now)是免费的,是运行“无服务器”lambda 的不错选择。它还提供 https 支持,但不提供任何数据持久性,因此在大多数情况下,您需要将其与 Amazon S3、Azure Storage、Google Cloud Storage、DigitalOcean Spaces 等存储服务配对。 在这篇文章的大部分内容中,我们将引导您完成一个仅使用 Firebase 的示例。这将得到更多的动手,所以我建议你卷起袖子,按照这些步骤边做边学!留言簿场景我们将构建一个 API 用于这个简单的示例场景,玩家可以在其中阅读和签署留言簿。当玩家打开留言簿时,场景需要向我们的 API 发送请求,该请求应返回已签名玩家的完整列表。当玩家在书上签名时,场景应该向我们的 API 发送另一个请求,以便将他们的名字添加到书上。这个场景是对所有那些破坏了他们本来应该做的事情的旅游陷阱地点的致敬。留言簿本身当然可以在其他任何地方以非讽刺的方式使用,如果您想拥有该功能,请随意将其复制到您自己的场景中。 https://github.com/decentraland-scenes/Guest-Book-API在我们开始之前让我们开始创建 Firebase 服务器的旅程。但在开始之前,请确保您具备以下条件: • 安装 Postman • 设置 Google 帐户创建一个 Firebase 项目如果您还没有 Firebase 帐户,请先创建一个。转到 Firebase 控制台,然后单击 Add Project(添加项目)。 为您的项目命名,然后选择一个位置来托管它。搭建项目支架通过运行以下命令安装 Firebase CLI:npm install -g firebase-tools 打开您选择的命令行工具并导航到要在其中创建服务器项目的文件夹。在那里,运行 firebase init。在这里,您将看到 Firebase 提供的各种不同服务。对于这个例子,我们真正需要的是 Functions 选项。所以点击空格键选择 Functions,然后回车继续。 然后 CLI 提出了更多问题: • 使用现有项目并选择您刚刚创建的项目 • 选择 TypeScript 作为语言,因为这是我们将在本教程和大多数示例场景中使用的语言 • Linting 由您决定。我们推荐使用它,除非您已经配置了可能有冲突的东西 • 自动安装附属 太棒了!我们现在应该已经设置好项目并准备好开始工作了。查看此过程创建的新文件和文件夹。您现在应该会看到一个 functions 文件夹,其中包含以下文件: • src/index.ts • package.json • tsconfig.json • node_modules 文件夹 • lib 文件夹 我们将在本教程中编辑的这些文件中唯一的一个是 index.ts 文件。你几乎可以忽略其余的——只要知道它们就在那里就好。创建第一个端点在我们的 index.ts 文件中,我们将使用流行的 Express 库来挂载一个“无服务器”API。关于“无服务器”API:通常所说的“无服务器”API 可能会引起一些混淆,因为毕竟您实际上是在设置服务器。 “无服务器” API 和传统服务器托管之间的不同之处在于,您的代码运行的环境完全由提供商管理,您只需要编写代码,而不用担心更新机器操作系统等事情运行它。这也意味着您没有一台机器或机器的一部分持续致力于运行您的代码。相反,只要您的 API 端点被命中,虚拟机就会按需实例化,然后当工作完成时,这些资源会返回到它们来自的大脑海洋。这通常意味着更便宜的托管,因为服务提供商可以更高效地集中资源。我们建议将项目中使用的 Node 版本更改为版本 10。默认情况下,您的项目使用 Node 版本 8,该版本在今天可以正常工作,但最终会被 Firebase 弃用。要更改它,打开 package.json 并编辑*“node”:“8”的这一行,使其显示“node”:“10”*。"engines": { "node": "10" } 打开 index.ts 文件并使用以下行覆盖默认代码以创建您的第一个 API 端点:const functions = require('firebase-functions') const express = require('express') const cors = require('cors') const app = express() app.use(cors({ origin: true })) app.get('/hello-world', (req: any, res: any) => { return res.status(200).send('Hello World!') }) exports.app = functions.https.onRequest(app) 此程式附屬于必须安装的几个附屬项,因此在终端上执行 cd 到 functions 文件夹,然后运行以下命令:npm i express npm i cors 这段代码做的第一件事是导入三个库: firebase-functions:允许您创建在 Firebase 上运行的无服务器函数 express:允许您创建服务器实例来处理请求 cors:使您的端点返回的内容可访问外部站点(例如 Decentraland) 之后,我们将实例化一个 Express 路由器来处理端点的路由,并设置 CORS 策略,以便这些端点返回的内容对所有人开放。 然后是更有趣的部分。 app.get('/hello-world', (req: any, res: any)。 让 Express 路由器监听到 /hello-world 子 URL 的 GET HTTP 请求。它每次都会运行下面的函数接收其中一个请求。我们还定义了一个 req 对象,它包含请求中的所有附加数据,以及一个 res 对象,它将作为回应发送给调用者。 每次调用此端点时,我们都会返回一个“Hello World!”字符串,其 HTTP 状态代码为 200,这被普遍认为是成功回应。 最后,exports.app = functions.https.onRequest(app) 将 express 应用程序暴露给 Firebase 函数。 我们终于准备好试用我们的应用程序了!我们可以通过在 functions 文件夹的命令行中执行以下命令来在本地运行它:npm run serve 注意:如果您对此步骤有任何问题,在 /functions 文件夹中,您将看到两个新文件 firebase-debug.log 和 ui-debug.log,其中包含比您在控制台上看到的日志更详细的信息,这些可能是很有用。好的,所以我们的 API 正在运行,但是在哪里?我们需要拨打的完整地址是什么?终端响应一些信息,包括本地主机和模拟器的端口。如果您认为就是这样,那么您就在正确的道路上,但并非一路走来。老实说,Firebase 使 URL 有点拗口。以下是你如何把它放在一起:[<------domain--->]/[<-app id->]/[<-zone->]/app/[<-endpoint->] http://localhost:5001/dcl-guestbook/us-central1/app/hello-world 您可能想知道您的应用程序的 ID 是什么,并且您可能不记得您选择了哪个区域。但是,如果您仔细查看终端的响应,您会发现一个模拟器 UI 的 URL:http://localhost:4000/functions。将其转贴到浏览器选项卡中,您将找到 API 的完整路径。 您还可以在 Project 设置页面中查找您的应用 ID 和部署区域:所以现在我们可以使用 Postman 向我们新公开的 API 发送一个 GET 请求,地址为 http://localhost:5001/dcl-door/us-east1/app/hello-world(或者,因为这是一个简单的 GET 请求,你也可以只使用浏览器)。请记住,地址可能因您的项目名称或地区而异。 发送请求后,您应该会看到我们的服务器以 Hello World! 响应 Postman。如果你做到了这一步,恭喜你——你的服务器已经在运行了!设置数据库我们创建服务器的原因是能够存储和检索其他玩家上传的数据。为此,我们需要一个数据库。 Firebase 提供了两种不同的选择:一种称为 Realtime 数据库的传统数据库,以及一种称为 Cloud Firestore 的 NoSQL 数据库。在本教程中,我们将使用 Cloud Firestore。关于 NoSQL 数据库:在 NoSQL 数据库中,数据存储在集合内的文档中,每个文档可以具有与其他文档不同的结构,因此它们不一定像传统 SQL 数据库那样适合表的列和行,因为其中,它们往往更轻巧,性能更好。转到 Firebase 上的项目页面并打开 Database 选项卡。单击 Create database 以创建您的初始数据库。选择“Start in test mode”以轻松启用所有读取和写入。此外,选择托管数据库的区域,该区域可以是您托管应用程序的同一区域,也可以不是。请记住,我们的函数服务器作为无服务器的短暂事物运行,仅在有人访问其端点时才存在 - 这将是与 Firestore 数据库分开的一台机器,它将保存持久数据。任何需要从功能服务器机器到数据库服务器机器的请求都需要以某种方式进行身份验证。 我们可以配置我们的数据库,以便拥有 Google 帐户的不同用户可以编辑其中的数据,但这对我们没有任何用处,因为玩家不会从 Decentraland 内部登录他们的 Google 帐户。我们需要的是创建一个 Service account(服务帐户)。服务帐户是用于应用程序间通信的假用户。我们的函数服务器将在连接到 Firestore 数据库时模拟该用户。 我们将创建一个服务帐户,检索其密钥并将其存储在我们的项目中。 在 Firebase 控制台中转到您的应用,单击小齿轮箱并选择 Users and permissions(用户和权限)。然后打开 Service account 选项卡。在此屏幕中,保持选中 Node.js 选项并单击 Generate new private key。这将使用此服务帐户的秘密凭据将 .json 文件下载到您的计算机。 将此文件复制到您的项目文件夹中,并根据需要重命名。确保您没有在任何地方公开共享服务帐户凭据。在这种情况下,我将此文件添加到 .gitignore 文件中,这样它就不会上传到 GitHub 存储库。 然后,回到服务帐户页面,您会注意到有一段非常有用的代码已经写出,它已经指向您的数据库的 URL。您可以将这些代码行直接复制到 index.ts 文件的末尾,这就是完成这项工作所需的全部内容!只需确保带有键的 .json 文件的路径与您实际存储文件的位置相匹配。var admin = require('firebase-admin') var serviceAccount = require('path/to/serviceAccountKey.json') admin.initializeApp({ credential: admin.credential.cert(serviceAccount), databaseURL: 'https://dcl-door.firebaseio.com', }) 这些代码行导入负责权限的 Firebase 模块,然后它加载您的凭据文件并初始化一个经过身份验证的应用程序,我们稍后可以在我们的代码中使用该应用程序。 之后我们将再添加一行代码,它会启动一个经过身份验证的 db 对象以与 Firestore 交互。const db = admin.firestore() 现在让我们返回到 Firebase 上项目仪表板的 Database 选项卡。我们将手动创建我们的第一个数据文档。请记住,在 NoSQL 数据库中,数据存储在集合中,每个集合都包含一个文档列表。在我们的例子中,每个签名都将存储为它自己的文档,包含一个 name 和一个 id 字段。这些文档都将属于一个集合。 单击 Start collection,然后将您的收藏命名为 Signatures,然后在该收藏中创建第一个签名。单击 AutoID 为文档 ID 生成一个随机字符串,并为其提供两个字符串类型的字段:id 和 name。进行数据库调用有了这个,我们就可以开始构建我们打算构建的真正功能了。基本上我们需要做两件事: 1 当有人阅读这本书时检索签名列表 2 当有人在书上签名时存储新签名 将以下代码添加到 index.ts,以处理这两个操作:let signatures = db.collection('Signatures') app.get('/get-signatures', async (req: any, res: any) => { try { let response: any = [] await signatures.get().then((queryResult: { docs: any }) => { for (let doc of queryResult.docs) { response.push(doc.data()) } }) return res.status(200).send(response) } catch (error) { console.log(error) return res.status(500).send(error) } }) app.post('/add-signature', async (req: any, res: any) => { let newSignature = req.body try { await signatures .doc('/' + Math.floor(Math.random() * 100000) + '/') .create({ id: newSignature.id, name: newSignature.name, }) return res.status(200).send('Signed book!') } catch (error) { console.log(error) return res.status(500).send(error) } }) 提示:如果您不熟悉这里的某些语法,我们会在本系列的上一篇文章中解释 try、catch、async 和 await 的含义。您还可以在他们的文档中找到有关 Firebase API 的信息。此处的第一行检索代表我们通过 UI 创建的 Signatures 集合的对象。我们将使用这个对象来读取和写入这个集合。然后我们创建两个新端点,一个在 /get-signatures 上侦听 GET 请求,另一个在 /add-signature 上侦听 POST 请求,然后我们定义在这两个事件中的每一个上运行的函数。 两个端点中的第一个在数据库上运行查询以获取集合中的所有文档,然后将所有这些打包到一个数组中,并在响应消息中发回。 让我们用 npm run serve 再次启动我们的服务器并试一试……我们得到的响应是单个项目的列表,即我们之前通过 UI 添加的项目。 第二个端点期望传入请求具有包含用户 ID 和名称的正文。这些信息通过 req 对象进入我们的函数,因此我们可以通过 let newSignature = req.body 轻松获取它。然后,我们为签名集合创建一个新文档,给它一个随机数作为 ID,并用请求正文中的数据填充它。 当通过 Postman 调用这个端点时,我们需要记住将 POST 设置为方法,并将数据写入请求的 Body。如果一切顺利,我们会收到“Signed book!”的回复。如果我们现在向 /get-signatures 端点发出另一个请求,我们应该会看到我们的两个条目。 Firebase 的一个很酷的功能是,如果我们转到项目的 Database 选项卡,您可以在 UI 中看到实时更新的新数据。因此,如果我们打开该屏幕,我们应该会看到其中列出的两个文档。部署服务器当通过 npm run serve 在本地运行它们时,我们所有的功能都能完美运行。现在还剩下最后一步:将我们的服务器部署到云端!您所要做的就是在控制台上运行它:到达端点的 URL 现在不同了。再一次,Firebase 使 URL 结构有点拗口。这是您构建的方式:[<---zone + app id + cloudfunctions.net----->]/app/[<--endpoint-->] https://us-central1-dcl-guestbook.cloudfunctions.net/app/hello-world 如果您在项目的 Firebase 仪表板中打开 Functions 选项卡,您还可以查看完整 URL 的外观。在 Postman 上测试您的端点的新路径,以确保一切正常。 您现在可以开始围绕此 API 构建您的场景。我们不会在本教程中介绍它,但它与我们在本系列的第 1 部分中介绍的内容应该不会有太大的不同。 您还可以查看我们在项目 repo 中所做的工作:github.com/decentraland-scenes/Guest-Book-API排行榜示例场景查看另一个示例场景。它非常相似,但稍微复杂一些,可能正是您的场景所需要的。随意借用你想要的任何代码! github.com/decentraland-scenes/Leader-Board 将 Firebase 与其他存储服务相结合# 对于创世纪广场,我们在显示玩家在塔上写的消息时使用了类似的东西。我们使用 Firebase 来公开我们的端点,但我们实际上是在 Amazon S3 服务器上而不是在 Firebase 的 Firestore 服务上存储数据。更改数据的请求会通过我们的 Firebase 服务器,但当玩家获取数据以读取时,他们会直接从 Amazon S3 服务器获取数据。为什么要这样做? Firebase 是一个很好的解决方案,但 Amazon S3 服务器更强大,并且在处理许多请求时仍然是一种更便宜的解决方案。虽然我们可以直接从 Decentraland 场景中读取 S3 服务器中的 .json 文件的数据,但我们不能直接从场景中写入。这是因为您需要 Amazon 凭据来进行编写,我们不建议将它们保存在场景代码中,因为它们在那里并不完全安全,因此我们从 Firebase 服务器处理该部分。 查看 Plaza 的仓库:github.com/decentraland-scenes/Genesis-Plaza将服务器与消息总线相结合与其让玩家每隔几秒轮询一次服务器中的新值,不如使用服务器和场景消息总线结合使用。我们已经在这篇关于 Genesis Plaza 的博文中介绍了如何使用 MessageBus。使用这种方法,您可以减少发送到服务器的请求数量,并且可以让玩家更好地同步彼此的操作。 github.com/decentraland-scenes/Zenquencer在此示例中,您可以使用音序器来编写一些音乐模式,然后播放它们。当玩家进入场景时,他们会从服务器下载最新模式。然后,随着那里的不同玩家改变模式,他们使用消息总线从彼此那里获得这些变化,他们不需要定期检查服务器来了解什么是新的。 为了使其正常工作,我们需要为每个领域保留此模式的单独版本,并知道每个玩家在更新模式时所在的领域。这是因为只有在同一领域的玩家才能通过消息总线相互发送消息。否则,当处于不同领域的玩家在没有相互通知的情况下修改相同的模式时,模式最终会出现奇怪的不一致。场景包括玩家的领域作为它发送的请求的一部分,然后服务器根据领域处理不同的 .json 文件。 我们在这个例子中做的另一件值得注意的事情是,更改不会立即发送到服务器,而是使用 utils.Delay 组件做一个小缓冲,这样如果玩家快速连续更改多个音符,服务器只收到模式最终状态的通知。这有助于减少服务器需要处理的请求数量。为了使其工作,每个更新请求都需要发送模式的完整状态,而不仅仅是更改的元素。 这个另一个例子包括一幅壁画,其中像素可以被涂上不同的颜色。它使用消息总线和服务器的方式与我们刚刚共享的排序器非常相似。github.com/decentraland-scenes/mural-example-scene 这是一个更酷的示例,可让您通过放置 3D 体素进行构建。从服务器的角度来看,它确实与壁画非常相似,只是它处理的是 3D 坐标而不是 2D 坐标。 github.com/decentraland-scenes/voxel-art-creator关于安全性的说明:我们分享的示例均未实施任何针对恶意使用 API 的安全措施。如果玩家愿意,他们可以向这些 API 发送虚假请求。在这些示例中无需担心太多,因为如果有人这样做,那将是无害的。但是,例如,如果我们自动向记分牌 API 上达到特定分数的玩家发送 NFT 奖励,那么这可能会被滥用。您可以轻松添加安全密钥,以便场景在调用 API 时使用该密钥,但这不会增加太多安全性,因为玩家可以从场景代码中获取该密钥。他们还可以通过在浏览器控制台的 Network 选项卡中查看传出的 HTTP 请求来选择它。理想情况下,您应该在服务器端运行验证。例如,您可以跟踪游戏中发生的各种事件,并且只向在请求奖励之前经历了所有这些事件的玩家发送奖励。这仍然是可以破解的,但如果他们看不到服务器的代码,他们将不知道他们需要通过什么验证,并且它达到了合法玩游戏比寻找作弊方法更容易的地步。这是一篇很长的文章,但我们希望您发现自己配备了新工具,使您能够在 Decentraland 中创造令人兴奋的新体验! 请继续关注本系列的第 3 部分,我们将在其中讨论使用 Websockets 服务器创建具有更流畅和实时感觉的多人游戏体验。 选择您常用的频道加入与我们联系,关注Decentraland(MANA)的最新动态 DCL基金会全球社区: 【Official Website】 【Telegram】 【Blog】 【Twitter】 【Discord】 DCL中文社区: 【电报群】 【推特】 【微博】 【微信群】请加微信ID ChinWaan 【微信公众号】manalandcn ## Publication Information - [Decentraland 中文社区](https://paragraph.com/@decentraland-2/): Publication homepage - [All Posts](https://paragraph.com/@decentraland-2/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@decentraland-2): Subscribe to updates - [Twitter](https://twitter.com/decentralandcn): Follow on Twitter