Protobuf

序列化/反序列化

  • 序列化 :Class实例对象 »> byte、xml、json等
  • 反序列化 :byte、xml、json等 »> Class实例对象

序列化的好处有很多,比如可以存到硬盘,比如可以压缩数据等等

Protobuf

类似于json的一种序列化格式,可以做到数据压缩更小,反序列化速度更快

是谷歌开源的一种数据存储格式,需要我们定义好Proto描述文件,然后通过谷歌提供的对应的代码生成工具,生成对于语言的代码

image-20230730190148723

工程内的Proto描述文件,位于./Proto中

  • OuterMessage 客户端与服务端通信
  • InnerMessage 服务器之间进行通讯
  • MongoMessage 服务器之间进行通讯,但可以传送Entity

打开InnerMessage.proto,可以看见头两行有一个定义

syntax = "proto3";
package ET;

语法是Proto3(注意,不同版本的Proto语法是不一样的)

第二行就是一个命名空间了

尝试写一个测试消息

message C2M_Test
{
	int32 RpcId = 90,
	string test = 1
}

然后运行文件夹下的bat生成文件

然后可以在此处找到对应的生成代码

image-20230730192809109

[ResponseType(nameof(M2C_TestResponse))]
[Message(OuterOpcode.C2M_TestRequest)]
[ProtoContract]
public partial class C2M_TestRequest: Object, IActorLocationRequest
{
   [ProtoMember(90)]
   public int RpcId { get; set; }

   [ProtoMember(1)]
   public string request { get; set; }

}

[Message(OuterOpcode.M2C_TestResponse)]
[ProtoContract]
public partial class M2C_TestResponse: Object, IActorLocationResponse
{
   [ProtoMember(90)]
   public int RpcId { get; set; }

   [ProtoMember(91)]
   public int Error { get; set; }

   [ProtoMember(92)]
   public string Message { get; set; }

   [ProtoMember(1)]
   public string response { get; set; }

}

会生成2个结构,C2S以及S2C的

注意,ET中的proto生成器,并非谷歌提供的版本,而是ET项目自己提供的

image-20230730192930741

登录流程

image-20230709191645856

添加协议

添加一条客户端发送给服务器的直连协议,不经过网关,所以添加到OuterMsg中

//ResponseType R2C_LoginTest 注意:这里一定不能多加空格
message C2R_LoginTest // IRequest
{
	int32 RpcId = 90;
	string Account = 1;
	string Password = 2;
}
  • C2R 客户端发送给网关负载均衡服务器
  • RpcId 如果这条消息需要服务器回复,那么需要添加,固定为90,此时必须添加IRequest注释,并且在消息前增加一行注释,标明回复的消息类型

对应的服务器回包

message R2C_LoginTest // IResponse
{
	int32 RpcId = 90;
	int 32 Error = 91;
	string Message = 92;
	string GateAddress = 1;
	string Key = 2;
}
  • Error 错误码,固定91
  • Message 消息体,固定92

顺便再写两条客户端/服务端单向的测试协议

message C2R_SayHello // IMessage
{
	string Hello = 1;
}

message R2C_SayGoodbye // IMessage
{
	string Goodbye = 1;
}

此时不需要RpcId,并且注释为IMessage

登录逻辑

客户端

找到我们的LoginHelper类(Client\Hotfix\Demo\Login)

里面有一个默认登录方法,不管他,我们添加一个

public static async ETTask LoginTest(Scene zoneScene, string address)
{
    try
    {
        ...
    }
    catch (Exception e)
    {
        Log.Error(e.ToString());
    }
}

进一步细化逻辑

public static async ETTask LoginTest(Scene zoneScene, string address)
{
    try
    {
        Session session = null;
        R2C_LoginTest r2CLoginTest = null;
        try
        {
            // 创建一次会话
            session = zoneScene.GetComponent<NetKcpComponent>().Create(NetworkHelper.ToIPEndPoint(address));
            // 对于需要服务器回包的消息,用Call,并且await
            r2CLoginTest = (R2C_LoginTest)await session.Call(new C2R_LoginTest() { Account = "", Password = "" });
            Log.Debug(r2CLoginTest.Key);
            // 对于不需要服务器回包的消息,直接Send
            session.Send(new C2R_SayHello() { Hello = "Hello" });
        }
        finally
        {
            // 最终记得释放会话
            session?.Dispose();
        }
    }
    catch (Exception e)
    {
        Log.Error(e.ToString());
    }
}

然后新建一个类R2C_SayGoodbyeHandler,用于处理服务器单向消息

public class R2C_SayGoodbyeHandler: AMHandler<R2C_SayGoodbye>
{
    protected override void Run(Session session, R2C_SayGoodbye message)
    {
        Log.Debug(message.Goodbye);
    }
}

服务端

在Server/Hotfix/Demo下新建一个文件夹,Login

新建一个类

[MessageHandler]
public class C2R_LoginTestHaandler : AMRpcHandler<C2R_LoginTest,R2C_LoginTest>
{
    protected override ETTask Run(Session session, C2R_LoginTest request, R2C_LoginTest response, Action reply)
    {
        ...
    }
}

命名规范是:客户端协议名Handler

必须继承AMPrcHandler,两个泛型就是C2R和R2C的协议,这个类的具体意义不做解释

然后实现方法

// session代表服务器向客户端的连接,与客户端会话不是同一个东西
// request即客户端发送是消息
// response即服务端需要返回去的消息
// 调用replay代表发送
protected override async ETTask Run(Session session, C2R_LoginTest request, R2C_LoginTest response, Action reply)
{
    response.Key = "111111";
    reply();
    await ETTask.CompletedTask;
}

然后再处理另一条单向协议

此时继承AMHandler

public class C2R_SayHelloHandler: AMHandler<C2R_SayHello>
{
    protected override void Run(Session session, C2R_SayHello message)
    {
        Log.Debug(message.Hello);
        // 服务端可以通过sessiton发送单向协议
        session.Send(new R2C_SayGoodbye() { Goodbye = "Goodbye" });
    }
}

测试

修改登录逻辑

public static class UILoginComponentSystem
{
   public static void OnLogin(this UILoginComponent self)
   {
      // LoginHelper.Login(
      //     self.DomainScene(), 
      //     ConstValue.LoginAddress, 
      //     self.account.GetComponent<InputField>().text, 
      //     self.password.GetComponent<InputField>().text).Coroutine();
      LoginHelper.LoginTest(self.DomainScene(), ConstValue.LoginAddress).Coroutine();
   }
}

编译,运行服务器,运行Unity,登录

image-20230803190910338

Actor

image-20230805211353208

服务器进程间通讯,协议需要定义在InnerMsg中

格式基本相同

//ResponseType G2R_GetLoginKey
message R2G_GetLoginKey // IActorRequest
{
	int32 RpcId = 90;
	string Account = 1;
}

message G2R_GetLoginKey // IActorResponse
{
	int32 RpcId = 90;
	int32 Error = 91;
	string Message = 92;

	int64 Key = 1;
	int64 GateId = 2;
}

找到C2R_LoginHandler处理方法

[MessageHandler]
public class C2R_LoginHandler : AMRpcHandler<C2R_Login, R2C_Login>
{
   protected override async ETTask Run(Session session, C2R_Login request, R2C_Login response, Action reply)
   {
      // 随机分配一个Gate
      var config = RealmGateAddressHelper.GetGate(session.DomainZone());
      Log.Debug($"gate address: {MongoHelper.ToJson(config)}");
      
      // 向gate请求一个key,客户端可以拿着这个key连接gate
      G2R_GetLoginKey g2RGetLoginKey = (G2R_GetLoginKey) await ActorMessageSenderComponent.Instance.Call(
         config.InstanceID, new R2G_GetLoginKey() {Account = request.Account});

      response.Address = config.OuterIPPort.ToString();
      response.Key = g2RGetLoginKey.Key;
      response.GateId = g2RGetLoginKey.GateId;
      reply();
   }
}

当接收到客户端登录请求时,首先随机分配一个网关,其算法也很简单

public static class RealmGateAddressHelper
{
   public static cfg.StartSceneConfig GetGate(int zone)
   {
      var gates = new List<cfg.StartSceneConfig>();
      foreach (var config in LuBanComponent.Instance.GetAllTable().StartSceneTable.DataList)
      {
         if (config.SceneType == cfg.Enum.SceneType.Gate)
            gates.Add(config);
      }
      if (gates.Count > 0)
      {
         return gates[RandomHelper.RandomNumber(0, gates.Count)];
      }
      return null;
   }
}

我们的配表是这样的

image-20230805220905466

网关服本身就是一个Actor,所以我们拿他的InstanceId作为唯一Id,就可以向对应的Actor发送消息了

然后拿到对应网关的地址,再返回给客户端

IActorLocationMessage

image-20230805222103278

在OuterMsg中编写如下测试消息

//ResponseType M2C_TestActorLocationResponse
message C2M_TestActorLocationRequest //IActorLocationRequest
{
	int32 RpcId = 90;
	string Content = 1;
}

message M2C_TestActorLocationResponse //IActorLocationResponse
{
	int32 RpcId = 90;
	int32 Error = 91;
	string Message = 92;
	string Content = 1;
}

message C2M_TestActorLocationMessage //IActorLocationMessage
{
	int32 RpcId = 90; //ActorLocation消息,需要RpcId,尽管这条消息不需要服务器回复
	string Content = 1;
}

message M2C_TestActorMessage //IActorMessage
{
	string Content = 1;
}

因为Actor消息需要发给真正的玩家,所以我们需要写在玩家生成之后

可以把测试代码写在SceneChangeHelper.SceneChangeTo的最后

public static class SceneChangeHelper
{
    // 场景切换协程
    public static async ETTask SceneChangeTo(Scene zoneScene, string sceneName, long sceneInstanceId)
    {
        ...

        try
        {
            var session = zoneScene.GetComponent<SessionComponent>().Session;
            var res = (M2C_TestActorLocationResponse)await session.Call(
                new C2M_TestActorLocationRequest() { Content = "C2M_TestActorLocationRequest" });

            Log.Debug(res.Content);
            
            session.Send(new C2M_TestActorLocationMessage() { Content = "C2M_TestActorLocationMessage" });
        }
        catch (Exception e)
        {
            Log.Error(e.ToString());
        }
    }
}

先发送了一条需要服务器回复的Actor消息,然后又发送了一条不需要服务器回复的Actor消息

对应服务端的处理逻辑如下

[ActorMessageHandler]
public class C2M_TestActorLocationHandler: AMActorLocationRpcHandler<Unit,C2M_TestActorLocationRequest,M2C_TestActorLocationResponse>
{
    protected override async ETTask Run(...)
    {
        Log.Debug(request.Content);
        response.Content = "M2C_TestActorLocationResponse";
        reply();
        await ETTask.CompletedTask;
    }
}
[ActorMessageHandler]
public class C2M_TestActorLocationMessageHandler: AMActorLocationHandler<Unit,C2M_TestActorLocationMessage>
{
    protected override async ETTask Run(Unit unit, C2M_TestActorLocationMessage message)
    {
        Log.Debug(message.Content);
        MessageHelper.SendToClient(unit, new M2C_TestActorMessage() { Content = "M2C_TestActorMessage" });
        await ETTask.CompletedTask;
    }
}

区别在于,需要回复的消息,继承RpcHandler

对应的客户端处理

[MessageHandler]
public class M2C_TestActorMessageHandler: AMHandler<M2C_TestActorMessage>
{
    protected override void Run(Session session, M2C_TestActorMessage message)
    {
        Log.Debug(message.Content);
    }
}

编译运行

image-20230805232215757