Skip to main content

动态绑定游戏逻辑服

介绍

动态绑定游戏逻辑服,指的是玩家与游戏逻辑服绑定后,之后的请求都由该游戏逻辑服来处理。

玩家动态绑定逻辑服节点后,之后的请求都由这个绑定的游戏逻辑服来处理, 可以实现类似 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 两个玩家身上。

An

操作简图

  • chess request,象棋相关的操作请求。

当玩家与 ChessLogicServer-2 绑定之后, 与象棋相关的操作请求都会由 ChessLogicServer-2 这个游戏逻辑服来处理,比如开始游戏、下棋、吃棋、和棋等。

也可以把这理解成类似 LOL、王者荣耀的匹配机制。 在匹配服匹配到玩家后,把匹配结果中的所有玩家分配到一个房间里面。

An

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);