Skip to content

Addressable消息

在上一篇Route消息中,我们介绍了如何使用Route协议来实现消息的自动中转。然而,一个新的问题浮现了。以游戏为例,假设游戏中有多个地图,每个地图对应一个Map服务器。在这种情况下,我们可以通过Route消息自动将消息中转到相应的Map服务器。

但是,如果玩家在游戏中切换地图,情况就会变得复杂。每次切换地图后,必须在中转服务器上重新注册Route。每次地图切换时,都需要重复这一操作。这种方式不仅容易出错,而且还有可能导致消息发送到之前已经切换过的地图上,产生不必要的延迟或错误。

因此,我们需要特别注意处理这种情况。虽然可以通过额外的逻辑来解决,但从整体上看,确保所有操作都顺利进行且不出错是一项相当复杂的任务。

为了更好地解决这个问题,框架引入了Addressable协议。当Route发生变更时,Addressable会自动更新为最新的位置,从而确保消息始终发送到正确的目标位置。无论地图如何切换,位置发生了多少次变化,消息都能准确无误地发送到正确的目的地。

AddressableScene

为了确保实时获取Addressable的准确位置,框架会从一个名为Addressable的服务器中进行数据读取。因此,需要在SceneConfig配置文件中手动添加一个Scene,其SceneType必须设置为Addressable。

如果在线人数较多,可以在配置文件中增加多个Scene,且这些Scene的SceneType均设置为Addressable。框架会自动将用户分配到这些不同的Scene中。例如,在框架的示例(Examples)中只使用了一个Scene,主要是因为它只是一个演示,因此没有添加多个Scene。不过实际上,您可以添加多个Scene,以支持更多的在线用户,提升系统的负载能力。

Addressable协议

请按照定义网络通信协议的格式,并且在此基础上进行以下替换:

  • 将IMessage替换为IAddressableRouteMessage。
  • 将IRequest替换为IAddressableRouteRequest。
  • I将IResponse替换为IAddressableRouteResponse。

普通的Addressable消息示例

message C2M_TestMessage // IAddressableRouteMessage
{
   string Tag = 1;
}

RPC的Addressable消息示例

message C2M_TestRequest // IAddressableRouteRequest,M2C_TestResponse
{
   string Tag = 1;
}
message M2C_TestResponse // IAddressableRouteResponse
{
   string Tag = 1;
}

Addressable消息处理

服务器需要接收一个 Addressable 协议,为此,首先需要定义一个实体类,用于承载和处理 Addressable 协议的数据。这一实体将负责接收并解析协议中的各项参数和信息,为后续的逻辑处理提供支持和基础。

Unit

public sealed class Unit : Entity
{
   public long GateRouteId;
}

Addressable

接收发送端发送的Addressable消息。

需要定义一个类来继承Addressable,Addressable接受两个泛型类型:

  • 泛型1:指定要接收这个Addressable协议的实体。
  • 泛型2:指定了需要接收消息的类型。

在继承了Addressable类之后,必须实现一个名为Run的方法。这个方法接受两个参数:

  • entity:当前Route协议绑定的实体,类型就是Addressable后面泛型1定义的类型。
  • message:发送端发送过来的消息数据,类型就是Addressable后面泛型2定义的类型。

Addressable示例

public sealed class C2M_TestMessageHandler : Addressable<Unit, C2M_TestMessage>
{
   protected override async FTask Run(Unit unit, C2M_TestMessage message)
   {
       Log.Debug($"C2M_TestMessageHandler = {message.Tag}");
       await FTask.CompletedTask;
   }
}

AddressableRPC

接收发送端发送的AddressableRPC消息。

需要定义一个类来继承AddressableRPC,AddressableRPC接受三个泛型类型:

  • 泛型1:指定要接收这个Addressable协议的实体。
  • 泛型2:指定了需要接收消息的类型。
  • 泛型3:指定了发送端需要接收消息的类型。

在继承了AddressableRPC类之后,必须实现一个名为Run的方法。这个方法接受四个参数:

  • entity:当前Route协议绑定的实体,类型就是AddressableRPC后面泛型1定义的类型。
  • request:发送端发送过来的消息数据,类型就是AddressableRPC后面泛型2定义的类型。
  • response:需要返回给发送端的响应数据,类型就是AddressableRPC后面泛型3定义的类型。
  • response:一个用于立即发送 response 消息给发送端的函数。如果不调用 reply,系统会在 Run 方法执行完毕后自动调用它,将 response 发送回发送端。

AddressableRPC示例

public sealed class C2M_TestRequestHandler : AddressableRPC<Unit, C2M_TestRequest, M2C_TestResponse>
{
   protected override async FTask Run(Unit unit, C2M_TestRequest request, M2C_TestResponse response, Action reply)
   {
       Log.Debug($"Receive C2M_TestRequest Tag = {request.Tag}");
       response.Tag = "Hello M2C_TestResponse";  // 这里给发送端返回的消息
       await FTask.CompletedTask;
   }
}

注册Addressable

具体的原理在上一篇Route消息中已经介绍过了,如果没有看过的可以去看一下。

Gate服务器中向Map服务器发送消息获取一个RouteId

var scene = session.Scene;
// 1、首先要通过SceneConfig配置文件拿到进行注册Addressable协议的服务器
// 实际开发的时候,可能会根据一些规则来选择不同的Map服务器。
// 演示的例子里只有一个MapScene,所以我就拿第一个Map服务器进行通讯了。
// 我这里仅是演示功能,不是一定要这样拿Map
var sceneConfig = SceneConfigData.Instance.GetSceneBySceneType(SceneType.Map)[0];
// 2、使用Scene.NetworkMessagingComponent.CallInnerRoute方法跟Gate服务器进行通讯。
// CallInnerRoute方法跟Gate服务器进行通讯需要提供一个runTimeId,这个Id在sceneConfig.RouteId可以获取到。
// 第二个参数是需要发送网络协议,这个协议在Fantasy/Examples/Config/ProtoBuf里的InnerBson或Inner文件定义。
var responseAddressableId = (M2G_ResponseAddressableId)await scene.NetworkMessagingComponent.CallInnerRoute(sceneConfig.RouteId, new G2M_RequestAddressableId());
// 3、给session添加一个AddressableRouteComponent组件,这个组件很重要、能否转发Addressable协议主要是通过这个。
var addressableRouteComponent = session.AddComponent<AddressableRouteComponent>();
// 4、拿到MapScene返回的AddressableId赋值给addressableRouteComponent.AddressableId。
addressableRouteComponent.AddressableId = responseAddressableId.AddressableId;

responseAddressableId.AddressableId 是在 Map 服务器上生成的唯一标识符 (RouteId),它用来标识当前会话或请求的路由路径。当 Gate 服务器获取到这个 RouteId 后,可以利用该标识符高效地将后续消息直接路由到对应的目标服务器,从而避免了重复的路由解析和定位过程,提升了消息传输的速度和准确性。

现在看一下G2M_RequestAddressableIdHandler这个接收器的实现:

G2M_RequestAddressableIdHandler

public sealed class G2M_RequestAddressableIdHandler : RouteRPC<Scene, G2M_RequestAddressableId, M2G_ResponseAddressableId>
{
    protected override async FTask Run(Scene scene, G2M_RequestAddressableId request, M2G_ResponseAddressableId response, Action reply)
    {
        // 1、因为是测试代码,所以默认每次请求这个协议我都创建一个新的Unit来做Addressable。
        var unit = Entity.Create<Unit>(scene, false, true);
        // 2、给Unit添加AddressableMessageComponent组件,并执行Register(),向AddressableScene注册自己当前的位置。
        await unit.AddComponent<AddressableMessageComponent>().Register();
        // 3、返回给Gate服务器AddressableId
        response.AddressableId = unit.Id;
        await FTask.CompletedTask;
    }
}

到现在为止已经可以通过中转服务器自动中转Addressable消息了。

发送端发送Route协议

发送端没有任何变化,跟Session里一样发送就可以了。