以上最底层的tcp流的单个报文的格式
以下是经过粘包处理后的tcp字节流转为protobuf报文后的protobuf报文层级关系图
这些报文均由定义好的.proto文件通过protoc.exe来自动生成相应的.go文件,特此感谢google爸爸开源了这么好的项目(虽然并不能传到google爸爸的耳朵里),让我这个渣渣能有口饭吃(* ̄3 ̄)╭
报文层级1文件Tcp.proto:
这个proto文件里的报文都是与“单个服务器APP结构”图里的网络层相关的
TCPTransferMsg是用于数据传输的报文。
在发送的数据最后一步抵达网络层时,是对TCPTransferMsg使用proto.Marshal进行序列化,然后获取其长度length。以四个字节作为TCP流的报文头,这四个字节的组成一个int32类型的变量,这个变量值为length。
下图就是一个完整报文的TCP字节流的示意图
发送的过程就是将一个TCPTransferMsg类型的变量通过proto.Marshal序列化成一个[]byte类型的buff变量后作为body与其buff长度作为head组成一个完整的tcp字节流转为大端网络字节序发送出去
接收过程则是,调用net.TCPConn.Read()先读取的头四个字节,作为head,然后再根据head的值取相应的长度的[]byte,然后将这个[]byte通过proto.Unmarshal逆序列化或者还原为一个TCPTransferMsg类型的变量
TCPSessionCome是当listenManager新建了一个ConnectionSession会话时将会向业务逻辑层发送一个带有connId作为ConnectionSession这个会话唯一标识符的TCPSessionCome类型的报文给业务逻辑层。GateApp和RouterApp这两个用到了listenManager模块的APP需要根据相应的业务逻辑处理这个报文
TCPSessionClose是当listenManager结束了一个ConnectionSession会话会向逻辑层发送的报文
TCPSessionKick是当逻辑层因为超时,顶号登录,IP黑白名单之类的业务需求而要结束某个会话的时候由逻辑层向网络层发送
报文层级2文件router.proto和gate.proto
首先router.proto和gate.proto里的所有报文都通过proto.Marshal的方式转化为TCPTransferMsg的报文
以gate.proto的PulseReq为例,不完整代码如下(完整的代码要调用bs_proto.SetBaseKindAndSubId()和bs_proto.CopyBaseExceptKindAndSubId()两个函数,这里为了直观理解这么写):
pulse :=new(bs_gate.PulseReq)
pulse.Base.KinId = bs_types.CMDKindId_IDKindGate
pulse.Base.SubId = bs_gate.CMDID_Gate_IDPulseReq
msg := new(bs_tcp.TCPTransferMsg)
msg.DataKindId = pulse.Base.KindId
msg.DataSubId = pulse.Base.SubId
buf, err := proto.Marshal(req)
if err != nil {
return nil
} else {
msg.Data = buf
return msg
}
Gate.proto:
GateTransferData是这个proto文件里最重要的报文,功能是所有与用户客户端通信交互的报文都proto.Marshal到这个报文的TransferData.data成员变量中
Gate.proto的其他报文就不讲了,看注释很容易理解
Router.proto:
RouterTransferData是这个proto文件的最重要报文,功能与GateTransferData类似,不同之处是在于GateTransferData是用于GateApp与用户客户端之间的报文通信交互,
而RouterTransferData是用于服务端各个APP之间的通信交互,包括其他APP与GateAPP之间的交互也用RouterTransferData
RegisterAppReq是当一个除了routerAPP以外的APP启动时,DialManager模块拨号成功后立即向RouterApp发送的APP注册请求报文,包含了这个APP的apptype和appid的信息(apptype和appid在讲到types.proto会简单说明,然后后面讲解GateApp/RouterApp/以LoginApp为例子的业务型APP的时候可以看到其具体用法)
RegisterAppRsp是收到RegisterAppReq请求后的回复报文
PS:这里为什么要分为GateTransferData和RouterTransferData两个报文,而不是共用一个?
我的想法是(或者写这个C++框架的我的老大的说法是)
第一,在实际服务器部署时可能会出现routerApp监听的端口并不一定处于内网的安全范围内,可能暴露在外网中,可以被外网扫描。那么如果客户端和服务端用同一个报文,可能会收到直接伪造的报文的攻击。这样分成两种报文,如果外网向routerApp直接发送GateTransferData报文的时候proto.Unmarshal会解析失败并丢弃这个报文。至少增加了一点安全性。
第二,因为RouterTransferData报文比GateTransferData的size要稍微大一点,字段多了几个,把RouterTransferData转为GateTransferData向用户客户端发送时就可以去掉一些不必要字段
报文层级3的一般业务报文proto文件
这些报文在服务端APP相互传输的时候都先转为RouterTransferData报文,代码与上面的gate.proto报文转为TCPTransferMsg报文类似。这里就不重复写了
小结:一般业务逻辑的报文是在从服务端APP发往用户客户端时,从报文的角度看,过程是这样的:
BusinessMsg -> RouterTransferData -> TCPTransferMsg -----通过伟大的TCP/IP传到routerApp--->
TCPTransferMsg -> RouterTransferData -> TCPTransferMsg ----RouterApp通过伟大的TCP/IP传到GateApp---->TCPTransferMsg -> RouterTransferData -> GateTransferData -> TCPTransferMsg ------还是伟大的TCP/IP帮助我们神奇地传给了客户端------> TCPTransferMsg -> GateTransferData -> BusinessMsg -> 客户端根据BusinessMsg的值做相应的处理
Types.proto公共报文
多个proto可能公用的message或者enum都声明在types.proto里
BaseInfo是最典型的message,目前除了types.proto以外的所有proto文件里的报文都包含了types.BaseInfo类型的base成员变量,每个报文均有base。
以client.proto的LoginReq这个报文为例
kind_id = 1;表示这个报文的大类ID,对应的是types.proto的enum CMDKindId下面的值,通常每个enum CMDKindId对应一个proto。
LoginReq.base.kind_id = CMDKindId.IDKindClient
sub_id = 2;表示这个报文的小类ID,对应的是相应client.proto里定义的CMDID
LoginReq.base.sub_id = CMDID_Client.IDLoginReq
conn_id=3;表示这个报文来自于哪个ConnectionSession会话,在GateApp和RouterApp里有用到
gate_conn_id = 4;表示如果这个报文来自于客户端的话,那么gate_conn_id代表这个客户端在GateApp上对应的ConnectionSession会话
如果是发往客户端的话那么gate_conn_id指示GateApp发往哪个客户端,不过通常是通过userId来指示发往哪个客户端,当然发送登录请求的时候Gate并不知道客户端的userId,所以登录回复的时候要告诉GateApp是哪个gate_conn_id上的会话关联的用户客户端登录成功or失败了。如果登录成功了GateApp会记录下这个userId的值并将其与gate_conn_id关联绑定。
remote_add = 5;客户端的IP地址和端口,格式为“192.168.1.1:25536”的字符串
att_apptype = 6;表示这个报文关联的apptype,在收到的时候代表来源的apptype,在发出去的时候代表目标的apptype,对应的是types.proto的enum EnumAppType下面的值
当客户端发送登录请求LoginReq给服务端时
LoginReq.base.att_apptype = EnumAppType.Login
att_appid = 7; 表示这个报文关联的appid,在收到的时候代表来源的appid,在发出去的时候代表目标的appid。Appid是由程序猿自己整理的约定的表,只要值在uint32范围内就行了。但是值0,1,2是保留的有特殊含义的。对应的是types.proto的EnumAppId。Send2All = 1表示发送到所有,这里的所有指的并不是无差别的所有APP,而是根据AppType来发送的。就是属于同种appType的APP都发送,假设现在有3个GateApp和1个LoginApp连着Router,那么当DestAppType=Gate, DestAppId=Send2All的时候就是这个报文向3个GateApp都发送一次,而不会向LoginApp发送
Send2AnyOne=2是随机向确定appType的某个App发送,假设有2个LoginApp,那么就随机选一个发送,如果只有1个就只向这个发送
PS:建议APPID表10以下都不用了,说不定以后3,4,5也需要作为保留值。以现在APP举例,
GateApp的APPID = 101,RouterApp的APPID = 50,LoginApp的APPID = 30,这些数值由程序猿自己建个excel文档约定,只要保证不重复就行了。
不过实际上我对于GateApp这种会有多个的APP群,会把APPID从1000到1500的号码段都分配给他
我的邮箱:914509007@qq.com