<>>

# 服务端、客户端

# 概述

对 Minecraft 有一定程度了解的玩家对 “服务端” “客户端” 这两个词一定不陌生。但实际上,“客户端”和“服务端”的概念可能和普遍的认识存在一些出入————那便是物理端和逻辑端的区别。

# 物理端、逻辑端

客户端可以分为物理客户端和逻辑客户端。同样的,服务端也可以分为物理服务端和逻辑服务端。

物理端比较好理解,玩家在玩游戏之前所启动的程序便是物理客户端,而单独运行的多人游戏服务端便是物理服务端。但逻辑端又是什么?

当玩家选择单人游戏的时候,玩家所启动的 “物理客户端” 不可避免的要接管一些原本由 “服务端” 所执行的逻辑,例如世界的生成和实体的更新。实际上,Minecraft 的单人模式是在玩家的客户端程序里启动了一个 “内置服务器(Integrated Server)”,这个 “内置服务器” 便是逻辑服务端,而原本执行客户端逻辑的部分(例如处理用户键盘鼠标输入、渲染游戏画面)便是逻辑客户端

# 区分客户端和服务端的职责

作为一名合格 Minecraft Mod 开发者,在思考实现时应始终考虑到服务端与客户端的职责:

  • 游戏的核心逻辑(如攻击伤害的计算)只应由服务端负责
  • 用户输入的处理、画面的渲染只应由客户端负责
  • 与核心逻辑相关的配置文件(例如物品的掉落概率)应该从服务端读取,而类似 “是否开启声音” 这类的配置文件则应该从客户端读取

不对客户端和服务端的职责加以区分往往会造成不好的后果,轻则是特性没有被正确的实现,重则是造成严重的漏洞。下面举一个例子:

“拥有管理员权限的玩家按下特定按键可以变为创造模式” 这个特性,有以下几种实现思路。

  • 监听 “按键事件” ,在监听器判断玩家是否拥有管理员权限,如果有则将玩家设为创造模式
  • 监听 “按键事件” ,在监听器里判断玩家是否拥有管理员权限,如果有则发包让服务器把玩家设为创造模式
  • 监听 “按键事件” ,在监听器里给服务器发包,让服务器判断玩家是否拥有管理员权限,如果有则将玩家设为创造模式

TIP

“发包” 意为发送数据包,是在服务端、客户端之间交换信息的手段。有关它的细节将在下一节详细介绍。

以上三种思路,根据长度可以判断出只有最后一种是正确的实现思路。其他两种的问题分别在哪呢?

如果读者真的按照思路1去尝试实现了,会发现按下按键之后没有任何反应。这是当然的——按键事件作为用户输入,只会在客户端被触发,而更改玩家的游戏模式涉及游戏的核心逻辑,只能在服务端上实现。

如果读者按照思路2尝试实现,会发现已经达到了预期的目标,看似没有问题。但实际上,这种实现留下了一个巨大的漏洞:任何人都可以像服务端发送数据包而将自己的游戏模式更改为创造模式,因为服务端并不会再次去验证发包的玩家是否拥有管理员权限。

题外话:

看似思路2是很可笑的行为,但它是有实际背景的:一个名为“魔法与远征2”的 Mod 关于伤害判定的部分就是在客户端实现的,之后由客户端发包告知服务端给对应实体扣血,导致心怀不轨的玩家可以利用此特性随意秒杀服务器中的任意实体。

笔者十分希望正在读这篇教程的读者在实现某个特性的时候认真思考每一步的逻辑应该存放在何处,以避免类似的漏洞再次出现。

# 在代码中判断业务端

# 判断逻辑端

区分逻辑端最常见的做法是通过 World::isRemote 判断:若返回值为 true 则当前是逻辑客户端,反之则是逻辑服务端。

# 判断物理端

物理端可以通过 Dist::isClient 区分:若返回值为 true 则当前为物理客户端,反之则是物理服务端。此外,Dist 类中的 CLIENTDEDICATED_SERVER 字段代表的都是物理端。

# @OnlyIn

@OnlyIn(DIST.xxx) 注解用于标注某一类/字段/方法只应出现在哪一物理业务端中。例如:

@OnlyIn(Dist.DEDICATED_SERVER)
public class CapabilityManager {
    // ...
}
1
2
3
4

表明 CapabilityManager 只应在物理服务端中出现,如果尝试在客户端加载这个类便会抛出异常。

WARNING

错用 @OnlyIn 注解会导致各种各样的游戏崩溃,务必谨慎使用。

# 注意事项

  • 实现某个逻辑时,谨慎思考该逻辑应存在于服务端还是客户端
  • 在订阅事件之前,务必检查该事件在哪一个业务端触发
  • 谨慎使用 @OnlyIn 注解