# 介绍

**URL:** https://heroiclabs.com/docs/zh/nakama/server-framework/introduction/
**Summary:** Nakama 包括一个快速嵌入式代码运行库，用于编写 JavaScript 包、Go 插件和 Lua 模块形式的自定义逻辑。学习服务器运行时功能的使用基础.

---


# 介绍

Nakama 包括一个快速嵌入式代码运行库，用于编写 [JavaScript](https://developer.mozilla.org/en-US/docs/Web/javascript) 包、[Go 插件](https://golang.org/pkg/plugin/)和 [Lua 模块](https://www.lua.org/manual/5.1/manual.html) 形式的自定义逻辑。

运行时框架对于编写游戏或应用程序的服务器端逻辑是必不可少的。使用它可以编写不希望在客户端设备或浏览器上运行的代码。通过服务器部署的代码可以立即被客户端使用，从而可以动态更改行为并更快地添加新功能。
此代码可用于运行权威逻辑或执行验证检查，以及通过 HTTPS 与其他服务集成。

在想要设置各种功能的规则时，例如用户可以有多少[好友](../../concepts/friends/)或他们可以加入多少[群组](../../concepts/groups/)时，使用服务器端代码。 

{{< note important >}}
我们不建议修改 Nakama 源代码并从源代码进行重新构建，以添加新功能或自定义行为。推荐的方法是使用嵌入式运行库。
{{< / note >}}

本页面讲述 Nakama 运行时框架中的关键概念和功能。

## 加载模块

{{< note "important" "Tip" >}}
Heroic Labs 建议使用 JavaScript VM。
{{< / note >}}

默认情况下，服务器会在启动时扫描 `data/modules` 文件夹中相对于服务器文件或在 YAML [配置](../../getting-started/configuration/#runtime)中指定的文件夹中的所有文件。您还可以在启动服务器时，通过命令标志指定模块文件夹。

作为启动序列，将加载和评估运行库路径文件夹中带 `.lua`、`.so` 和 `.js` 扩展名的文件。每个运行库都可以访问 Nakama API 来操作来自客户端的消息，并按需执行逻辑。

受支持的不同语言以 Go -> Lua -> JavaScript 的优先顺序加载。如果在多个运行库中注册了比赛处理程序或 RPC 函数/挂钩，这可以确保确定的行为，从而提供灵活性，以最适合的方式利用不同的运行库，并使它们无缝地协同工作。例如，您可以在 JavaScript 运行库中定义一个 RPC 函数，以创建一个使用一组用 Go 编写的比赛处理程序的比赛。

### JavaScript 运行库

JavaScript 运行库需要一个 `index.js` 文件。要更改将在运行库路径中加载代码的相对文件路径的名称，可以在服务器 YML 中或作为命令标志对其进行设置。

```sh
nakama --runtime.js_entrypoint "some/path/index.js"
```

此路径必须相对于默认或设置的[运行库路径](../../getting-started/configuration/#runtime.path)。

### Go 运行库

Go 运行库寻找 Go 插件 `.so` 共享对象文件。 

参阅[构建 Go 共享对象](../go-runtime/#build-the-go-shared-object)，了解如何用自定义 Go 运行时代码生成此文件。

### Lua 运行库

Lua 运行库将解释和加载任何 `.lua` 文件，包括子目录中的文件。这些可以作为具有相对路径的模块引用。 

每个 Lua 文件代表一个模块，每个模块中的所有代码都将运行并可用于注册函数。

## 运行时上下文

所有运行库中的所有注册函数都会收到 `context` 作为第一个参数。其中包含的字段依赖于执行代码的时间和方式。您可以从上下文中提取有关请求或发出请求的用户的信息：

{{< code type="server" >}}
```lua
local user_id = context.user_id
```
{{< / code >}}

{{< code type="server" >}}
```go
userId, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
if !ok {
  // User ID not found in the context.
}
```
{{< / code >}}

{{< code type="server" >}}
```typescript
let userId = ctx.userId;
```
{{< / code >}}

如果您用 Lua 编写运行时代码，`context` 将是一个您可以从中直接访问字段的表。Go 运行时上下文是标准 `context.Context` 类型，可按如上所示方式访问其字段。在 JavaScript 中，上下文是有属性的纯对象。

{{< table name="nakama.server-framework.basics.context" >}}

### Go 上下文

运行时上下文与 [Go 上下文](https://pkg.go.dev/context)不同。重要的是，`Context` 类型应当包括在所有服务器请求中，避免服务器被死请求（已经断开连接的用户的请求）过载的可能性。

包含 `Context` 就能将上下文取消（在用户的服务器 HTTP 连接关闭时）传播到整条请求链，避免这类死请求的处理和/或积聚。

## 数据库处理程序

运行库包括可用于访问基础游戏数据库的数据库对象。这样您就可以在游戏设计和逻辑中加入自定义 SQL 查询。

数据库处理程序对于可用的数据库连接数有限制。连同其他错误一起，则会导致对用户的响应速率变慢。为避免此类问题，您必须确保一旦完成相关的行，自定义 SQL 查询就适当地释放连接。

如使用 `db.QueryContext()` 或 `db.Query()`，您必须在完成数据库行数据后调用 `row.Close()`。

如使用 `db.QueryRow()` 或 `db.QueryRowContext()`，您必须在完成数据库行数据后调用 `row.Scan` 或 `row.Close()`。

{{< note type="error" >}}
注意，应当尽可能避免使用自定义 SQL，而是首选使用 Nakama 的 [内置功能](../../concepts/)。您还应当避免创建自定义表。如果您的游戏设计需要这些选项之一，请[与 Heroic Labs 联系](mailto:support@heroiclabs.com)后再继续。
{{< / note >}}

## 记录器

服务器运行库中包含的记录器实例使您能够使用以下严重性描述在服务器代码中写入和访问日志消息：`INFO`、`WARN`、`ERROR` 和 `DEBUG`。

请查看用于 [TypeScript](../typescript-runtime/#develop-code)、[Go](../go-runtime/#develop-code) 和 [Lua](../lua-runtime/#develop-code) 运行库的实例。

## Nakama 模块

服务器中内置的代码运行库包含 Nakama 模块。此模块可让您访问一系列用于实现自定义逻辑和行为的函数。

请参阅您首选语言的函数参考，了解可用的函数：

* [TypeScript 函数参考](../typescript-runtime/function-reference/)
* [Go 函数参考](../go-runtime/function-reference/)
* [Lua 函数参考](../lua-runtime/function-reference/)

## 功能

### RPC 函数

借助远程过程调用 (RPC)，可调用运行时代码中注册的函数，对从客户端接收到的消息进行操作，或按需执行自定义逻辑，例如[聊天消息猥亵语言过滤器](../../concepts/chat/#filtering-message-content)。

从客户端和通过[服务器到服务器](#server-to-server)调用均可调用 RPC 函数。

### 挂钩

所有运行时代码都在服务器启动时接受评估，并可用于注册函数 — 这些函数叫做挂钩。您可以注册 [before 挂钩](./hooks/#before-hooks)来拦截和响应客户端消息，注册 [after 挂钩](./hooks/#after-hooks)来在处理事件后调用函数，注册客户端可调用的自定义 [RPC 挂钩](./hooks/#rpc-hooks)。

在运行库中注册函数有多种方法，每种方法都用于处理客户端和服务器之间的特定行为。例如：

{{< code type="server" >}}
```lua
-- NOTE: Function arguments have been omitted in the example.
-- If you are sending requests to the server via the real-time connection, ensure that you use this variant of the function.
nk.register_rt_before()
nk.register_rt_after()

-- Otherwise use this.
nk.register_req_after()
nk.register_req_before()

-- If you'd like to run server code when the matchmaker has matched players together, register your function using the following.
nk.register_matchmaker_matched()

-- If you'd like to run server code when the leaderboard/tournament resets register your function using the following.
nk.register_leaderboard_reset()
nk.register_tournament_reset()

-- Similarly, you can run server code when the tournament ends.
nk.register_tournament_end()
```
{{< / code >}}

{{< code type="server" >}}
```go
// NOTE: All Go runtime registrations must be made in the module's InitModule function.
//       Function arguments have been omitted in the example.

// If you are sending requests to the server via the real-time connection, ensure that you use this variant of the function.
initializer.RegisterBeforeRt()
initializer.RegisterAfterRt()

// Otherwise use the relevant before / after hook, e.g.
initializer.RegisterBeforeAddFriends()
initializer.RegisterAfterAddFriends()
// (...)

// If you'd like to run server code when the matchmaker has matched players together, register your function using the following.
initializer.RegisterMatchmakerMatched()

// If you'd like to run server code when the leaderboard/tournament resets register your function using the following.
initializer.RegisterLeaderboardReset()
initializer.RegisterTournamentReset()

// Similarly, you can run server code when the tournament ends.
initializer.RegisterTournamentEnd()
```
{{< / code >}}

{{< code type="server" >}}
```typescript
// NOTE: All JavaScript runtime registrations must be made in the bundle's InitModule function.
//       Function arguments have been omitted in the example.

// If you are sending requests to the server via the real-time connection, ensure that you use this variant of the function.
initializer.registerRtBefore()
initializer.registerRtAfter()

// Otherwise use the relevant before / after hook, e.g.
initializer.registerBeforeAddFriends()
initializer.registerAfterAddFriends()
// (...)

// If you'd like to run server code when the matchmaker has matched players together, register your function using the following.
initializer.registerMatchmakerMatched()

// If you'd like to run server code when the leaderboard/tournament resets register your function using the following.
initializer.registerLeaderboardReset()
initializer.registerTournamentReset()

// Similarly, you can run server code when the tournament ends.
initializer.registerTournamentEnd()
```
{{< / code >}}

参阅[消息名称](#message-names)中可用挂钩的完整列表。

#### Before 挂钩

可以注册任何函数来拦截从客户端接收的消息，并基于自定义逻辑对其进行操作（或拒绝）。这适用于在服务器的标准功能之外实施特定规则。

在 Go 中，每个挂钩接收到的请求输入都是作为 `struct`，其中包含的数据将由服务器为该请求而处理，如果此功能应该接收输入的话。在 Lua 中，第二个参数将是传入的 `payload`，其中包含的数据将由服务器处理。在 JavaScript 中，`payload` 是第四个参数。

您必须记住在函数末尾以接收到的相同结构返回有效负载。
如您选择返回 `nil` (Lua) 或 `null|undefined` (JavaScript)，而不是 `payload`（或 Go 中的 non-nil `error` ），服务器将停止进一步处理此消息。这可用于阻止服务器接受某些消息或禁用/拉黑某些服务器功能。

#### After 挂钩

类似于 [Before 挂钩](./hooks/#before-hooks)，您可以附加一个函数来操作消息。在管道中处理消息后，将调用注册的函数。在将响应消息发送到客户端后，将异步执行自定义代码。

第二个参数是“传出有效负载”，其中包含服务器对请求的响应。第三个参数包含“传入有效负载”，其中包含最初为此请求传递给服务器的数据。

After 挂钩无法更改发送回客户端的响应有效负载，并且错误也不会阻止发送响应。

#### RPC 挂钩

客户端和服务器之间的一些逻辑最好作为客户端可以执行的 RPC 函数来处理。为此 Nakama 支持自定义 RPC 挂钩注册。

可以在客户端代码中使用注册的 RPC 的 ID 来发送 RPC 消息，以便在服务器上执行函数并返回结果。

从 Go 运行时代码返回的结果为 `(string, error)`。从 Lua 运行时代码中，结果总是作为 Lua 字符串（或者可选的 `nil`）返回。从 JavaScript 运行时代码，结果始终都应当是字符串、`null` 或被省略（未定义）。

### 服务器到服务器

您可以[检查上下文是否有用户 ID](#RUNTIME_CTX_USER_ID)，以查看 RPC 函数是客户端还是服务器到服务器调用。服务器到服务器调用从不具有用户 ID。如果您希望将函数限定为永远不能从客户端访问，那么只要在上下文中找到用户 ID 就返回一个错误。

请参阅[服务器运行库示例](../runtime-examples/#server-to-server)。

### 运行一次

运行时环境允许您运行只能执行一次的代码。如果您需要执行或注册来自第三方服务的自定义 SQL 查询，这非常实用。

请参阅 [TypeScript](../typescript-runtime/code-samples/#database-handler)、[Go](../go-runtime/code-samples/#database-handler) 或 [Lua](../lua-runtime/code-samples/#database-handler) 的实现示例。

## 消息名称

{{< note "important" >}}
如果运行时代码为 Go，请参阅[界面定义](https://github.com/heroiclabs/nakama/blob/master/server/runtime.go)中的完整挂钩列表，这可从运行库包中获得。
{{< / note >}}

使用以下请求名称注册 [Before](./hooks/#before-hooks) 和 [After](./hooks/#after-hooks) 挂钩：

{{< table name="nakama.server-framework.basics.request-names" >}}

名称不区分大小写。更多信息，请参阅[`apigrpc.proto`](https://github.com/heroiclabs/nakama/blob/master/apigrpc/apigrpc.proto)。

对于实时 before 和 after 挂钩，请使用以下消息名称：

{{< table name="nakama.server-framework.basics.message-names" >}}

名称不区分大小写。更多信息，请参阅 [`realtime.proto`](https://github.com/heroiclabs/nakama-common/blob/master/rtapi/realtime.proto)。

## 限制

请参阅 [TypeScript](../typescript-runtime/#restrictions)、[Go](../go-runtime/#restrictions) 和 [Lua](../lua-runtime/#restrictions) 页面，了解运行库特有限制。

### 后台作业

为了避免“死机”（用户不在时宕机）和不必要的服务器负载，应该避免后台作业，而是采用[事件](../../concepts/events/)驱动的路由。在这种方法中，客户端在用户返回时进行 [RPC](#rpc-functions) 调用，并在该调用函数中执行游戏逻辑和用例所需的任何更新。

计划的后台作业将做不必要的工作，即在整个用户群中执行这些更新，而不管用户是否处于非活动状态，但这种方法可确保只为仍在游戏中活动的用户执行工作。

使用后台作业可能会导致进一步的问题，因为任何作业都将限于一个 Nakama 实例，这就需要跨所有实例进行复制，或者跨实例的非同质工作负载。
