ProtoBuffer消息设计经验

message设计

分类

消息用于client与server之间的交互,一般都是请求-响应的组合。写代码的一大原则是:类、函数等的命名尽量能体现其含义。所以,个人习惯在消息编号以及消息结构体中通过后缀REQ、Req、RSP、Rsp来却分请求、响应。有的响应没有响应内容,无须定义响应的message,这个就相当于一个ack消息。所以可以通过后缀来表示message的分类:

_REQ、Req:表示请求
_RSP、Rsp:表示请求的响应,带有响应消息
_ACK、Ack:表示请求的确认,无响应消息

例如:

message Test1Req{    
    optional uint64 uid    = 1;  
    optional uint32 type    = 2; 
}

message Test1Rsp{
    optional int32 test    = 1;     
}

message Test2Req{    
    optional uint64 uid    = 1;  
    optional uint32 type    = 2; 
}

message Test2Rsp{
    optional int32 test    = 1;     
}

抽象

消息往往会有很多公用的字段,特别是响应的消息,比如消息类型、消息的序列号、响应的结果(错误码)、响应结果的描述等,可以定义一个总的Message,将公共的字段提取出来放到Message中。例如:

message Message{
   required MAGIC type = 1;
   required fixed32 seq = 2;
   optional fixed32 errCode = 3;
   optional bytes errMsg = 4;
   optional Test1Req test1Req = 5;
   optional Test1Rsp test1Rsp = 6;
   optional Test2Req test2Req = 7;
   optional Test2Rsp test2Rsp = 8;
}

这样做的好处是:

(1)更加抽象
(2)所有消息针对Message parse一次即可,不需要再分别去parse test1Req、test1Rsp、test2Req、test2Rsp

上述代码中,请求中一般没有errcode字段,所以可以进一步抽象出Request和Response两个消息:

message Request{
    optional Test1Req test1Req = 1;
    optional Test2Req test2Req = 2;
}

message Response{
    required fixed32 errCode = 1;
    optional bytes errMsg = 2;
    optional Test1Rsp test1Rsp = 1;
    optional Test2Rsp test2Rsp = 2;
}

这样的话,如有需要,请求的公用字段都加到Request中,响应的公用字段都加到Response中,两者相同的公用字段都加到上层的Message中。Message就变成如下:

message Message{
   required MAGIC type = 1;//消息类型,MAGIC枚举
   required fixed32 seq = 2;
   optional Request request = 3;
   optional Response response = 4;
}

使用enum定义message的类型

接下来要为每一个消息定义一个消息类型,一般建议用enum定义,例如定义一个MAGIC的enum:

enum MAGIC{
    MAGIC_TEST1_REQ = 0x00001001;
    MAGIC_TEST1_RSP = 0x00001002;
    MAGIC_TEST2_REQ  = 0x00001003;
    MAGIC_TEST2_RSP  = 0x00001004;

}

本人喜欢用MAGIC这个命名。MAGIC(幻数)是计算机编码中的一个术语,直接翻译为魔幻一般的数字,多好听。
至于类型的数值,可以自定义,比如从1开始递增。

这样定义完后,拿到protobuffer后的字符串,一次解析,再根据type即可取出响应的对象

    //代码片段      
    msg := Message{}
    err := proto.Unmarshal(buf[:len], &msg)
    if err != nil {
        //错误处理
        return err
    }

    if msg.GetType() ==  MAGIC_TEST1_REQ{
        req := msg.GetRequest().GetTest1Req()//直接取出Test1Req对象
        //handle

    }
    if msg.GetType() ==  MAGIC_TEST2_REQ{
        req := msg.GetRequest().GetTest2Req()//直接取出Test2Req对象
        //handle
    }
    if msg.GetType() ==  MAGIC_TEST1_RSP{
        rsp := msg.GetRequest().GetTest1Rsp()//直接取出Test1Rsp对象
        //handle
    }

错误码定义

错误码定义也用enum。错误码定义的原则是:通过错误码就能直接识别是哪个模块产生的,这样定位问题的时候就直指目标。可以通过数字区间来定义不同模块的错误码:

enum ERR_CODE {
       CLIENT_ERR_REFUSE = 100001;//客户端的错误码从100000-19999
       CLIENT_ERR_ACCEPT = 100002;
       SERVER1_ERR_STOP = 200001;//模块1的错误码从200000-29999
       SERVER2_ERR_OVERLOAD = 300001;//模块2的错误码从300000-39999
 }

序列化后数据的封包和解包

  • 封包:定义好的Message对象通过encode之后序列化成一段buf(字符串),加上长度一起封包。一般包头前两个字节用来标示buf的长度,后面接上buf。然后发送给对端。

  • 解包:取前两个字节先解出长度,再读入相应长度的buf,parse成Message对象即可。

proto的维护

实际应用中,不同的模块增加、修改消息或者字段是很频繁的事情。如果多个人修改proto文件,容易导致不同模块上运行不同版本proto,会有一些不可预估的风险。所以一般指定一个人负责proto的维护,所有proto的修改都由一个人完成,且制定proto更新时涉及模块的发版顺序。

你可能感兴趣的:(Unix网络编程)