-
Notifications
You must be signed in to change notification settings - Fork 0
Design description of solidnet
本框架是为重构棋牌游戏后台Server而设计,原先的Server是C++程序,使用用libevent库,事件驱动单线程模型。对于棋牌游戏,采用单线程处理是没有性能问题的,而且单线程减少并发带来的种种问题,使程序编写变得容易。
那么,在使用Golang重构后台Server时,同样保持游戏逻辑采用单线程模型,也就是在处理消息事件时,必须是单线程处理,目的是保证原有业务的稳定性。
由于Golang中的socket读取数据时是阻塞的,无法编写单线程程序,但是这也是正是Golang的一大特色,擅长使用并发解决问题,对于一个socket连接,可以启动2个协程,一个负责读数据,一个发数据。
多个socket接受到的数据都放到一个全局的消息channel中,然后再由一个协程统一处理全局channel中的消息,这样就变成了单线程模型。
type IProcessor interface {
Dispatch(IMessage) // 多协程调用,将消息投入队列
Epoll() IMessage // 只有一个协程调用,取出一个消息
}
// 保证全局唯一实例
func GetProcessor() IProcessor {
once.Do(func() {
p = &ChannelProcessor{make(chan IMessage, MAX_CHANNEL_LEN)}
})
return p
}
// 消息队列采用channel
type ChannelProcessor struct {
messageChannel chan IMessage
}
业务中仅有三种消息:
- 网络消息 socket接受的数据
- 定时器消息 定时器事件
- 状态消息 socket建立或者关闭
将三种消息统一抽象出一个消息接口:
type IMessage interface {
Data() interface{} //消息的数据
Args() interface{} //消息的参数
}
然后定义实现了三种消息类型:
// 网络消息
type NetMessage struct {
packet []byte // socket上介绍的数据
client IClient // 封装socket的客户端对象
}
func (m *NetMessage) Data() interface{} {
return m.packet
}
func (m *NetMessage) Args() interface{} {
return m.client
}
// 定时器消息
type TimerMessage struct {
id int32 // 定时器id
handler ITimerHandler // 定时器处理对象
}
func (m *TimerMessage) Data() interface{} {
return m.id
}
func (m *TimerMessage) Args() interface{} {
return m.handler
}
// 状态消息
type StateMessage struct {
state int32 // socket的状态,目前只有connected和closed两种状态
client IClient // 封装socket的客户端对象
}
func (m *StateMessage) Data() interface{} {
return m.state
}
func (m *StateMessage) Args() interface{} {
return m.client
}
handler用来处理三种业务消息,需要业务层实现。
type IHandler interface {
HandleTimer(IMessage)
HandleNet(IMessage)
HandleState(IMessage)
}
server启动的入口,包括:
- 初始化日志
- 开启tcp服务
- 循环处理消息
// 最终的所有消息都这里处理,是单线程模型
func (g *Game) Run() {
for {
message := g.processor.Epoll()
switch message.(type) {
case *TimerMessage:
g.handler.HandleTimer(message)
case *NetMessage:
g.handler.HandleNet(message)
case *StateMessage:
g.handler.HandleState(message)
default:
logger.Error("type of message is error!")
}
}
}
包的格式必须是固定长度包头+包体的形式,其中包头中必须有包体长度字段或者包总长度的字段。
GetBodyLen() int32 // 从包头中得到包体的长度
GetHeadLen() int32 // 得到包头的固定长度
这2个接口函数,业务包定义必须实现,这是处理tcp粘包的关键,参见baseclient.go中的recv函数。
p := c.factory.NewPacket()
// 读取包头
head := make([]byte, p.GetHeadLen())
_, err := io.ReadFull(c.conn, head)
if nil != err {
logger.Error("io.ReadFull() failed, error[%s]", err.Error())
c.stop()
continue
}
p.WriteBytes(head)
bodyLen := p.GetBodyLen()
if bodyLen >= MAX_USER_PACKET_LEN {
// 包体太长,有可能是网络攻击包或者错误包,丢掉不处理
logger.Error("length of uesr packet more than MAX_USER_PACKET_LEN, bodyLen=%d", bodyLen)
continue
}
// 读取包体
bodyData := make([]byte, bodyLen)
_, err = io.ReadFull(c.conn, bodyData)
if nil != err {
logger.Error("io.ReadFull() failed, error[%s]", err.Error())
c.stop()
continue
}
p.WriteBytes(bodyData)
BaseClient实现了全部的网络功能,有3个channel实现的缓冲区:
type BaseClient struct {
conn net.Conn
input chan []byte // 存放从socket接受的数据
output chan []byte // 存放应用层向socket发送的数据
state chan int32 // 存在socket状态的消息
mutex sync.Mutex
remoteAddr string
localAddr string
factory IPacketFactory
}
启动2个协程:
// 启动2个协程,如果对端关闭或者出错,2个协程会先后退出
func (c *BaseClient) run() {
// 启动接受数据的协程 数据放入 input
go c.recv()
// 启动发送数据的协程 将output中的数据发送出去
go c.send()
// 向应用层通知有新的连接 状态消息放入state
c.notifyState(STATE_CONNECTED)
}
BaseClient的数据流,都是与其内部的三个缓冲区交互,而TcpClient继承自BaseClient则是将内部的网络消息和状态消息派发到应用层。因此,启动2个协程派发消息。
func (c *TcpClient) Run() {
c.stopWait.Add(1)
go c.dispatchStateMessage()
c.stopWait.Add(1)
go c.dispatchNetMessage()
c.stopWait.Wait()
}
比较简单,创建新和管理客户端连接。
定时器使用Golang标准库time.Timer实现。
type ITimerHandler interface {
DoTimerAction(int32)
}
type Timer struct {
timer *time.Timer
id int32
isLoop bool
timeout time.Duration
isRunning bool
handler ITimerHandler
}
handler 用来处理定时器事件,所以ITimerHandler接口必须是自己实现。
func (t *Timer) procTimeout() {
message := &TimerMessage{t.id, t.handler}
TimerMsgprocessor.Dispatch(message) // 将定时器消息派发到应用层
if t.isLoop {
t.timer.Reset(t.timeout)
}
}
这里定时器超时,并不会立即调用消息处理函数,而是将消息派发到应用层。