这是使用go语言实现的一个网游服务端框架,得益于go语言对多核编程的良好支持,可充分利用服务器的多核优势。
1.服务端运行时架构
使用本框架编写的服务端以单进程执行,进程内部就是数个goroutine的大杂汇。这些goroutine主要分两种,一种是Agent,作为客户端在服务端的代理,另一种是Service,表示游戏中的各种基础玩法。
这两类goroutine都由消息驱动,保证时序。Agent负责处理仅操作玩家个人数据的业务逻辑,例如装备、任务、人物属性等。Service则负责处理需要操作游戏世界公共资源的业务逻辑,例如场景、竞技场、拍卖所等。
每个客户端与服务端保持一条tcp长连接,直接与自身的代理Agent进行通信。架构上没有规划网关,但框架已提供了广播服务,所以对网关的需求目前来看并不大。
2.框架概述
如上所述,应用层开发需要编写Agent及Service两类业务逻辑,两者都是消息驱动,框架的核心仅仅是一个消息的通道而已,Agent和Service都可以向这个通道投递消息,也可以指定自己要接收的消息。
消息分两种,第一种是来自框架外部的外源消息。不同的外源消息通过预定义的类型值(整数类型)区分,框架对底层通信进行了封装,要接收消息注册回调即可,回调函数类型为:
type NetMsgCB func(*Agent, proto.Message)
回调函数通过registerHandler注册到框架的消息分派器,例如:
registerHandler(pb.MECHO, &pb.MQEcho{}, echo)
其中pb是为了让main package再干净而独立出来的package,MECHO是整数类型的消息类型常量,echo是NetMsgCB类型的函数,MQEcho是消息包的结构,需要传递一个消息结构体的空间用于解包。框架依赖于protobuf,目前不能很方便地切换协议(需要改动以net开头的文件)。
发送外源消息使用replyMsg,例如:
replyMsg(agent, pb.MECHO, rep)
其中agent是客户端的代理实体,里面包含了客户端的连接信息(实际上是一切信息,Agent业务逻辑处理的就是agent的数据),rep是protobuf的数据包。
而一种消息是来自框架内部的内源消息。内源消息类型定义为InnserMsg,这个结构体类型包含了消息类型(cmd)、消息参数(ud)以及消息返回(reply),考虑到内源消息类型并不多,cmd使用的是string类型,而消息参数及返回都是interface{}类型,处理时需要自行转换到相应类型。
和外源消息的处理一样,处理注册回调函数以处理对应类型的内源消息,但和外源消息的回调注册分散在各个同的逻辑模块文件不同,内源消息的注册统一放在了innermsg.go。原因在于内源消息的类型太少了。
目前提供了两个函数用于发送内源消息:sendInnerMsg
和call
,前者只发送消息不期待返回,而后者会阻塞直至消息处理返回。
需要明确的一点是,内源消息通常只用于Agent,因为对于Service而言,使用消息进行请求回复并不是理想的接口(甚至call
也不是),我希望Service能做得更好一些,调用者可以无视消息这一概念,例如:
broadcast(bc, msg)
而不是:
sendInnerMsg(bc, "push", msg)
这样额外的好处是,Service可以根据需要选择goroutine或者锁的方式去实现。
除了消息通道这个核心功能外,框架还负责了进程的启动与关闭,提供了通用的日志接口,实现了agent的管理服务以及广播服务。
3.与客户端的通信
每个客户端将保持一条tcp长连接与服务端进行通信,数据包格式为|len (2 bytes)| + |data|
,其中len为2字节的data的长度,使用大端字节序,data为自定义的协议数据,框架默认使用protobuf作为data的编码协议。
4.代码规范
文件名: 消息处理入口(xxxhandler.go),消息处理逻辑主体(xxx.go),当逻辑简单时可以直接写在xxxhandler.go。例如taskhandler.go和task.go。数据加载保存(xxxdb.go),服务(xxx.go)。
类型、函数、方法及变量均使用驼峰式命名,除类型名称首字母大写,其余首字母小写。常量均用大写,前置业务逻辑的前缀,以下划线分隔,如XXX_XXX。所有文件使用utf8编码,使用gofmt进行格式化。
总的来说,这是一个多线程的事件驱动模型框架,多线程的好处在于充分利用多核,难点在于线程间的同步,得益于业务逻辑的分离以及go语言本身对多核编译的良好支持,这个难点在这里并不存在。同时goroutine可以放心地阻塞,这为rpc之类的实现提供了可能。对比其他框架,多线程的没有我们简单,当然这更多的是针对框架的实现而言,因为通常多线程的框架对上层都表现为单线程。多进程的没有我们高效,进程间通信不会比线程间通信快。单线程的没有我们强大。使用动态语言的没有我们的速度。
当然,我们不及多进程灵活(跨机步骤,单个进程崩溃不影响全局),没有单线程来得简单,没有动态语言的热更新。
根据经验,本框架适用于中小型的MMO游戏(限时战略及动作类游戏慎用)。
提供Service的外源消息支持,有可能统一Agent与Service的处理;抽离强加的游戏逻辑;与protobuf解耦。
安装mysql及protobuf
配置go开发环境,参见: http://pkg.golang.org/doc/code.html
安装依赖包:
go get github.com/golang/protobuf/{proto,protoc-gen-go}
go get github.com/go-sql-driver/mysql
go get github.com/bitly/go-simplejson
安装游戏框架:
go get github.com/g-xianhui/op
cd $GOPATH/src/github.com/g-xianhui/op/server
go build
cd $GOPATH/src/github.com/g-xianhui/op/client
go build
生成数据库:
cd $GOPATH/src/github.com/g-xianhui/op/database
mysql <game.sql
现在server及client目录下分别生成了server及client可执行文件,在不同终端分别执行:
./server
./client
client可执行的指令有请求角色列表(rolelist),创建角色(createrole),登录(login),登出(logout),echo以及chat,具体用法可参考cmd.go。
在正式的场合应该将server进程变为精灵进程,由于go语言本身不支持daemonize,所以需要daemonize之类的工具的支持。作为例子,安装daemonize之后可使用control.sh脚本启动及关闭服务端进程。
默认的服务端配置在config.json,正式场合切记修改。