在查看官方issues时,也发现了一些个人觉得有用的讨论,摘录一下

Q:leaf的log蛮好用的了,可以log到文本。但是现在需要做日志分析。要能支持json格式,异步log等等。希望可以加强下。
看了下zap日志库,感觉过去复杂了。有没有简单实用,能分析的日志库?
A:Leaf 的 log 的定位并非用于数据分析,最好额外加一套用于数据分析的机制,同时也保留 Leaf 的 log。

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

游戏服务器一定需要用户管理,最常见的方式就是建立用户 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)
}