Spaze · 分布式Minecraft服务器初设想
一次创建高性能、分布式、跨平台的Minecraft服务器的尝试
开工之前,在这里简要分析一下完成一个mc服务器所需的功能
MC服务器所需实现的功能
基础功能
-
Gate
- 响应客户端的连接
- 聊天消息的广播
- 命令的解析
- 日夜交替和天气改变
-
Master
- 管理Gate和Node之间的连接
-
Node
- Unit
- 每tick对各个维度中的方块进行更新(包括但不限于红石、水流、农作物生长)
- 玩家的动作对周围玩家可见(局部广播玩家状态)
- 动物和怪物的AI
- 物品合成、附魔等处理
- 玩家可以在各个维度之间转移
- 管理每个维度中的方块和实体
- Unit
-
Database
- 将世界(所有维度的集合)储存到文件或数据库
- 读取配置文件,设置端口、online-mode、难度、服务器简介等
-
Generater
- 玩家进入未生成的区块附近时生成区块
拓展功能
- 提供用于编写插件的API(是否有必要兼容Bukkit API呢?)
服务器结构
| 概念 | 中文 | 解释 |
|---|---|---|
| Gate | 进程。接受客户端连接,将玩家托管到Node | |
| Master | 调度器 | 管理多个Gate和多个Node之间的连接 |
| Node | 计算节点 | 是一个进程,运行着多个Unit |
| Unit | 工作单元 | 在独立的线/协程上负责对一定区块进行更新 |
| Database | 存档 | 用于储存和交换诸如地形和实体等数据 |
| Generater | 生成器 | 用于生成地形 |
碎碎念(帮助我理清思路)
玩家加入Gate之后,Gate完成登录验证,然后请求Master安排一个Unit来处理。
Master收到请求之后根据玩家的在游戏中地理位置找到一个合适的Unit,并将UnitAddr(包括两个信息:Node地址和Unit编号)返回给Gate。
当Gate收到分配给该玩家的UnitAddr之后,发起对该Unit的连接(先连接对应的Node,再确认是进入Node中哪个Unit)。
此时Unit要加载该玩家所在位置附近的区块,并加载该玩家的数据。同时Gate会代理玩家和Unit之间的通讯。
有时,由于玩家之间距离的改变,可能需要将两个Unit的玩家合并到一个Unit处理,这就需要Master介入管理。玩家游玩时,Unit要向Master报告玩家的位置。
Unit的合并及拆分本质上是玩家在Unit之间的转移。当Master发现有Unit需要合并或拆分时。会命令Unit进行玩家的转移操作。
玩家在Unit之间的切换需要Gate、Node和Master之间通力合作。收到Master的通知后,原Unit首先将玩家的数据保存到Database,之后通知Gate连接新的Unit,然后卸载附近的区块,也把区块数据保存到数据库。
Gate收到需要切换代理目标的通知后,断开与原Unit的连接,并连接新的Unit,继续转发数据包。
新Unit不需要特殊的操作,当玩家从Gate过来之后,照常加载附近的区块就好了(可能需要等待原Unit释放)
容易看出,Unit之间的数据交换是通过Database,而不是直接通讯实现的。
在Unit交接的时候会出现EID(实体ID)不一致的问题,即原Unit告诉玩家一个实体的EID是A,而转移到新Unit之后可能发现A已被另一个实体使用了。这个问题我想到了几个解决方案:
- 使用唯一的全局EID,每次创建实体时Unit需要从某个服务申请EID。会导致服务端运行速度变慢
- 保持客户端和服务端的EID不一致性,服务端建立EID映射的map,会增加Unit复杂度。
- 在切换Unit的时候旧Unit先删除玩家客户的上原有的实体,新Unit重新给玩家加载所有实体。或是旧Unit将EID顺带传递给新Unit,新Unit只修正冲突的EID。可将顺序EID生成改为随机EID生成,降低EID冲突的几率,会增加服务端和数据库的复杂度。
当玩家退出游戏时,Gate要通知Master和Unit,Unit完成最后的数据保存工作。
sequenceDiagram
note right of Player: Player Join
Player->>Gate: Connect
activate Player
opt Online-mode
Gate->Player: Login Check
end
Gate->>Master: Player Join
participant Unit
Database->>Master: Player data
Master->>Gate: UnitAddr
Gate->>Unit: Player Join
activate Unit
Unit->>Database: Request
Database->>Unit: Player data
Database->>Unit: Chunk data
loop Playing
Gate->Unit: Proxy Playing Packet
Database-->Unit: Chunk data
Unit-->>Master: Report player position
end
note right of Player: Player Quit
Player->>Gate: Disconnect
deactivate Player
Gate->>Master: Player disconnect
Gate->>Unit: Player disconnect
Unit->>Database: Player data
deactivate Unit
sequenceDiagram
participant Gate
participant Master
participant UnitS as 原Unit
participant UnitD as 新Unit
loop Playing
Gate-->UnitS: Proxy Playing Packet
activate UnitS
UnitS-->>Master: Report player position
end
note left of Master: Player transfer
Master->>UnitS: Quit Player
UnitS->>Gate: Change Unit
UnitS->>Database: Player data
deactivate UnitS
Gate->>UnitD: Player Join
activate UnitD
UnitD->>Database: Request
Database->>UnitD: Player data
Database->>UnitD: Chunk data
loop Playing
Gate-->UnitD: Proxy Playing Packet
UnitD-->>Master: Report player position
deactivate UnitD
end