在说明之前我先展示一下我的工程目录层级,因为我连github有点慢,所以像protoc.exe这样上了1M的文件就不传了。所以如果从github上获取源码的同学可能还需要再去我的百度云盘上获取build.bat和protoc.exe。百度云盘链接:http://pan.baidu.com/s/1dFebn25密码:oq2h,当然用linux的同学需要自己再另外获取protoc和自己写build的shell脚本,这里对linux同学造成的小小不便请谅解
不过在git.oschina上有传protoc.exe
这里build.bat、protoc.exe和railgun这个工程的是处于同一个目录下的
Go/workspace就是我的GoPath这个环境变量的值
就是说railgun工程所处的目录是$GoPath/src
Build.bat是protoc.exe操作.proto文件生成相应的.go源文件的批处理命令,在linux下替换成相应.sh脚本命令就行了(这里要稍微注意点的是,如果你的GoPath环境变量有两个以上的路径,这个批处理命令是无法执行的,需要你把批处理里面的%GOPATH%这个值替换成protoc-gen-go.exe所处的绝对路径)
下面开始讲解代码,以发送LoginReq请求为例
运行GateApp.exe,RouterApp.exe, LoginApp.exe
1.在工程里写了一个很粗糙的客户端测试用例ClientForTest.exe:
开始先建立与GateApp建立TCP连接
ch := make(chan proto.Message, 1000)
session := DialManager.CreateClient("127.0.0.1:4101", ch)
if session == nil {
fmt.Println("连接gate失败")
return
}
创建消息接受协程
go ReceiveMsg(ch)
将loginReq转化为bs_tcp.TCPTransferMsg类型后向gateApp发送报文
loginReq := new(bs_client.LoginReq)
loginReq.LoginAccount = "yourname"
loginReq.LoginPassword = "E10ADC3949BA59ABBE56E057F20F883E" //123456的MD5加密
tcpMsg := ChangeCommonMsgToTCPTransferMsg(loginReq)
if tcpMsg != nil {
session.MsgWriteCh <- tcpMsg //向gate发送消息
}
2.通过TCP传输到了GateApp.exe:
2.1.从main()入口函数开始分析GateApp的代码
func main() {
//先创建需要的变量
quit := make(chan int)
var myAppId uint32 = 101
pNetAgent := PoolAndAgent.CreateNetAgent("0.0.0.0:4101") //监听4101端口
pLogicPool := PoolAndAgent.CreateMsgPool(quit, uint32(bs_types.EnumAppType_Gate), myAppId)
pRouterAgent := PoolAndAgent.CreateRouterAgent("127.0.0.1:2001") //连接127.0.0.1:2001地址
pGateLogic := CreateGateLogicInstance()
//将他们都与Pool绑定起来
pLogicPool.AddLogicProcess(pGateLogic)
pLogicPool.BindNetAgent(pNetAgent)
pLogicPool.BindRouterAgent(pRouterAgent)
//运行
ok := pLogicPool.InitAndRun(nil)
if ok {
fmt.Println("初始化完毕")
pInitMsg := &PrivateInitMsg{
pNetAgent: pNetAgent,
pRouterAgent: pRouterAgent,
myAppId: myAppId}
//在初始化完毕后向逻辑层发送初始化报文,带着一些初始化信息,比如自己的APPID等
pLogicPool.PushMsg(pInitMsg, 0)
}
//阻塞直到收到quit请求
for {
select {
case v := <-quit:
if v == 1 { //只有在收到1时才退出主线程
return
}
}
}
}
这里quit是通知整个GateApp.exe进程结束的channel
pNetAgent 是监听2001端口listenManager
pLogicPool 是一个消息队列管理池将会与主逻辑绑定
pRouterAgent 是去拨号连接RouterApp的DialManager
pGateLogic是业务逻辑GateLogic的一个单例,GateLogic实现了agent.go里的type ILogicProcess interface这个接口,GateLogic.go里定义和实现了GateLogic
然后和这些变量都和pLogicPool绑定后,pLogicPool.InitAndRun来启动运行,如果运行成功后向pLogicPool push私有的初始化用途报文PrivateInitMsg,把自己的APPId之类的信息传递给主逻辑层。
2.2.下一步分析package PoolAndAgent的MsgPool.go的SingleMsgPool.InitAndRun:
这里主要分3部分
1.判断有没有bindingNetAgent,就是是否需要创建网络监听实例,如果需要那么就创建新的协程,用于一直select case阻塞读取NetToLogicChannel这个channel
2.判断有没有RouterToLogicChannel,就是是否需要创建去连接RouterApp的网络拨号实例(除了RouterApp自身都需要)
3.遍历bindingLogicProcesses这个slice,如果是主逻辑业务的ILogicProcess建议只需要一个就够了,因为主逻辑业务会不可避免的要保存各种数据,那么如果是多协程可能就会涉及到全局数据加锁的情况,反复加锁解锁说效率不一定比单协程要高。(当然如果你要主逻辑多协程我也不反对,只是按照我的经验来说不建议这么做)。像数据库操作就这种IO速度较慢不可避免需要多协程的来处理的情况,那么实际上操作数据库的协程之间是没有相互通信的。都是在获取好数据后直接传给主逻辑协程来处理,这样程序结构上显得清楚点。后面讲到LoginApp.exe时会举例。
在遍历循环中为每个bindingLogicProcesses创建RunLogicProcess(pLogic ILogicProcess, pDataBase *CADODatabase, InitMsg proto.Message)
协程pLogic就是业务逻辑的单例,pDataBase是数据库,如果不需要操作数据库传nil,InitMsg是需要传入的初始化报文,不需要也传nil
RunLogicProcess()函数里每当收到一个报文就调用pLogic.ProcessReq来处理这个报文,和定时调用pLogic.OnPulse(nMs)函数,每次OnPulse的时间间隔ONPULSE_INTERVAL由程序猿根据实际需求调整
OK,这里当上文中来自ClientForTest.exe的TCPTransferMsg报文由网络层收到以后就丢给SingleMsgPool,SingleMsgPool收到后再丢给自己的成员变PoolToLogicChannel
,然后RunLogicProcess()协程一直在阻塞读取PoolToLogicChannel在收到TCPTransferMsg报文后调用pLogic.ProcessReq函数来处理这个报文
2.3.下一步分析GateLogic.go里的ProcessReq:
开始需要调用Gate_CreateCommonMsgByTCPTransferMsg函数来过滤掉不需要的报文,并转化成自己需要的报文。ClientForTest.exe发来的报文到这里变为了bs_gate.GateTransferData,然后Gate_GateTransferData()函数里把GateTransferData转为RouterTransferData发往RouterApp.exe
3.GateApp.exe通过TCP传输到了RouterApp.exe:
Main(),SingleMsgPool.InitAndRun()和GateApp.exe部分大同小异,区别部分就是GateApp.exe是有RouterAgent模块的,RouterApp.exe是没有RouterAgent模块的
讲一下RouterLogic.go文件的ProcessReq():
在收到RouterTransferData报文后,根据dest_apptype和dest_appid的值向目标APP发送(其实这里就是LoginApp)
4.RouterApp.exe通过TCP传输到了LoginApp.exe:
4.1 这里要说一下Main():
func main() {
//先创建需要的变量
quit := make(chan int)
var myAppId uint32 = 30
pLogicPool := PoolAndAgent.CreateMsgPool(quit, uint32(bs_types.EnumAppType_Login), myAppId)
pRouterAgent := PoolAndAgent.CreateRouterAgent("127.0.0.1:2001") //连接127.0.0.1:2001地址
pMainLoginLogic := CreateLoginLogicInstance()
//将他们都与Pool绑定起来
pLogicPool.AddLogicProcess(pMainLoginLogic)
pLogicPool.BindRouterAgent(pRouterAgent)
//创建数据库协程,只有主线程pLogicPool需要绑定RouterAgent,数据库协程是不需要的,所以退出quit也不需要创建,因为程序退出又逻辑主线程来控制
//先创建需要的变量
pDBPool := PoolAndAgent.CreateMsgPool(nil, uint32(bs_types.EnumAppType_Login), myAppId)
for i := 0; i < 10; i++ {
//创建10个数据库协程,因为数据库IO速度比较慢会存在阻塞时间,所以要多开几个,
//数据库逻辑协程间最好不要有数据通信,都应该通过主逻辑POOL与主逻辑进行通信
//相对的主逻辑最好只用一个协程,这样写业务逻辑也好写,
pDBLogic := CreateLoginDBInstance()
pDBProcess := PoolAndAgent.CreateADODatabase("root:hello123@tcp(localhost:3306)/gotest?charset=utf8")
//将他们都与Pool绑定起来
pDBPool.AddLogicProcess(pDBLogic)
pDBPool.AddDataBaseProcess(pDBProcess)
}
//运行主逻辑线程pool
ok := pLogicPool.InitAndRun(nil)
if ok {
fmt.Println("主逻辑POOL初始化完毕")
//这里要在初始化完毕后把DBPool和MainPool的指针传过去,这样才能两个Pool之间才可以相互传递数据
pInitMsg := &PrivateInitMsg{
pNetAgent: nil,
pRouterAgent: pRouterAgent,
myAppId: myAppId,
pMainPool: pLogicPool,
pDBPool: pDBPool}
//在初始化完毕后向逻辑层发送初始化报文,带着一些初始化信息,比如自己的APPID等
pLogicPool.PushMsg(pInitMsg, 0)
} else {
return
}
//运行数据库协程pool
pInitMsg := &PrivateInitMsg{
pNetAgent: nil,
pRouterAgent: pRouterAgent,
myAppId: myAppId,
pMainPool: pLogicPool,
pDBPool: pDBPool}
ok = pDBPool.InitAndRun(pInitMsg)
if ok {
fmt.Println("数据库逻辑POOL初始化完毕")
} else {
return
}
//阻塞直到收到quit请求
for {
select {
case v := <-quit:
if v == 1 { //只有在收到1时才退出主线程
return
}
}
}
}
在main()函数里建立了两个SingleMsgPool,一个是绑定主逻辑业务协程的Pool,另一个是绑定多个数据库逻辑业务协程的Pool。
而且在绑定数据库协程的pDBPool.InitAndRun(pInitMsg)的时候参数不是nil而是pInitMsg。为什么呢?因为如果不在InitAndRun的时候就传进去,在初始化完成后再向pDBPool发送,那么多个数据库业务逻辑协程实际上是对PoolToLogicChannel是处于共同读取状态,如果哪个协程空闲或者说正处于管道读取阻塞状态,就会去处理这个报文。就是说就算发送多个初始化报文仍然有可能存在有些数据库逻辑没有收到初始化报文的情况,造成初始化不完全的情况。
绑定了数据库逻辑业务协程的Pool实际上是没有绑定RouterAgent的,只有主逻辑Pool才绑定了RouterAgent,就是说LoginApp和RouterApp通信是通过主逻辑Pool,数据库的pDBPool并不直接与RouterApp通信
4.2.LoginMainLogic.go里的ProcessReq:
在收到TCPTransferMsg时,RouterAgent会将其转为RouterTransferData再让ProcessReq来处理。ProcessReq先将RouterTransferData转为LoginReq,
Client_OnLoginReq()函数中打印后就调用this.PushToDBPool(req) push给数据库的DBPool。
DBPool收到后就让空闲的数据库业务协程来处理
4.3.LoginDBLogic.go里的ProcessReq:
在LoginDBLogic收到的直接就是LoginReq,直接调用Client_OnDBLoginReq来处理
这里我对数据库sql.DB做了简单的封装,如果你不喜欢我的封装直接操作CADODatabase
.TheDB也是可以的,或者重新封装也没问题。
在从数据库里获取了数据后new了一个bs_client.LoginRsp登录回复报文丢给主逻辑业务Pool
接下来LoginRsp从LoginApp最终返回到客户端,就是上述过程的逆过程,这里就不再赘述了。需要简单注意一下的是,在GateApp收到LoginRsp时,如果是登录成功则会记录下这个userId并和对应的会话关联起来,以后其他APP向某个用户客户端发送报文时就可以用userId作为标识发送了,不一定要gateConnId,因为用userId通常比较方便,因为gateConnId用户客户端每当重新连接时都会变更。
5.说明和小结:每当根据业务需求新写一个业务App时,需要手写的源文件有
package main的main.go、PrivateMsg.go(这个如果没有需要新增私有报文就直接复制过来就行了)、XXXMsgFilter.go、XXXMainLogic.go、XXXDBLogic.go(如果需要操作数据库的话)
Package bs_proto的SetBaseInfo.go中的SetBaseKindAndSubId函数,要根据proto数据类型对其new Base并对Base.KindId和SubId赋值