# 游戏集成

### 为什么我需要一个匹配器和/或大厅？

大厅服务（[托管集群](/zh/learn/advanced-features/managed-clusters.md#nakama-by-heroic-labs)）系统在在线多人游戏中因各种技术原因发挥着关键作用。

**玩家管理**

大厅或匹配器系统通过根据技能水平、地理位置或游戏偏好等各种条件将玩家分组，帮助管理玩家。这可确保所有参与玩家获得平衡且愉快的游戏体验。

**后端**

您不希望玩家直接与您的后端通信，并且希望为他们提供一个集中访问点。想象一下您有一个可以在某个提供商处启动虚拟机的 API 令牌，您不希望该令牌硬编码在游戏客户端中，任何玩家都能访问到！

**安全性**

使用集中的大厅或匹配器系统有助于维护游戏的安全性。通过过滤和验证玩家连接，匹配器可以防止未授权访问、保护用户数据，并减轻潜在的作弊或黑客攻击。

**游戏会话管理**

大厅或匹配器系统通过根据需要创建、更新和终止会话来管理游戏会话。这确保玩家可以无缝加入或离开游戏，并且系统可以高效地管理游戏实例。

<figure><img src="/files/bda314e4a988ee95af727b11adfd0ed9eff2a59c" alt=""><figcaption></figcaption></figure>

### 您不必立即使用！（开发模式）

在开发阶段，出于若干技术原因，您可能不需要一个功能完善的大堂或匹配器系统：

**简化的测试环境**

在开发的初期阶段，您可能会专注于实现和测试核心游戏机制和网络功能。通过避免大厅或匹配器系统的复杂性，您可以更容易地在较小的玩家群体中（通常是开发团队内部）测试和调试游戏。

**更快的迭代**

没有大厅或匹配器，您可以快速更改游戏代码并测试新功能或修复，而无需担心对匹配流程的影响。这允许更敏捷的开发方法，使团队能够更快地迭代和完善游戏机制。

**资源分配**

开发一个完整规模的大堂或匹配器系统需要相当多的时间和资源。通过推迟实现这些组件，您可以更有效地分配资源，先专注于核心玩法和网络功能。

**可扩展性问题**

在开发早期，您可能不会有大量玩家需要管理。因此，用于连接玩家的基本系统，甚至用于测试目的的手动连接，可能就足够了。然而，随着游戏增长并吸引更多玩家，大厅或匹配器系统将变得越来越必要，以管理玩家连接并确保平稳的游戏体验。

重要的是将您的游戏架构设计为将来能够容纳大厅或匹配器系统。具有前瞻性的设计会让您在准备好以更大规模测试游戏时更容易集成这些组件，从而实现更平滑的过渡和更完善的最终产品。

### 与 Edgegap API 交互

Edgegap 提供了一个简单的 API 来与中继会话交互，以授权您的玩家连接到最近的中继以获得更低的延迟。

<figure><img src="/files/62a53298c1b821046777178ca23cef1d921b434b" alt=""><figcaption></figcaption></figure>

要与该 API 交互，您需要在请求中提供一个 `授权头令牌` 。

{% hint style="info" %}
您可以通过注册账户并生成一个中继配置文件来获取令牌。 [**在此注册**](https://app.edgegap.com/auth/register)

请注意，您需要一个专门用于中继的令牌。如果您已经有一个标准的 API 令牌，它将无法使用。
{% endhint %}

这是可以从仪表板侧边栏访问的。

<figure><img src="/files/cb4939d2156136f5ce5b46237364fd8962eb46b5" alt=""><figcaption></figcaption></figure>

一旦您拥有令牌，请在所有 API 调用的请求头中包含它。别忘了在 API 密钥前加上前缀 `令牌`.

{% hint style="success" %}
如果您不熟悉使用 API，可以参考 [本节](#interacting-with-api-curl-or-postman) 以帮助您入门。
{% endhint %}

### 创建中继会话

要使用分布式中继，您需要创建一个中继会话。这是通过发送一个 `POST` 请求到 `/v1/relays/sessions` 端点并带有 JSON 负载来完成。会话可以手动创建、通过匹配器创建、通过大厅创建，或通过您自己的自定义服务创建。

这将实时动态选择最适合您玩家的可用中继，并创建访问该中继的授权。然而，我们建议在匹配时预先筛选以将玩家分组在同一地区，以避免他们之间产生极远距离。

{% hint style="info" %}
这一步不会直接将您的玩家连接到中继；您需要在流程的后续阶段处理连接。这一步仅为您提供一个用于建立连接的中继。
{% endhint %}

示例请求：

```bash
POST - /v1/relays/sessions
```

负载应包含一个用户对象数组，每个对象包含用户的 IP 地址。您也可以包含一个 `webhook_url`，这是将接收与会话相关通知的 URL。

示例负载：

```json
{
  "users": [
    {
      "ip": "1.1.1.1"
    },
    {
      "ip": "2.2.2.2"
    }
  ],
  "webhook_url": "https://webhook.example.com/notify"
}
```

{% hint style="info" %}
其中 `"users"` 是包含 `"ip"` 的用户对象数组，表示会话中每个用户的 `"webhook_url"` 是可选的用于接收会话更新的 URL。

发送 `POST` 请求后，您将收到一个包含 `session_id`的 JSON 响应，该 ID 在稍后检索会话信息时需要使用。
{% endhint %}

示例响应：

```json
{
  "session_id": "3960c873aafd-S",
  "authorization_token": null,
  "status": "Initializing",
  "ready": false,
  "linked": false,
  "error": null,
  "session_users": [],
  "relay": null,
  "webhook_url": "https://webhook.example.com/notify"
}
```

### 获取有关中继会话的信息

要检索会话信息，请发送一个 `GET` 请求到 `/v1/relays/sessions/{session_id}` 端点，并附带从 `session_id` 请求中获得的 `POST` 。

示例请求：

```bash
GET - /v1/relays/sessions/3960c873aafd-S
```

响应将包含有关会话的信息，包括会话状态、用户信息和中继信息。

期望的响应将包含如下会话信息：

```json
{
  "session_id": "3960c873aafd-S",
  "authorization_token": 1031196689,
  "status": "Linked",
  "ready": true,
  "linked": true,
  "error": null,
  "session_users": [
    {
      "ip_address": "2.2.2.2",
      "latitude": 48.8602294921875,
      "longitude": 2.34106993675232,
      "authorization_token": 3499933322
    },
    {
      "ip_address": "1.1.1.1",
      "latitude": -37.7036018371582,
      "longitude": 145.180633544922,
      "authorization_token": 4261594560
    }
  ],
  "relay": {
    "ip": "178.79.131.238",
    "host": "cc84b011777b.pr.edgegap.net",
    "latitude": 45.513707,
      "server": {
        "port": 31527,
        "protocol": "UDP",
        "link": "cc84b011777b.pr.edgegap.net:31527"
      },
      "client": {
        "port": 32089,
        "protocol": "UDP",
        "link": "cc84b011777b.pr.edgegap.net:32089"
      }
    }
  },
  "webhook_url": "https://webhook.example.com/notify"
}
```

获取到最近中继的授权可能需要一点时间，因此当字段 `ready` 为 `@Super Port!` 时，您可以提取 JSON 数据并使用它。

[更多细节在此](/zh/docs/api.md)

{% hint style="success" %}
或者，您可以在创建中继会话时使用 \`webhook\_url\` 参数，以在会话成功分配到中继或分配失败时收到通知。
{% endhint %}

### 终止中继会话

当您决定结束与中继的连接时，可以轻松终止中继会话。您需要发送一个 `DELETE` 请求到以下端点：

```bash
DELETE - /v1/relays/sessions/{session_id}
```

{% hint style="info" %}
将 `{session_id}` 替换为您想要终止的实际会话 ID。
{% endhint %}

返回 204 无内容的响应表示会话已成功删除。玩家将失去对中继的访问，并且您将不再为该会话付费。

{% hint style="warning" %}
正确终止会话非常重要，以避免留下任何未使用的资源分配，这可能影响性能并产生不必要的费用。
{% endhint %}

***

### C# 示例

这是一个 C# 示例，您的大厅或匹配器可以使用它来创建中继会话并提取需要返回给游戏客户端的数据。

{% hint style="info" %}
此示例使用 C# 版本 > 7.0
{% endhint %}

```csharp
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace LobbyNamespace
{
    public class EdgegapAPI
    {
        private readonly HttpClient _httpClient;

        public EdgegapAPI(string apiToken)
        {
            _httpClient = new HttpClient();
            _httpClient.DefaultRequestHeaders.Add("Authorization", $"Token {apiToken}");
        }

        public async Task<RelaySessionAuthorization> CreateRelaySession(List<string> ips)
        {
            var jsonBody = new
            {
                users = ips.ConvertAll(ip => new { ip })
            };

            var postResponse = await _httpClient.PostAsJsonAsync("https://api.edgegap.com/v1/relays/sessions", jsonBody);
            var postJsonData = await postResponse.Content.ReadFromJsonAsync<Dictionary<string, object>>();

            if (!postJsonData.ContainsKey("session_id"))
            {
                throw new Exception("Failed to create relay session");
            }

            var sessionId = postJsonData["session_id"].ToString();

            var retries = 5;
            while (retries > 0)
            {
                var getResponse = await _httpClient.GetAsync($"https://api.edgegap.com/v1/relays/sessions/{sessionId}");
                var getJsonData = await getResponse.Content.ReadFromJsonAsync<Dictionary<string, object>>();
                var ready = (JsonElement)getJsonData["ready"];

                if (ready.ValueKind == JsonValueKind.True)
                {
                    var usersData = (JsonElement)getJsonData["session_users"];
                    var userAuthorizationTokens = new Dictionary<string, UInt32>();
                    foreach (var user in usersData.EnumerateArray())
                    {
                        var ipAddress = user.GetProperty("ip_address").GetString();
                        var userAuthorizationToken = user.GetProperty("authorization_token").GetUInt32();
                        userAuthorizationTokens[ipAddress] = userAuthorizationToken;
                    }

                    var relayData = (JsonElement)getJsonData["relay"];
                    var relayAddress = relayData.GetProperty("host").GetString();
                    var relayServerPort = relayData.GetProperty("ports").GetProperty("server").GetProperty("port").GetInt32();
                    var relayClientPort = relayData.GetProperty("ports").GetProperty("client").GetProperty("port").GetInt32();

                    return new RelaySessionAuthorization
                    {
                        SessionAuthorizationToken = sessionId,
                        UserAuthorizationTokens = userAuthorizationTokens,
                        RelayAddress = relayAddress,
                        RelayServerPort = relayServerPort,
                        RelayClientPort = relayClientPort
                    };
                }

                retries--;
                await Task.Delay(2000); // wait for 2 second before retrying
            }

            throw new Exception("Failed to get a ready relay session");
        }
    }

    public class RelaySessionAuthorization
    {
        public string? SessionAuthorizationToken { get; set; }
        public Dictionary<string, UInt32>? UserAuthorizationTokens { get; set; }
        public string? RelayAddress { get; set; }
        public int RelayServerPort { get; set; }
        public int RelayClientPort { get; set; }
    }
}
```

您需要将此信息返回给相应的玩家。

充当服务器的玩家将需要 `RelayServerPort` 而其他所有玩家将需要 `RelayClientPort`

```csharp
using System.Text.Json;
using LobbyNamespace;

class Program
{
    static async Task Main(string[] args)
    {
        // 将 YOUR_API_TOKEN 替换为您实际的 API 令牌
        var apiToken = "YOUR_API_TOKEN";

        // 1.1.1.1 是作为服务器的玩家
        // 2.2.2.2 是作为客户端的玩家
        var ips = new List<string> { "1.1.1.1", "2.2.2.2" };

        var edgegapAPI = new EdgegapAPI(apiToken);
        var relaySessionAuthorization = await edgegapAPI.CreateRelaySession(ips);

        // 为服务器 (1.1.1.1) 生成 JSON 正文
        var serverJsonBody = new
        {
            relay_address = relaySessionAuthorization.RelayAddress,
            port = relaySessionAuthorization.RelayServerPort,
            session_authorization_token = relaySessionAuthorization.SessionAuthorizationToken,
            user_authorization_token = relaySessionAuthorization.UserAuthorizationTokens["1.1.1.1"]
        };
        var serverJson = JsonSerializer.Serialize(serverJsonBody);
        Console.WriteLine($"JSON body for server (1.1.1.1):\n{serverJson}");

        // 为客户端 (2.2.2.2) 生成 JSON 正文
        var clientJsonBody = new
        {
            relay_address = relaySessionAuthorization.RelayAddress,
            port = relaySessionAuthorization.RelayClientPort,
            session_authorization_token = relaySessionAuthorization.SessionAuthorizationToken,
            user_authorization_token = relaySessionAuthorization.UserAuthorizationTokens["2.2.2.2"]
        };
        var clientJson = JsonSerializer.Serialize(clientJsonBody);
        Console.WriteLine($"JSON body for client (2.2.2.2):\n{clientJson}");
    }
}
```

### 与 API 交互（cURL 或 POSTMAN）

在开发游戏时，您需要与 API 交互以创建、管理和删除中继会话。您可以使用像 cURL 或 POSTMAN 这样的工具向 API 发送 HTTP 请求。

cURL 是一个命令行工具，允许您从终端直接发出 HTTP 请求并与 API 交互。POSTMAN 是一个流行的图形用户界面（GUI）应用程序，通过提供便捷的界面来创建、发送和分析 HTTP 请求，从而简化了 API 测试。

以下是使用 cURL 创建中继会话的示例：

```bash
curl -X POST -H "Content-Type: application/json" -H "Authorization: Token API_TOKEN" -d '{
  "users": [
    {
      "ip": "1.1.1.1"
    },
    {
      "ip": "2.2.2.2"
    }
  ],
  "webhook_url": "https://webhook.example.com/notify"
}' "https://api.edgegap.com/v1/relays/sessions"
```

{% hint style="info" %}
请记得将 `API_TOKEN` 替换为您实际的中继 API 令牌。
{% endhint %}

API 还会返回包含数据的响应。您需要解析该响应并提取所需的数据。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.edgegap.com/zh/learn/distributed-relay/relay-edgegap-api.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
