golang 二进制协议处理的一种方式

一种golang二进制协议处理接口映射方式

简介

  • 在写服务器程序时,特别是业务向的服务(比如游戏服务器),经常会遇到处理许多客户端协议的情况,如果是http服务,那么定义好处理接口,剩下的交给web服务器就可以了。但是二进制协议就没有这么方便了。

  • 通常的自定义二进制协议规则都是固定长度消息头+变长消息体构成,在消息头中会有消息长度,消息id等字段。(基于TCP流式协议),服务器接收到客户端消息后,首先读取消息头,解析得到消息长度,再按照指定长度获取到完整的消息体的二进制数据。

  • 在写具体业务逻辑时,需要面临从网络层获取到的原始数据,怎么映射到内存数据结构并调用相应处理接口的问题。前面所说的二进制消息体的格式多种多样,大家都有自己的做法,这里以protobuf为例,构建服务器端接收到原始数据后,通过消息id映射生成对应的protobuf结构体,并调用处理接口。

  • golang种有一个reflect包,可以对类型进行反射,动态生成相应结构体,具体做法就是,将protobuf消息结构通过interface类型和消息id注册到一个自定义map中,在map中保存结构体的类型。

具体实现

网关层

  • 主要负责接受网络数据,然后将数据进行解析,分发到各个业务处理模块
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    package gate

    import (
    "errors"
    "reflect"

    "github.com/golang/protobuf/proto"
    )

    type MessageHandler func(msgID uint16, msg interface{})

    type MessageInfo struct {
    msgType reflect.Type
    msgHandler MessageHandler
    }

    type Gate struct {
    msgHandlerMap map[uint16]MessageInfo
    }

    func NewGate() *Gate {
    return &Gate{
    msgHandlerMap: make(map[uint16]MessageInfo),
    }
    }

    func (g *Gate) RegisterMessage(msgID uint16, msg interface{}, handler MessageHandler) {
    var info MessageInfo
    info.msgType = reflect.TypeOf(msg.(proto.Message))
    info.msgHandler = handler
    g.msgHandlerMap[msgID] = info
    }

    func (g *Gate) HandleRawData(msgID uint16, data []byte) error {
    if info, ok := g.msgHandlerMap[msgID]; ok {
    msg := reflect.New(info.msgType.Elem()).Interface()
    err := proto.Unmarshal(data, msg.(proto.Message))
    if err != nil {
    return err
    }
    info.msgHandler(msgID, msg)
    return err
    }
    return errors.New("not found msgID")
    }

模块A

  • 业务逻辑模块需要向网关注册各个协议
  • Register 方法向网关注册协议
  • 导入的 "test/protocol"proto 编译之后的 go 文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package module1

    import (
    "fmt"
    "test/gate"
    pb "test/protocol"
    )

    func Register(g *gate.Gate) {
    g.RegisterMessage(1, &pb.Player{}, PlayerHandler)
    }

    func PlayerHandler(msgID uint16, msg interface{}) {
    p := msg.(*pb.Player)
    fmt.Println("player handler msgid:", msgID, " body:", p)
    }

主函数

  • 主函数工作流程为

    • 初始化网关
    • 各个模块向网关注册服务
    • 启动服务
  • 本例只是一个测试,因此我给网关 gate 增加了一个 HandleRawData 的方法,模拟接受到网络数据之后的流程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    package main

    import (
    "github.com/golang/protobuf/proto"
    "test/gate"
    "test/module1"
    "test/module2"
    pb "test/protocol"
    )

    func main() {
    g := gate.NewGate()
    module1.Register(g)
    module2.Register(g)

    // test data 1
    player := &pb.Player{
    OpenId: "openid",
    Nickname: "nickname",
    Sex: 1,
    }

    data, _ := proto.Marshal(player)
    g.HandleRawData(1, data)

    // test data 2
    time := &pb.Time{
    EventTime: "xxxxxx",
    }
    data, _ = proto.Marshal(time)
    g.HandleRawData(2, data)
    }