动态绑定游戏逻辑服
介绍
动态绑定游戏逻辑服,指的是玩家与游戏逻辑服绑定后,之后的请求都由该游戏逻辑服来处理。
玩家动态绑定逻辑服节点后,之后的请求都由这个绑定的游戏逻辑服来处理, 可以实现类似 LOL、王者荣耀匹配后动态分配房间的效果。
解决的问题
动态绑定游戏逻辑服可以解决玩家增量的问题,我们都知道一台机器所能承载的运算是有上限的。 当上限达到时,就需要增加新机器来分摊请求量。 如果你开发的游戏是有状态的,那么你如何解决请求分配的问题呢? 在比如让你做一个类似 LOL、王者荣耀的匹配,将匹配好的玩家分配到一个房间中, 之后这些玩家的请求都能在同一个游戏逻辑服上处理,这种业务你该如何实现呢?
使用框架提供的动态绑定逻辑服节点可以轻松解决此类问题,而且还可以根据业务规则, 计算出当前空闲最多的游戏逻辑服,并将此游戏逻辑服与玩家做绑定, 从而做到均衡的利用机器资源,来防止请求倾斜的问题。
场景举例
什么意思呢?这里用匹配与象棋的场景举例。
假设我们部署了 3 台象棋逻辑服,在玩家开始游戏之前,我们可以在匹配服中进行匹配。
当匹配逻辑服把 A、B 两个玩家匹配到一起后, 我们可以通过 request/multiple_response 通讯模型来得到象棋房间数最少的象棋逻辑服。
这里假设是房间数最少的象棋逻辑服是 ChessLogicServer-2,并将其的逻辑服 id 绑定到 A、B 两个玩家身上。 之后与象棋相关的操作请求都会由 ChessLogicServer-2 这个游戏逻辑服来处理,比如开始游戏、下棋、吃棋、和棋等。
也可以简单点把这理解成,类似 LOL、王者荣耀的匹配机制。 在匹配服匹配到玩家后,把匹配结果中的所有玩家分配到一个房间(节点)里面, 这是一种动态分配资源最少的节点(游戏逻辑服)的用法之一。
匹配简图
- Player-1、Player-2,表示玩家1、玩家2,后续简称为 A、B 玩家。
- ExternalServer-1、ExternalServer-2,表示游戏对外服。
- ChessLogicServer,是象棋逻辑服。
- MatchLogicServer,是匹配逻辑服。
- match request,匹配请求。
图中,玩家1 和 玩家2 可以在不同的游戏对外服上,两个玩家发起一个匹配请求。 由匹配逻辑服来处理,假设是房间数最少的象棋逻辑服是 ChessLogicServer-2,那么将其的逻辑服 id 绑定到 A、B 两个玩家身上。
操作简图
- chess request,象棋相关的操作请求。
当玩家与 ChessLogicServer-2 绑定之后, 与象棋相关的操作请求都会由 ChessLogicServer-2 这个游戏逻辑服来处理,比如开始游戏、下棋、吃棋、和棋等。
也可以把这理解成类似 LOL、王者荣耀的匹配机制。 在匹配服匹配到玩家后,把匹配结果中的所有玩家分配到一个房间里面。
Example Source Code
see https://github.com/iohao/ioGameExamples
path : SimpleExample/example-endpoint
示例内容如下
- 匹配服逻辑服 (提供两个 action)
- 登录验证请求
- 匹配请求
- 房间逻辑服 (提供两个 action)
- 统计当前房间请求
- 接收操作请求
- 一个模拟的客户端(提供3个请求)
- 登录请求
- 匹配请求
- 循环给房间逻辑服发送操作请求
在游戏服务器中,我们会启动如下内容
- 启动 2 个房间逻辑服的实例
- 启动 1 个匹配逻辑服
- 启动 1 个游戏对外服
- 启动 1 个游戏网关
在模拟客户端中,玩家登录后会发起申请匹配。
匹配逻辑服匹配到玩家后,通过使用 request/multiple_response 通讯模型来得到房间逻辑服中房间数最少的游戏逻辑服。 将其的逻辑服 id 绑定到 A、B 两个玩家身上,之后的与房间相关的请求都由该房间逻辑服来处理。
之后该玩家会每隔几秒向游戏服务器发送一个操作的请求, 请求会进入房间逻辑服的 DemoEndPointRoomAction.operation 方法。
关键代码说明
下面,我们列出一些重点源码来讲解,开发者可搭配示例源码来阅读当前文档。
匹配逻辑服相关
matching action
是匹配方法的入口。
代码说明
- code 8,使用 request/multiple_response 通讯模型,来同时请求同类型多个游戏逻辑服。
- code 10,选出房间数最少的房间逻辑服
- code 16~19,构建需要动态绑定的消息。
- code 17,添加需要绑定的用户(玩家)
- code 18,添加需要绑定的逻辑服 id。
- code 19,覆盖绑定游戏逻辑服。see EndPointOperationEnum
- code 22,执行绑定。
@ActionController(11)
public class DemoMatchAction {
...
@ActionMethod(DemoCmdForEndPointMatch.matching)
public MatchResponse matching(FlowContext flowContext) {
CmdInfo cmdInfo = CmdInfo.of(DemoCmdForEndPointRoom.cmd, DemoCmdForEndPointRoom.countRoom);
ResponseCollectMessage responseCollectMessage = flowContext.invokeModuleCollectMessage(cmdInfo);
ResponseCollectItemMessage minItemMessage = getMinItemMessage(responseCollectMessage);
String logicServerId = minItemMessage.getLogicServerId();
List<Long> userIdList = new ArrayList<>();
userIdList.add(flowContext.getUserId());
EndPointLogicServerMessage endPointLogicServerMessage = new EndPointLogicServerMessage()
.setUserList(userIdList)
.addLogicServerId(logicServerId)
.setOperation(EndPointOperationEnum.COVER_BINDING);
ProcessorContext processorContext = BrokerClientHelper.getProcessorContext();
processorContext.invokeOneway(endPointLogicServerMessage);
MatchResponse matchResponse = new MatchResponse();
matchResponse.matchSuccess = true;
return matchResponse;
}
}
getMinItemMessage 代码说明
- code 6,房间逻辑服返回的数据集合,集合中有各个逻辑服的返回值信息。
- code 19,得到最少房间的逻辑服信息。
@ActionController(11)
public class DemoMatchAction {
...
ResponseCollectItemMessage getMinItemMessage(ResponseCollectMessage responseCollectMessage) {
var messageList = responseCollectMessage.getMessageList();
ResponseCollectItemMessage minMessage = null;
for (ResponseCollectItemMessage itemMessage : messageList) {
if (minMessage == null) {
minMessage = itemMessage;
continue;
}
RoomNumMsg roomNumMsg1 = itemMessage.getData(RoomNumMsg.class);
RoomNumMsg roomNumMsgMin = minMessage.getData(RoomNumMsg.class);
if (roomNumMsgMin.roomCount > roomNumMsg1.roomCount) {
minMessage = itemMessage;
}
}
return minMessage;
}
}
房间逻辑服相关
这个房间逻辑服是表示象棋逻辑服,也就是图中的 ChessLogicServer。
- code 8,随机数来表示当前逻辑服的房间数量。
@ActionController(DemoCmdForEndPointRoom.cmd)
public class DemoEndPointRoomAction {
...
static LongAdder longAdder = new LongAdder();
@ActionMethod(DemoCmdForEndPointRoom.countRoom)
public RoomNumMsg countRoom() {
int anInt = ThreadLocalRandom.current().nextInt(1, 100);
RoomNumMsg roomNumMsg = new RoomNumMsg();
roomNumMsg.roomCount = anInt;
return roomNumMsg;
}
}
启动类
在启动类中,配置了
- 两个房间逻辑服,通过不同的 id 构建。
- 一个匹配逻辑服
- 一个游戏对外服
代码说明
- code 4~5,创建 2 个房间逻辑服
- code 7,逻辑服列表,添加了匹配逻辑服和两个房间逻辑服
- code 12,启动
public class DemoEndPointApplication {
...
public static void main(String[] args) {
DemoEndPointRoomServer roomServer1 = createRoomServer(1);
DemoEndPointRoomServer roomServer2 = createRoomServer(2);
List<AbstractBrokerClientStartup> logicList = List.of(
new DemoEndPointMatchServer(),
roomServer1, roomServer2
);
NettySimpleHelper.run(ExternalGlobalConfig.externalPort, logicList);
}
}
createRoomServer 代码说明
- code 5~8,通过 BrokerClient 构建器来创建房间逻辑服的信息。
- code 6,设置逻辑服 id,用于绑定。
- code 7,设置逻辑服名字。
- code 8,设置同类型。注意,这个 tag 很重要,表示同类型的游戏逻辑服。
- code 10,创建房间逻辑服。
- code 11,手动指定房间逻辑服的信息。
public class DemoEndPointApplication {
...
private static DemoEndPointRoomServer createRoomServer(int id) {
BrokerClientBuilder brokerClientBuilder = BrokerClient.newBuilder()
.id("1-" + id)
.appName("RoomLogic-" + id)
.tag("endPointRoomLogic");
DemoEndPointRoomServer roomLogicServer = new DemoEndPointRoomServer();
roomLogicServer.setBrokerClientBuilder(brokerClientBuilder);
return roomLogicServer;
}
}
模拟客户端相关
DemoForEndPointClient 是模拟客户端的入口,启动后可以观察控制台的输出。
public class DemoForEndPointClient {
public static void main(String[] args) {
List<InputCommandRegion> inputCommandRegions = List.of(
new InternalMatchRegion()
, new InternalRoomRegion()
);
new ClientRunOne()
.setInputCommandRegions(inputCommandRegions)
.startup();
}
}
EndPointOperationEnum
在绑定游戏辑服时,有一个参数比较的重要
EndPointLogicServerMessage.setOperation(EndPointOperationEnum.xxxx);
EndPointOperationEnum 是用于绑定的业务类型的枚举类,内容如下
APPEND_BINDING : 追加绑定游戏逻辑服。
举例 : 在追加之前,如果玩家已经绑定了 [1-1] 的游戏逻辑服 id。
现在玩家添加 [2-2、3-1] 的游戏逻辑服 id, 此时玩家所绑定的游戏逻辑服数据为 [1-1、2-2、3-1],一共 3 条数据。
COVER_BINDING : 覆盖绑定游戏逻辑服。
举例 : 在覆盖之前,如果玩家已经绑定了 [1-1] 的游戏逻辑服 id。
现在玩家添加 [2-2、3-1] 的游戏逻辑服 id, 此时玩家所绑定的游戏逻辑服数据为 [2-2、3-1],一共 2 条数据,新的覆盖旧的。
无论之前有没有绑定的数据,都将使用当前设置的值。
REMOVE_BINDING : 移除绑定的游戏逻辑服。
举例 : 在移除之前,如果玩家已经绑定了 [1-1、2-2、3-1] 的游戏逻辑服 id。
现在玩家添加 [2-2、3-1] 为需要移除的游戏逻辑服 id, 此时玩家所绑定的游戏逻辑服数据为 [1-1],一共 1 条数据,被移除了 2 条数据。
CLEAR : 清除所有绑定的游戏逻辑服。
注意事项
事项1
如果玩家断线了,在重新登录后需要重新绑定一下这个游戏逻辑服。 因为绑定的数据与元信息-附加信息一样, 是保存在游戏对外服的,断线后游戏对外服会移除用户的 channel 并清除相关数据。
简单的说,需要开发者来维护这个信息,框架只提供赋值功能。
事项2
玩家在绑定游戏逻辑服时,框架不会检测需要绑定的游戏逻辑服是否存在。 检测这个事由开发者来做,可以参考上面示例介绍的动态绑定最少房间的方式来做检测。
小结
本篇我们介绍了如何动态绑定游戏逻辑服,在使用上来说是比较简单的,只有几句代码。
构建动态绑定信息,执行绑定。
EndPointLogicServerMessage endPointLogicServerMessage = new EndPointLogicServerMessage()
.setUserList(userIdList)
.addLogicServerId(logicServerId)
.setOperation(EndPointOperationEnum.COVER_BINDING);
ProcessorContext processorContext = BrokerClientHelper.getProcessorContext();
processorContext.invokeOneway(endPointLogicServerMessage);