在查看官方issues时,也发现了一些个人觉得有用的讨论,摘录一下
一、issues 希望可以增强下log的功能
Q:leaf的log蛮好用的了,可以log到文本。但是现在需要做日志分析。要能支持json格式,异步log等等。希望可以加强下。
看了下zap日志库,感觉过去复杂了。有没有简单实用,能分析的日志库?
A:Leaf 的 log 的定位并非用于数据分析,最好额外加一套用于数据分析的机制,同时也保留 Leaf 的 log。
二、issues 服务端崩溃
zcwtop:请问下,你是如何设计“一局游戏开一个Goroutine”的。能否给个简单的demo或者思路。
我自己写,一局游戏基本上是每个操作都放在goroutine里运行的,用的是leaf的顺序goroutine。这样感觉goroutine会非常多,不知道这样设计是不是错了?
exfun:为什么每个操作都要单独开goroutine。我之前就是开太多了,几十局游戏开运行完全跑不出问题。跑几万的次数就崩溃,这就是我开个issue求助的原因,找BUG找半天。我听了作者了。尽量少开,一局游戏的逻辑操作都放一个goroutine里执行,数据同步放到另外一个goroutine专门处理,我们用的Postgress。这样防止并发下未知的RACE。简单点好
zcwtop:是啊。我现在感觉是太多了。我现在的设计是这样的,假如一局游戏的结构是
type T struct {
*g // leaf 平行的go
...
}
func (t *T) add {
t.Go(func() {
// ...
}, func(){
//...
})
}
func (t *T) del {
t.Go(func() {
// ...
}, func(){
//...
})
}
基本上每个操作都是这样。如果改成一个go运行,我该如何改? 或者你是如何写的,能否给个参考。
exfun:我没有用leaf这个,用go 关键字声明的。leaf的只用了它的消息和工具类这些。我是一局游戏比如game *Game, 然后会在一个goroutinue里面执行,比如 go game.Process() , 然后在process里面完成单局所有的事情。游戏局完了之后,这个goroutine就会执行完,等待GC回收。
zcwtop:那是不是这个Process是一个死循环,用select多路复用等待通道消息,然后执行。我理解是不是类似这样处理:
func (g *Game) Process(...) {
begin()
//...
for {
select {
case c:=<-通道1:
handle1()
case c := <-通道2:
handle2()
}
}
//...
end()
}
func (g *Game)handle1() {
//...
}
func (g *Game)handle2() {
//...
}
exfun:写好点应该是这样的,leaf负责消息转发chan通知,来了就处理,注意游戏结束条件。别一直结束不了卡住,这样就会有问题。超时的地方得处理!我们是在线答题的游戏,比较简单。
zcwtop:人多的时候一定要注意别资源竞争(多核下就很容易崩溃),要么用锁,原子,条件变量控制。或者用chan来传递消息。以下是我一个Game一个goroutine来控制游戏过程的流程Process() 。重构后的代码,比之前清晰多了,这也是我第一次写golang,还需要学习的东西很多。
func (GR *GameRoom) Process() {
// 处理游戏过程结束/正常/异常
defer func() {
GR.End()
if e := recover(); e != nil {
panic(e)
}
}()
GR.Playing = true
gameStartTime := conf.GameConfig.StartTime
robotTime := conf.GameConfig.RobotTime
Playing:
for {
select {
case m := <-GR.MsgChan:
player := m.P
switch m.Mtype {
case MSG_JOIN:
GR.onJoin(player)
case MSG_ANSWER:
GR.onAnswer(player, m.Answer, m.Pm)
}
case wf := <-GR.WaitChan:
switch wf {
case WAIT_GAME:
// 游戏开始倒计时,补充一部分AI
joinRobots(GR, utils.RandIntFrom2(18, 25))
// 等待是否补充足房间
time.AfterFunc(time.Second*time.Duration(robotTime), func() {
GR.WaitChan <- ON_RO_JOIN
})
case ON_RO_JOIN:
// 不足AI
joinRobots(GR, 0)
// 剩余倒计时
time.AfterFunc(time.Second*time.Duration(gameStartTime-robotTime), func() {
GR.WaitChan <- ON_GAME_START
})
case ON_GAME_START:
// 游戏开始
GR.Start()
case ON_RO_ANS_1:
// 机器人第一次回答
helpRobotAnswer(GR, true, false)
time.AfterFunc(time.Second*time.Duration(1), func() {
GR.WaitChan <- ON_RO_ANS_2
})
case ON_RO_ANS_2:
// 机器人第二次回答
helpRobotAnswer(GR, false, false)
time.AfterFunc(time.Second*time.Duration(1), func() {
GR.WaitChan <- ON_RO_ANS_3
})
case ON_RO_ANS_3:
helpRobotAnswer(GR, false, true)
// 倒计时回答结束
time.AfterFunc(time.Second*time.Duration(GR.AnswerTimeout-4), func() {
GR.WaitChan <- ON_ANSWER_END
})
case ON_ANSWER_END: // 回答结束,结算
GR.onAnswerOver()
case ON_SEND_QUESTION: // 出题
GR.sendQuestion()
case MSG_END_GAME: // 游戏结束
//break game
break Playing
// goto END
}
}
}
//END:
log.Debug("Game goroutine over ->%s", GR.Uuid)
}
三、Leaf 中如何做消息广播
游戏服务器一定需要用户管理,最常见的方式就是建立用户 ID 到用户实例的映射关系(还有可能存在用户帐号 ID、用户名到用户实例的映射关系)。例如:
users = make(map[int]*User)
User 本身为了简单,可以直接组合 Agent:
type User struct {
gate.Agent
}
这样的话,广播消息就是:
for _, user := range users {
user.WriteMsg(msg)
}
一个最简单的广播的例子:打开 Leafserver game/internel/chanrpc.go 文件,加入一个全局变量。
var agents = make(map[gate.Agent]struct{})
agents 的管理:
// agent 被创建时
func rpcNewAgent(args []interface{}) {
a := args[0].(gate.Agent)
agents[a] = struct{}{}
}
// agent 被关闭时
func rpcCloseAgent(args []interface{}) {
a := args[0].(gate.Agent)
delete(agents, a)
}
由此可见 agents 中保存了当前所有连接,广播的处理:
for a := range agents {
a.WriteMsg(msg)
}