消息用于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定义,例如定义一个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更新时涉及模块的发版顺序。