Skip to content

Design description of solidnet

Kent Zhang edited this page Jan 10, 2019 · 1 revision

项目背景

本框架是为重构棋牌游戏后台Server而设计,原先的Server是C++程序,使用用libevent库,事件驱动单线程模型。对于棋牌游戏,采用单线程处理是没有性能问题的,而且单线程减少并发带来的种种问题,使程序编写变得容易。

那么,在使用Golang重构后台Server时,同样保持游戏逻辑采用单线程模型,也就是在处理消息事件时,必须是单线程处理,目的是保证原有业务的稳定性。

单线程设计

由于Golang中的socket读取数据时是阻塞的,无法编写单线程程序,但是这也是正是Golang的一大特色,擅长使用并发解决问题,对于一个socket连接,可以启动2个协程,一个负责读数据,一个发数据。

多个socket接受到的数据都放到一个全局的消息channel中,然后再由一个协程统一处理全局channel中的消息,这样就变成了单线程模型

processor定义

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
}

message定义

业务中仅有三种消息:

  • 网络消息 socket接受的数据
  • 定时器消息 定时器事件
  • 状态消息 socket建立或者关闭

将三种消息统一抽象出一个消息接口:

type IMessage interface {
	Data() interface{} //消息的数据
	Args() interface{} //消息的参数
}

然后定义实现了三种消息类型:

1、网络消息

// 网络消息
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
}

2、定时器消息

// 定时器消息
type TimerMessage struct {
	id      int32          // 定时器id
	handler ITimerHandler  // 定时器处理对象 
}

func (m *TimerMessage) Data() interface{} {
	return m.id
}

func (m *TimerMessage) Args() interface{} {
	return m.handler
}

3、状态消息

// 状态消息
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定义

handler用来处理三种业务消息,需要业务层实现。

type IHandler interface {
	HandleTimer(IMessage)
	HandleNet(IMessage)
	HandleState(IMessage)
}

game定义

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!")
		}
	}
}

packet定义

包的格式必须是固定长度包头+包体的形式,其中包头中必须有包体长度字段或者包总长度的字段。

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)

client定义

BaseClient

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)
}

TcpClient

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()
}

tcpServer定义

比较简单,创建新和管理客户端连接。

timer定义

定时器使用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)
	}
}

这里定时器超时,并不会立即调用消息处理函数,而是将消息派发到应用层。

Clone this wiki locally