1. 系统架构
MC 中转系统基于 FRP(Fast Reverse Proxy)实现多房间 Minecraft Java 版联机中转。服务器运行 frps 作为中转枢纽,客户端应用(一号端)通过 frpc 将本地 MC LAN 端口隧道到服务器公网端口,玩家连接服务器对应端口即可加入游戏。
1.1 架构图
┌─────────────┐ FRP 隧道 ┌───────────────────────┐ ┌─────────────┐
│ 一号端(房主) │ ───────frpc:7000──────▶ │ 服务器 39.104.52.226 │ ◀────── │ 玩家客户端 │
│ MC LAN 端口 │ localPort → remotePort │ frps + Nginx + Node │ 连接端口 │ mc.yuki-can.top│
│ (如 49152) │ │ 25566~25575 端口池 │ (如 25566)│ :25566 │
└─────────────┘ └───────────────────────┘ └─────────────┘
▲ ▲
│ claim/heartbeat/release │ MC SLP 探测
│ (HTTP API) │ (每5秒)
▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ mc_status.js (Node.js, port 3005) │
│ 端口池管理 | 心跳检测 | MC SLP 探测 | 带宽监控 | 健康检查 │
└──────────────────────────────────────────────────────────────┘
1.2 组件说明
| 组件 | 位置 | 端口 | 职责 |
|---|---|---|---|
| frps | 服务器 C:\Users\Administrator\frp_0.69.1_windows_amd64\ | 7000 (TCP) / 7500 (Dashboard) | FRP 隧道服务端,转发流量 |
| Nginx | 服务器 C:\nginx\ | 80 / 443 | HTTPS 反代 mc_status API + 静态页面 |
| mc_status.js | 服务器 C:\api_server\mc_status.js | 3005 (PM2 托管) | 核心后端:端口池/心跳/探测/带宽/健康 |
| mc_keys.json | 服务器 C:\api_server\mc_keys.json | - | API Key 持久化存储 |
| 客户端应用 | 一号端 PC | - | 检测MC端口、claim端口、启动frpc、发送心跳 |
1.3 端口规划
| 端口范围 | 协议 | 用途 | 数量 |
|---|---|---|---|
| 7000 | TCP | frps 主端口(隧道建立) | 1 |
| 7500 | TCP | frps Dashboard(管理面板) | 1 |
| 25566 ~ 25575 | TCP + UDP | MC 房间端口池 | 10 |
| 33890 | TCP | RDP 远程桌面中转 | 1 |
| 3005 | TCP | mc_status.js API 服务 | 1 |
Base URL:
本地调试:
所有 API 返回 JSON,Content-Type: application/json; charset=utf-8
https://mc.yuki-can.top本地调试:
http://127.0.0.1:3005所有 API 返回 JSON,Content-Type: application/json; charset=utf-8
2. 核心概念
2.1 端口池(Port Pool)
服务器维护一组预分配端口(25566~25575),每个端口对应一个"房间"。端口有三态:
| 状态 | 含义 | 判定条件 | 颜色 |
|---|---|---|---|
available | 空闲,无人使用 | 无 API Key 绑定 + 无 FRP proxy + SLP 无响应 | 灰 |
tunneling | 已申领,frpc 已连接但 MC 未开 | 有 Key 绑定 + FRP proxy 存在 + SLP 无响应 | 琥珀 |
online | MC 服务器在线,可加入 | SLP 有响应(无论 Key 状态) | 绿 |
2.2 API Key
每个房间需要一个 API Key 来申领端口。Key 的核心规则:
- 一对一绑定:一个 Key 同时只能绑定一个端口
- 持久化:Key 存储在
mc_keys.json,服务器重启不丢失 - 状态流转:
idle(空闲)→in-use(已绑定端口)→idle(释放后) - 管理入口:
https://mc.yuki-can.top/key(密码门禁) - 格式:32位随机十六进制字符串,如
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
2.3 心跳机制(Heartbeat)
客户端申领端口后,必须定期发送心跳包以保持端口占用。超时未收到心跳,后端自动释放端口。
| 参数 | 默认值 | 说明 |
|---|---|---|
heartbeat_interval | 15 秒 | 客户端发送心跳的推荐间隔 |
heartbeat_timeout | 30 秒 | 超过此时间未收到心跳 → 自动释放 |
check_interval | 5 秒 | 后端扫描超时的间隔 |
心跳超时释放流程:后端每 5 秒检查所有已分配端口,若某端口超过 30 秒未收到心跳:
- 将该端口状态重置为
available - 将绑定的 Key 状态重置为
idle - 该端口立即可被其他 Key 申领
- 客户端会收到下一次心跳请求返回
409 Conflict(端口已释放)
2.4 呼吸包 / 健康探测
除了客户端主动心跳,后端还有独立的服务器端探测机制:
- MC SLP 探测:每 5 秒对所有端口 25566~25575 发送 Minecraft Server List Ping,判断 MC 服务是否在线、读取玩家数/MOTD/版本
- FRP proxy 检测:每 2 秒查询 frps Dashboard API,获取各 proxy 状态和流量数据。若 proxy 消失(frpc 断开),且该端口超过心跳超时,触发释放
- 端口可达性检测:TCP connect 探测端口是否在监听,作为 SLP 的前置判断
3. 房间生命周期
available
─claim─▶
tunneling
─SLP─▶
online
─release─▶
available
3.1 完整流程(客户端视角)
1. POST /api/rooms/claim { "key": "a1b2c3..." }
← 200 { "port": 25566, "heartbeat_interval": 15, "heartbeat_timeout": 30 }
2. [客户端自动检测 MC LAN 端口,如 49152]
3. [客户端生成 frpc.toml:
localPort = 49152
remotePort = 25566
proxy 名称 = mc-25566-tcp / mc-25566-udp ]
4. [客户端启动 frpc.exe -c frpc.toml]
→ frpc 连接 frps:7000
→ 后端检测到 proxy mc-25566-tcp 出现 → 状态变为 tunneling
5. [MC 开放局域网,SLP 开始响应]
→ 后端 SLP 探测 25566 有响应 → 状态变为 online
→ 玩家可连 mc.yuki-can.top:25566
6. 每 15 秒: POST /api/rooms/heartbeat { "key": "a1b2c3...", "port": 25566 }
← 200 { "ok": true, "ttl": 30 }
7. 关服时: POST /api/rooms/release { "key": "a1b2c3...", "port": 25566 }
← 200 { "ok": true, "port": 25566, "released": true }
3.2 异常场景
| 场景 | 后端行为 | 客户端应对 |
|---|---|---|
| frpc 进程崩溃 | FRP proxy 消失 → 端口转 tunneling → 心跳超时后释放 | 重新启动 frpc,若端口已释放需重新 claim |
| 网络断连 | 心跳停止 → 30秒后自动释放 | 重连后 heartbeat 返回 409 → 重新 claim |
| MC 崩溃但 frpc 在 | proxy 还在 → tunneling,SLP 无响应 | 重启 MC 开放局域网即可 |
| 服务器重启 | 端口池状态全清空(内存),Key 文件保留 | 客户端重新 claim + 重启 frpc |
| Key 被管理员删除 | 若该 Key 正在用,立即释放端口 | heartbeat 返回 403 → 需获取新 Key |
4. 数据模型
4.1 Room 对象
{
"port": 25566, // 房间端口号
"status": "online", // "available" | "tunneling" | "online"
"key_bound": true, // 是否有 Key 绑定
"key_name": "YuKi的房间", // Key 的名称备注(仅在线时返回)
"last_heartbeat": "2026-08-08T21:30:00Z", // 最后一次心跳时间
"heartbeat_ttl": 18, // 心跳剩余存活秒数(-1 表示无绑定)
"frp_proxy": true, // FRP proxy 是否存在
"address": "mc.yuki-can.top:25566", // 玩家连接地址
"mc": { // MC SLP 数据,offline 时为 null
"online": true,
"latency": 42, // 延迟 ms
"version": { "name": "26.2", "protocol": 776 },
"players": { "online": 2, "max": 8 },
"description": { "text": "YuKi_Can - 新的世界" },
"favicon": null, // base64 图标(可能为 null)
"forgeData": null, // Forge 模组数据(原版为 null)
"modinfo": null, // 旧版模组信息
"enforcesSecureChat": false,
"previewsChat": false,
"extras": {} // 其他非标准字段
},
"bandwidth": { // 该房间实时带宽
"up_rate": 51200, // 上行速率 bytes/sec
"down_rate": 38400, // 下行速率 bytes/sec
"up_total": 1048576, // 上行总量 bytes(今日累计)
"down_total": 786432, // 下行总量 bytes(今日累计)
"conns": 3 // 当前连接数
}
}
4.2 Key 对象
{
"key": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", // 32位十六进制
"name": "YuKi的房间", // 名称备注
"status": "idle", // "idle" | "in-use"
"bound_port": null, // 绑定端口(idle 时为 null)
"created_at": "2026-08-08T20:00:00Z",
"last_active": "2026-08-08T21:30:00Z" // 最后活跃时间
}
4.3 通用响应包装
// 成功
{ "ok": true, "data": { ... } }
// 失败
{
"ok": false,
"error": {
"code": "KEY_IN_USE",
"message": "This key is already bound to port 25567"
}
}
5. 申领端口
POST
/api/rooms/claim
需要 API Key
从端口池申领一个空闲端口。一个 Key 只能同时绑定一个端口。
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
key | string | 必填 | API Key(32位十六进制) |
请求示例
POST /api/rooms/claim HTTP/1.1
Content-Type: application/json
{
"key": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
}
成功响应 (200)
{
"ok": true,
"data": {
"port": 25566,
"heartbeat_interval": 15,
"heartbeat_timeout": 30,
"server": "39.104.52.226",
"frps_port": 7000,
"token": "Z0nFrPw8yKi", // frpc 认证 token
"proxy_prefix": "mc-25566" // proxy 命名前缀
}
}
错误响应
| HTTP | error.code | 说明 |
|---|---|---|
| 400 | MISSING_KEY | 未提供 key 字段 |
| 403 | INVALID_KEY | Key 不存在或已被删除 |
| 409 | KEY_IN_USE | 该 Key 已绑定其他端口 |
| 503 | NO_PORT_AVAILABLE | 端口池已满,10个房间全被占用 |
客户端动作(claim 成功后):
- 用返回的
port、token、proxy_prefix生成 frpc.toml - 自动检测 MC LAN 端口,填入
localPort - 启动
frpc.exe -c frpc.toml - 启动心跳定时器,间隔 =
heartbeat_interval秒
6. 发送心跳
POST
/api/rooms/heartbeat
需要 API Key
保活已申领的端口。必须在 heartbeat_timeout(30秒)内至少发送一次。
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
key | string | 必填 | API Key |
port | integer | 必填 | 已申领的端口号 |
请求示例
POST /api/rooms/heartbeat HTTP/1.1
Content-Type: application/json
{
"key": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"port": 25566
}
成功响应 (200)
{
"ok": true,
"data": {
"ttl": 30, // 剩余存活秒数(重置为 30)
"port": 25566,
"mc_online": true // MC 是否已在线(方便客户端判断)
}
}
错误响应
| HTTP | error.code | 说明 | 客户端处理 |
|---|---|---|---|
| 403 | INVALID_KEY | Key 不存在 | 提示用户获取新 Key |
| 409 | PORT_RELEASED | 端口已超时释放 | 重新 claim |
| 409 | PORT_MISMATCH | Key 绑定的端口与请求不符 | 重新 claim |
心跳策略建议:客户端每
heartbeat_interval(15秒)发送一次。建议实现重试机制——网络抖动时最多重试 3 次,间隔 2 秒。连续 2 次失败后提示用户检查网络。
7. 释放端口
POST
/api/rooms/release
需要 API Key
主动释放已申领的端口。关服或退出时调用。
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
key | string | 必填 | API Key |
port | integer | 必填 | 要释放的端口号 |
成功响应 (200)
{
"ok": true,
"data": {
"port": 25566,
"released": true
}
}
幂等性:重复 release 同一端口返回 200 但
released: false(已经释放过了)。不会报错。
8. 获取所有房间
GET
/api/rooms
无需认证
返回端口池中所有 10 个房间的状态总览。数据每 5 秒更新。
成功响应 (200)
{
"ok": true,
"data": {
"total": 10,
"online_count": 2,
"tunneling_count": 1,
"available_count": 7,
"rooms": [
{
"port": 25566,
"status": "online",
"key_bound": true,
"address": "mc.yuki-can.top:25566",
"players_online": 2,
"players_max": 8,
"latency": 42,
"version": "26.2",
"motd": "YuKi_Can - 新的世界",
"description": "YuKi的房间"
},
{
"port": 25567,
"tunneling",
"key_bound": true,
"address": "mc.yuki-can.top:25567",
"heartbeat_ttl": 22
},
{
"port": 25568,
"status": "available",
"key_bound": false,
"address": "mc.yuki-can.top:25568"
}
]
}
}
9. 获取单个房间详情
GET
/api/rooms/:port
无需认证
返回指定端口的完整房间信息,包含完整 MC SLP 数据和带宽。
路径参数
| 参数 | 类型 | 说明 |
|---|---|---|
port | integer | 端口号(25566~25575) |
成功响应 (200)
返回完整的 Room 对象(见 4.1 Room 对象),包含 mc(完整 SLP 数据)和 bandwidth 字段。
错误响应
| HTTP | error.code | 说明 |
|---|---|---|
| 404 | PORT_NOT_IN_POOL | 端口号不在 25566~25575 范围内 |
10. 端口池状态
GET
/api/ports
无需认证
返回端口池中每个端口的状态。适合客户端在选择端口时查询。
成功响应 (200)
{
"ok": true,
"data": {
"range": [25566, 25575],
"ports": [
{ "port": 25566, "status": "online", "key_bound": true },
{ "port": 25567, "status": "tunneling", "key_bound": true },
{ "port": 25568, "status": "available", "key_bound": false },
{ "port": 25569, "status": "available", "key_bound": false }
]
}
}
11. 可用端口列表
GET
/api/ports/available
无需认证
仅返回空闲端口列表。claim 前调用此接口选择端口。
成功响应 (200)
{
"ok": true,
"data": {
"available": [25568, 25569, 25570, 25571, 25572, 25573, 25574, 25575],
"count": 8
}
}
12. 带宽监测
GET
/api/bandwidth
无需认证
返回服务器总带宽和各房间带宽。数据每 2 秒更新。
成功响应 (200)
{
"ok": true,
"data": {
"total": {
"up_rate": 125000, // 服务器总上行 bytes/sec
"down_rate": 89000, // 服务器总下行 bytes/sec
"up_total": 1073741824, // 启动以来总上传 bytes
"down_total": 536870912, // 启动以来总下载 bytes
"interface": "Ethernet0" // 网卡名称
},
"rooms": {
"25566": {
"up_rate": 51200,
"down_rate": 38400,
"up_total": 1048576,
"down_total": 786432,
"conns": 3
},
"25567": {
"up_rate": 0,
"down_rate": 0,
"up_total": 0,
"down_total": 0,
"conns": 0
}
}
}
}
单位说明:所有带宽值的单位均为
bytes(字节),速率为 bytes/sec。客户端可自行转换为 KB/s 或 Mbps(1 Mbps = 125000 bytes/sec)。
13. 服务器健康监测
GET
/api/health
无需认证
返回服务器各组件运行状态、系统资源使用情况。
成功响应 (200)
{
"ok": true,
"data": {
"services": {
"frps": { "status": "up", "pid": 1234, "uptime": 3600 },
"nginx": { "status": "up", "pid": 5678, "uptime": 7200 },
"mc_status": { "status": "up", "pid": 9012, "uptime": 1800, "pm2": "online" },
"counter": { "status": "up", "port": 3001 },
"bili_proxy": { "status": "up", "port": 3002 },
"gaokao": { "status": "up", "port": 3004 }
},
"system": {
"cpu_usage": 12.5, // CPU 使用率 %
"cpu_cores": 4,
"memory": {
"total": 8589934592, // 总内存 bytes (8GB)
"used": 4294967296, // 已用 bytes (4GB)
"usage": 50.0 // 使用率 %
},
"disk": {
"total": 107374182400, // 总磁盘 bytes (100GB)
"used": 53687091200, // 已用 bytes (50GB)
"usage": 50.0 // 使用率 %
},
"uptime": 86400, // 服务器启动时长(秒)
"load_avg": [0.25, 0.30, 0.28] // 1/5/15分钟负载
},
"timestamp": "2026-08-08T21:45:00Z"
}
}
14. 向后兼容接口
GET
/api/status
无需认证
向后兼容旧接口。返回第一个 online 房间的 MC 状态。如果没有在线房间,返回 offline。
成功响应 (200) — 有在线房间
{
"online": true,
"port": 25566,
"latency": 42,
"players": { "online": 2, "max": 8 },
"version": { "name": "26.2", "protocol": 776 },
"description": { "text": "YuKi_Can - 新的世界" },
"timestamp": "2026-08-08T21:45:00Z"
}
成功响应 (200) — 无在线房间
{
"online": false,
"timestamp": "2026-08-08T21:45:00Z"
}
15. 管理接口 — 列出所有 Key
GET
/api/admin/keys
管理密码
列出所有 API Key 及其状态。需要管理员密码认证。
认证方式
HTTP Basic Auth,用户名 admin,密码为管理页面密码。
GET /api/admin/keys HTTP/1.1
Authorization: Basic YWRtaW46eHh4eHg= // base64(admin:password)
成功响应 (200)
{
"ok": true,
"data": {
"keys": [
{
"key": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"name": "YuKi的房间",
"status": "in-use",
"bound_port": 25566,
"created_at": "2026-08-08T20:00:00Z",
"last_active": "2026-08-08T21:30:00Z"
},
{
"key": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a1",
"name": "朋友A",
"status": "idle",
"bound_port": null,
"created_at": "2026-08-08T19:00:00Z",
"last_active": null
}
],
"count": 2
}
}
16. 管理接口 — 添加 Key
POST
/api/admin/keys
管理密码
添加一个新 Key。可指定自定义 Key 值或让服务器随机生成。
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 必填 | Key 名称备注 |
key | string | 可选 | 自定义 Key 值,不填则随机生成 |
请求示例
POST /api/admin/keys HTTP/1.1
Authorization: Basic YWRtaW46eHh4eHg=
Content-Type: application/json
{
"name": "朋友B"
}
成功响应 (201)
{
"ok": true,
"data": {
"key": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6a1b2",
"name": "朋友B",
"status": "idle",
"created_at": "2026-08-08T21:45:00Z"
}
}
错误响应
| HTTP | error.code | 说明 |
|---|---|---|
| 409 | KEY_EXISTS | 自定义 Key 值已存在 |
17. 管理接口 — 删除 Key
DELETE
/api/admin/keys/:key
管理密码
删除指定 Key。如果该 Key 正在使用中(绑定了端口),会先释放端口再删除。
路径参数
| 参数 | 类型 | 说明 |
|---|---|---|
key | string | 要删除的 Key 值 |
成功响应 (200)
{
"ok": true,
"data": {
"deleted": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6a1b2",
"released_port": null // 若删除时绑定了端口,这里返回端口号
}
}
错误响应
| HTTP | error.code | 说明 |
|---|---|---|
| 404 | KEY_NOT_FOUND | Key 不存在 |
18. frpc 配置规范
客户端应用生成 frpc.toml 时必须遵守以下规范,否则后端无法正确识别房间。
18.1 配置模板
# ── frpc.toml(客户端自动生成)──
# 由 MC 中转客户端应用生成
# 房间端口: {remotePort}
# 本地MC端口: {localPort}
serverAddr = "39.104.52.226"
serverPort = 7000
auth.token = "{token}"
# ── TCP 隧道(MC 游戏数据)──
[[proxies]]
name = "mc-{port}-tcp"
type = "tcp"
localIP = "127.0.0.1"
localPort = {localPort}
remotePort = {remotePort}
transport.useCompression = true
# ── UDP 隧道(MC 局域网发现)──
[[proxies]]
name = "mc-{port}-udp"
type = "udp"
localIP = "127.0.0.1"
localPort = {localPort}
remotePort = {remotePort}
transport.useCompression = true
18.2 变量说明
| 变量 | 来源 | 示例 | 说明 |
|---|---|---|---|
{token} | claim 响应的 token 字段 | Z0nFrPw8yKi | frps 认证令牌 |
{port} | claim 响应的 port 字段 | 25566 | 服务器分配的房间端口 |
{remotePort} | = {port} | 25566 | frpc 远程端口 |
{localPort} | 客户端自动检测 | 49152 | MC LAN 开放后的本地端口 |
18.3 Proxy 命名规则
必须严格遵守:proxy 名称格式为
mc-{端口号}-tcp 和 mc-{端口号}-udp。后端通过此命名匹配 FRP 流量到对应房间。命名错误会导致带宽数据无法关联。
18.4 完整示例
serverAddr = "39.104.52.226"
serverPort = 7000
auth.token = "Z0nFrPw8yKi"
[[proxies]]
name = "mc-25566-tcp"
type = "tcp"
localIP = "127.0.0.1"
localPort = 49152
remotePort = 25566
transport.useCompression = true
[[proxies]]
name = "mc-25566-udp"
type = "udp"
localIP = "127.0.0.1"
localPort = 49152
remotePort = 25566
transport.useCompression = true
19. 错误码参考
| error.code | HTTP | 说明 | 触发场景 |
|---|---|---|---|
MISSING_KEY | 400 | 缺少 key 字段 | claim/heartbeat/release 请求体无 key |
MISSING_PORT | 400 | 缺少 port 字段 | heartbeat/release 请求体无 port |
MISSING_NAME | 400 | 缺少 name 字段 | 添加 Key 时未提供名称 |
INVALID_KEY | 403 | Key 无效或已删除 | claim/heartbeat 时 Key 不存在 |
UNAUTHORIZED | 401 | 管理密码错误 | admin 接口认证失败 |
PORT_NOT_IN_POOL | 404 | 端口不在池范围 | 查询的端口不在 25566~25575 |
KEY_NOT_FOUND | 404 | Key 不存在 | 删除不存在的 Key |
KEY_IN_USE | 409 | Key 已绑定端口 | claim 时该 Key 已在用 |
PORT_RELEASED | 409 | 端口已释放 | heartbeat 时端口已被超时回收 |
PORT_MISMATCH | 409 | 端口与 Key 不匹配 | heartbeat 的 port 与 Key 绑定不符 |
KEY_EXISTS | 409 | Key 值已存在 | 添加自定义 Key 时重复 |
NO_PORT_AVAILABLE | 503 | 端口池已满 | 10个房间全被占用 |
INTERNAL_ERROR | 500 | 服务器内部错误 | 未知异常 |
统一错误格式
{
"ok": false,
"error": {
"code": "KEY_IN_USE",
"message": "This key is already bound to port 25567"
}
}
20. 客户端集成指南
20.1 一号端(房主)完整流程
┌─────────────────────────────────────────────────────────┐
│ 步骤 1: 用户输入 API Key │
│ 客户端暂存 Key,校验格式(32位十六进制) │
├─────────────────────────────────────────────────────────┤
│ 步骤 2: 用户在 MC 中"对局域网开放" │
│ 客户端监听 MC 聊天消息 / 扫描本地端口 │
│ 自动提取 LAN 端口号(如 49152) │
├─────────────────────────────────────────────────────────┤
│ 步骤 3: 申领端口 │
│ POST /api/rooms/claim { key } │
│ → 获得 port, token, proxy_prefix │
├─────────────────────────────────────────────────────────┤
│ 步骤 4: 生成 frpc.toml(按 18.1 模板) │
│ 替换 {token}, {port}, {remotePort}, {localPort} │
├─────────────────────────────────────────────────────────┤
│ 步骤 5: 启动 frpc.exe -c frpc.toml │
│ 等待 "start proxy success" 日志 │
├─────────────────────────────────────────────────────────┤
│ 步骤 6: 启动心跳定时器 │
│ 每 15 秒 POST /api/rooms/heartbeat { key, port } │
│ 失败时重试 3 次,间隔 2 秒 │
├─────────────────────────────────────────────────────────┤
│ 步骤 7: 显示连接地址给用户 │
│ "玩家请连接: mc.yuki-can.top:{port}" │
├─────────────────────────────────────────────────────────┤
│ 步骤 8: 关服 / 退出时 │
│ POST /api/rooms/release { key, port } │
│ 终止 frpc.exe 进程 │
└─────────────────────────────────────────────────────────┘
20.2 MC LAN 端口检测方法
客户端需要自动检测 MC "对局域网开放"后的端口号。两种方法:
方法 A:解析 MC 聊天消息(推荐)
// MC 开放局域网后会在聊天框输出:
// "Local game hosted on port 49152"
// 客户端可读取 MC 日志文件提取端口号
// 日志路径 (Windows):
// %APPDATA%\.minecraft\logs\latest.log
// 正则匹配:
// /Local game hosted on port (\d+)/
方法 B:端口扫描(备用)
// 扫描本地 49152~65535 端口
// MC LAN 端口通常在这个范围
// 找到新出现的 TCP 监听端口即为 MC LAN 端口
// 注意: 需要排除已知服务端口 (3001, 3002, 3004, 3005, 7000, 7500 等)
20.3 frpc 进程管理
// 启动 frpc
const { spawn } = require('child_process');
const frpc = spawn('frpc.exe', ['-c', 'frpc.toml'], {
cwd: frpcDir,
windowsHide: false
});
// 监听 stdout 判断连接成功
frpc.stdout.on('data', (data) => {
const line = data.toString();
if (line.includes('start proxy success')) {
// frpc 连接成功,隧道已建立
onTunnelReady();
}
if (line.includes('login to server failed')) {
// 连接失败,可能 token 错误或服务器不可达
onTunnelError(line);
}
});
// 退出时清理
function cleanup() {
frpc.kill();
// POST /api/rooms/release
}
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
process.on('exit', cleanup);
20.4 心跳重试策略
class HeartbeatManager {
constructor(key, port, interval = 15) {
this.key = key;
this.port = port;
this.interval = interval;
this.retryCount = 0;
this.maxRetries = 3;
this.retryDelay = 2000;
}
start() {
this.timer = setInterval(() => this.send(), this.interval * 1000);
}
async send() {
try {
const res = await fetch('https://mc.yuki-can.top/api/rooms/heartbeat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: this.key, port: this.port })
});
const data = await res.json();
if (res.ok) {
this.retryCount = 0;
// 心跳成功,data.data.ttl = 剩余秒数
} else if (res.status === 409) {
// 端口已释放,需要重新 claim
this.stop();
this.onReleased(data.error.code);
} else if (res.status === 403) {
// Key 无效
this.stop();
this.onKeyInvalid();
}
} catch (e) {
this.retryCount++;
if (this.retryCount >= this.maxRetries) {
this.onNetworkError();
}
}
}
stop() { clearInterval(this.timer); }
}
20.5 完整状态机
客户端状态流转:
[等待Key输入] → [等待MC开放LAN] → [claim中] → [生成配置+启动frpc]
↑ ↓
│ [心跳保活中]
│ ↓
│ ┌──── 409 PORT_RELEASED ────────┘
│ ↓
│ [重新claim] ←─────────────────────────────┐
│ │ │
│ ↓ │
│ [frpc启动成功?] ──No──→ [重试/报错] │
│ │ │
│ Yes │
│ ↓ │
│ [心跳: 409 PORT_RELEASED] ────────────────────┘
│ │
│ ↓
│ [心跳: 403 INVALID_KEY] → [提示获取新Key]
│
└── [用户关服] → POST /api/rooms/release → [终止frpc] → [结束]
20.6 轮询建议
| 接口 | 建议频率 | 用途 |
|---|---|---|
GET /api/rooms | 5 秒 | 状态页实时刷新房间列表 |
GET /api/bandwidth | 2 秒 | 带宽监控实时刷新 |
GET /api/health | 30 秒 | 健康状态监控(低频即可) |
POST /api/rooms/heartbeat | 15 秒 | 客户端保活(claim 后启动) |
21. 变更记录
| 版本 | 日期 | 变更内容 |
|---|---|---|
| v1.0 | 2026-08-08 | 初始版本。端口池 25566~25575,API Key 认证,心跳机制,10 个房间,完整 API 文档 |
Base URL 汇总:
生产环境:
本地调试:
frps 连接信息:
服务器:
端口:
认证 Token:claim 响应中返回(每个 Key 一致)
生产环境:
https://mc.yuki-can.top本地调试:
http://127.0.0.1:3005frps 连接信息:
服务器:
39.104.52.226端口:
7000认证 Token:claim 响应中返回(每个 Key 一致)
注意事项:
- 所有时间戳均为 UTC ISO 8601 格式(如
2026-08-08T21:45:00Z) - 所有带宽值的单位均为
bytes(字节),不是 bits - 端口号大小写敏感——虽然是数字,但 proxy 名称中的端口号必须与 claim 返回的 port 完全一致
- frpc 进程退出后 FRP proxy 会立即消失,但端口不会立即释放(需等心跳超时 30 秒)
- 建议客户端在退出时主动调用
POST /api/rooms/release以加快端口回收
落雨栖 · MC 中转系统技术文档 v1.0
© 2026 落雨栖 Luoyuqi · 豫ICP备2026007315号 · 豫公网安备41112202000230号
© 2026 落雨栖 Luoyuqi · 豫ICP备2026007315号 · 豫公网安备41112202000230号