Go语言网络游戏服务器模块化编程
本文以使用 origin框架(一款使用Go语言写的开源游戏服务器框架)为例进行说明,当然也可以使用其它的框架或者自己写。
在框架中 PBProcessor用来处理Protobuf消息,在使用之前,需要使用Register函数注册网络消息:
1func (pbProcessor *PBProcessor) Register(msgType uint16, msg proto.Message, handle MessageHandler) {
2 var info MessageInfo
3
4 info.msgType = reflect.TypeOf(msg.(proto.Message))
5 info.msgHandler = handle
6 pbProcessor.mapMsg[msgType] = info
7}
网络消息来时通过MsgRoute进行分发:
1func (pbProcessor *PBProcessor) MsgRoute(clientId string, msg interface{}, recyclerReaderBytes func(data []byte)) error {
2 pPackInfo := msg.(*PBPackInfo)
3 defer recyclerReaderBytes(pPackInfo.rawMsg)
4
5 v, ok := pbProcessor.mapMsg[pPackInfo.typ]
6 if ok == false {
7 return fmt.Errorf("cannot find msgtype %d is register", pPackInfo.typ)
8 }
9
10 v.msgHandler(clientId, pPackInfo.msg)
11 return nil
12}
这是框架提供的基础功能。在平常使用中最方便的就是游戏中各个功能模块相互独立,减少耦合性。
Go语言中支持包,可以将包看作一个模块,进行模块化编程。游戏中常见的操作是玩家数据的存取以及网络消息处理,可以定义接口:
1package common
2
3// 游戏逻辑模块的存取
4type ISaveLoad interface {
5 // 由于每个模块使用的PB不一样,在使用InitFromPB之前需要调用NewPB创建PB
6 NewPB() proto.Message
7 // 从PB中读取模块数据
8 InitFromPB(pb proto.Message)
9 // 完成数据读取后的处理,主要解决模块数据的相互依赖,比如模块A可能会依赖模块B,但模块B的数据可能还没读取出来
10 PostLoad(pb proto.Message)
11 // 保存模块数据到PB中,bSave2DB用于判断是存数据库,还是发送给客户端,可能存在有些数据不能发给客户端,可以通过此变量进行处理
12 Save2PB(bSave2DB bool) proto.Message
13}
14
15// 游戏逻辑模块
16type IGameModule interface {
17 ISaveLoad
18 // 获取模块ID,这些ID都可以写在PB中,客户端、服务器共用,玩家上线时,服务器可以根据模块ID发送数据给客户端
19 GetModuleID() netmsg.ModuleID
20}
21
22// 游戏逻辑模块的工厂模式
23type IModuleFactory interface {
24 // 新建模块,每个模块中都有一个IRole归属,方便使用角色中的其它模块数据
25 NewModule(owner IRole) IModule
26 // 注册模块中的网络消息
27 RegNetMsg()
28}
29
30// 游戏角色
31type IRole interface {
32 // 账号ID
33 GetUserID() uint64
34 // 角色ID
35 GetRoleID() uint64
36 // 发消息给客户端
37 SendMsg2Client(cmdId netmsg.NetCmdID, pb proto.Message)
38 // 获取角色中的游戏模块
39 GetGameModule(ID netmsg.ModuleID) IGameModule
40}
定义好接口后,就可以将Go的包当然游戏模块编写逻辑了,这样写的逻辑会比较清晰。
下面以背包模块为例来说明,创建一个bag目录来作为游戏逻辑模块,里面再分文件来区分是模块注册(bag_mod.go
),模块IO(bag_io.go
),模块逻辑(bag.go
)等等,为什么里面还要这么分,是因为当一个模块比较大时,定位起来方便,比如在实际开发中经常需要定位要IO部分,可能需要修改与客户端的通信。
bag.go
:
1package bag
2
3type bag struct{
4 owner common.IRole
5 cap uint16
6}
7
8func newBag(owner common.IRole) {
9 return &bag{owner: owner}
10}
11
12func (slf *bag) addItem(pb *netmsg.BagAddItem) {
13}
bag_mod.go
:
1package bag
2
3func init() {
4 // 这里向模块管理器注册模块,模块管理器会调用factory的NewModule创建模块,调用RegNetMsg注册网络消息
5 mod.RegModule(netmsg.ModuleID_Bag, factory{})
6}
7
8type factory struct {
9}
10
11func (f factory) NewModule(owner common.IRole) common.IGameModule {
12 return newBag(owner)
13}
14
15// 注册本模块中的所有网络消息处理函数
16func (f factory) RegNetMsg() {
17 mod.RegNetMsg(netmsg.NetCmdID_AddItem, onAddItem)
18}
19
20func onAddItem(p *bag, pb *netmsg.BagAddItem) {
21 p.addItem(pb)
22}
bag_io.go
:
1package bag
2
3func (slf *Bag) GetModuleID() netmsg.ModuleID {
4 return netmsg.ModuleID_Bag
5}
6
7func (slf *Bag) NewPB() proto.Message {
8 return &netmsg.Bag{}
9}
10
11func (slf *Bag) InitFromPB(pb proto.Message) {
12 msg := pb.(*netmsg.Bag)
13 slf.cap = msg.Cap
14}
15
16func (slf *Bag) Save2PB(isSave2DB bool) proto.Message {
17 return &netmsg.Bag{Cap: slf.cap}
18}
19
20func (slf *Bag) PostLoad(pb proto.Message) {
21}
前面代码中有使用到mod
包,它是模块的管理包。
mod.go
1package mod
2
3var (
4 modules = map[netmsg.ModuleID]common.IModuleFactory{}
5 mapNetMsg = map[netmsg.NetCmdID]netmsg.ModuleID{}
6 process *processor.PBProcessor
7 modType netmsg.ModuleID
8)
9
10// 供各逻辑模块调用以注册模块
11func RegModule(moduleID netmsg.ModuleID, module common.IModuleFactory) {
12 if _, ok := modules[id]; ok {
13 log.Fatalf("Repeated RegModule Module ID:%s", moduleID.String())
14 return
15 }
16 modules[id] = module
17}
18
19// 供Service调用以注册各模块的网络消息
20func RegModuleNetMsg(p *processor.PBProcessor) {
21 // 记录下处理器
22 process = p
23 for _, m := range modules {
24 m.RegNetMsg()
25 }
26}
27
28// 供各模块调用以注册本模块的网络消息处理。M为模块结构指针,T为处理函数使用的网络消息结构指针
29func RegNetMsg[T proto.Message, M any](cmdId netmsg.NetCmdID, handle func(M, T)) {
30 f := func(p common.IRole, pb T) {
31 // 根据网络消息ID查模块ID
32 id, ok := mapNetMsg[cmdId]
33 if !ok {
34 return
35 }
36 // 根据模块ID获取取模块
37 m := p.GetGameModule(id)
38 if m != nil {
39 defer func() {
40 if r := recover(); r != nil {
41 buf := make([]byte, 4096)
42 l := runtime.Stack(buf, false)
43 errString := fmt.Sprint(r)
44 log.Errorf("UserID:%d RoleID:%d Module:%v NetMsg:%v Core dump info[%s]\n%s",
45 p.GetUserID(), p.GetRoleID(), id, cmdId, errString, string(buf[:l]))
46 }
47 }()
48 // 调用处理函数时,把模块接口转为实际的模块指针
49 handle(m.(M), pb)
50 }
51 }
52 if _, ok := mapNetMsg[cmdId ]; ok {
53 panic("Repeated RegModule Module NetMsg:%s", id.String())
54 } else {
55 mapNetMsg[cmdId ] = modType
56 }
57 register(cmdId, f)
58}
59
60// 注册网络消息处理器,UserData为游戏逻辑模块结构的指针,T为模块网络消息处理函数中使用的网络消息结构指针
61func register[T proto.Message, UserData any](cmdId netmsg.NetCmdID, handle func(UserData, T)) {
62 f := func(userData interface{}, msg proto.Message) {
63 // 转换为游戏逻辑模块结构的指针
64 p := userData.(UserData)
65 // 转换为消息处理函数中使用的网络消息结构指针
66 pb := msg.(T)
67 handle(p, pb)
68 }
69 var pb T
70 // 这里调用origin框架的PB处理器,注册网络消息处理函数
71 process.Register(uint16(cmdId), pb, f)
72}
在各个模块中调用mod.RegModule
来注册模块,如bag_mod.go
所示。
然后在origin的服务中调用mod.RegModuleNetMsg
来注册各模块的网络消息。比如:
1package myService
2
3func init() {
4 // 注册服务service
5}
6
7type service struct {
8 service.Service
9 process *processor.PBProcessor
10}
11
12func (slf *myService) OnInit() error {
13 slf.process = processor.NewPBProcessor()
14 mod.RegModuleNetMsg(slf.process)
15}
这样就可以清晰地写游戏逻辑中的模块了。
如果本文对你有帮助,欢迎点赞收藏!
- 原文作者:Witton
- 原文链接:https://wittonbell.github.io/posts/2025/2025-07-08-Go语言网络游戏服务器模块化编程/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。