Akka分布式游戏后端开发10 Protobuf协议处理

29 阅读2分钟

本专栏的项目代码在Github上,如果感兴趣的话,麻烦点点Star,谢谢啦。

如果我们的协议定义如下:

syntax = "proto3";

package com.mikai233.protocol;

message TestReq{

}

message TestResp{

}

在业务层是直接这样处理的:

class TestHandler : MessageHandler {
    @Handle
    fun handleTestReq(player: PlayerActor, testReq: TestReq) {
        player.send(testResp { })
    }
}

服务器网关收到的肯定是经过序列化的 Protobuf 二进制消息,需要一种形式将二进制数据解析成 Protobuf 消息,客户端和服务端定义的数据包格式一般会在包头写入包体长度, Protobuf 的协议号。客户端和服务端都根据包体长度去切分数据,然后根据 Protobuf 的协议号去确定包体具体是哪个消息的数据:

message MessageClientToServer{
  PingReq ping_req = 1;
  GmReq gm_req = 2;
  TestReq test_req = 3;
  LoginReq login_req = 10001;
}

message MessageServerToClient{
  PingResp ping_resp = 1;
  GmResp gm_resp = 2;
  TestResp test_resp = 3;
  LoginResp login_resp = 10001;
  TestNotify test_notify = 99999;
}

因此我们需要一个协议号到 Protobuf 消息的映射以及 Protobuf 消息到协议号的映射,客户端发送消息到服务端时,我们根据这个映射将数据包解析成具体的消息,服务端发消息给客户端时,我们根据消息类型找到对应的协议号来封装数据包。所以我们需要如下的代码:

private val ClientToServerMessageById0: Map<KClass<out GeneratedMessage>, Int> = mapOf(
    ProtoSystem.PingReq::class to 1,
    ProtoSystem.GmReq::class to 2,
    ProtoTest.TestReq::class to 3,
    ProtoLogin.LoginReq::class to 10_001
    )

public val ClientToServerMessageById: Map<KClass<out GeneratedMessage>, Int> = listOf(
    ClientToServerMessageById0,
    ).flatMap { it.entries.map { entry -> entry.key to entry.value } }.toMap()

private val ClientToServerParserById0: Map<Int, Parser<out GeneratedMessage>> = mapOf(
    1 to ProtoSystem.PingReq.parser(),
    2 to ProtoSystem.GmReq.parser(),
    3 to ProtoTest.TestReq.parser(),
    10_001 to ProtoLogin.LoginReq.parser()
    )

public val ClientToServerParserById: Map<Int, Parser<out GeneratedMessage>> = listOf(
    ClientToServerParserById0,
    ).flatMap { it.entries.map { entry -> entry.key to entry.value } }.toMap()

public enum class CSEnum(
  public val id: Int,
) {
  PingReq(1),
  GmReq(2),
  TestReq(3),
  LoginReq(10001),
  ;

  public companion object {
    private val entriesById: Map<Int, CSEnum> = entries.associateBy { it.id }

    public operator fun `get`(id: Int): CSEnum = requireNotNull(entriesById[id]) { "$id not found" }
  }
}

与之对应的,还有 ServerToClient,代码是自动生成的,不需要我们手动去写,这个流程会在 generateProto 这个任务之后做:

tasks.register<JavaExec>("generateProtoMeta") {
    group = "other"
    mainClass = "com.mikai233.protocol.MessageMetaGeneratorKt"
    classpath = sourceSets["main"].runtimeClasspath
}

tasks.named("generateProto") {
    finalizedBy("generateProtoMeta")
}

代码生成部分的逻辑就不看了,还是比较多的。