Go语言网络游戏服务器模块化编程

本文以使用origin框架(一款使用Go语言写的开源游戏服务器框架)为例进行说明,当然也可以使用其它的框架或者自己写。

在框架中PBProcessor用来处理Protobuf消息,在使用之前,需要使用Register函数注册网络消息:

go 复制代码
func (pbProcessor *PBProcessor) Register(msgType uint16, msg proto.Message, handle MessageHandler) {
	var info MessageInfo

	info.msgType = reflect.TypeOf(msg.(proto.Message))
	info.msgHandler = handle
	pbProcessor.mapMsg[msgType] = info
}

网络消息来时通过MsgRoute进行分发:

go 复制代码
func (pbProcessor *PBProcessor) MsgRoute(clientId string, msg interface{}, recyclerReaderBytes func(data []byte)) error {
	pPackInfo := msg.(*PBPackInfo)
	defer recyclerReaderBytes(pPackInfo.rawMsg)

	v, ok := pbProcessor.mapMsg[pPackInfo.typ]
	if ok == false {
		return fmt.Errorf("cannot find msgtype %d is register", pPackInfo.typ)
	}

	v.msgHandler(clientId, pPackInfo.msg)
	return nil
}

这是框架提供的基础功能。在平常使用中最方便的就是游戏中各个功能模块相互独立,减少耦合性。

Go语言中支持包,可以将包看作一个模块,进行模块化编程。游戏中常见的操作是玩家数据的存取以及网络消息处理,可以定义接口:

go 复制代码
package common

// 游戏逻辑模块的存取
type ISaveLoad interface {
	// 由于每个模块使用的PB不一样,在使用InitFromPB之前需要调用NewPB创建PB
	NewPB() proto.Message
	// 从PB中读取模块数据
	InitFromPB(pb proto.Message)
	// 完成数据读取后的处理,主要解决模块数据的相互依赖,比如模块A可能会依赖模块B,但模块B的数据可能还没读取出来
	PostLoad(pb proto.Message)
	// 保存模块数据到PB中,bSave2DB用于判断是存数据库,还是发送给客户端,可能存在有些数据不能发给客户端,可以通过此变量进行处理
	Save2PB(bSave2DB bool) proto.Message
}

// 游戏逻辑模块
type IGameModule interface {
	ISaveLoad
	// 获取模块ID,这些ID都可以写在PB中,客户端、服务器共用,玩家上线时,服务器可以根据模块ID发送数据给客户端
	GetModuleID() netmsg.ModuleID
}

// 游戏逻辑模块的工厂模式
type IModuleFactory interface {
	// 新建模块,每个模块中都有一个IRole归属,方便使用角色中的其它模块数据
	NewModule(owner IRole) IModule
	// 注册模块中的网络消息
	RegNetMsg()
}

// 游戏角色
type IRole interface {
	// 账号ID
	GetUserID() uint64
	// 角色ID
	GetRoleID() uint64
	// 发消息给客户端
	SendMsg2Client(cmdId netmsg.NetCmdID, pb proto.Message)
	// 获取角色中的游戏模块
	GetGameModule(ID netmsg.ModuleID) IGameModule
}

定义好接口后,就可以将Go的包当然游戏模块编写逻辑了,这样写的逻辑会比较清晰。

下面以背包模块为例来说明,创建一个bag目录来作为游戏逻辑模块,里面再分文件来区分是模块注册(bag_mod.go),模块IO(bag_io.go),模块逻辑(bag.go)等等,为什么里面还要这么分,是因为当一个模块比较大时,定位起来方便,比如在实际开发中经常需要定位要IO部分,可能需要修改与客户端的通信。

bag.go

go 复制代码
package bag

type bag struct{
	owner common.IRole
	cap uint16
}

func newBag(owner common.IRole) {
	return &bag{owner: owner}
}

func (slf *bag) addItem(pb *netmsg.BagAddItem) {
}

bag_mod.go

go 复制代码
package bag

func init() {
	// 这里向模块管理器注册模块,模块管理器会调用factory的NewModule创建模块,调用RegNetMsg注册网络消息
	mod.RegModule(netmsg.ModuleID_Bag, factory{})
}

type factory struct {
}

func (f factory) NewModule(owner common.IRole) common.IGameModule {
	return newBag(owner)
}

// 注册本模块中的所有网络消息处理函数
func (f factory) RegNetMsg() {
	mod.RegNetMsg(netmsg.NetCmdID_AddItem, onAddItem)
}

func onAddItem(p *bag, pb *netmsg.BagAddItem) {
	p.addItem(pb)
}

bag_io.go

go 复制代码
package bag

func (slf *Bag) GetModuleID() netmsg.ModuleID {
	return netmsg.ModuleID_Bag
}

func (slf *Bag) NewPB() proto.Message {
	return &netmsg.Bag{}
}

func (slf *Bag) InitFromPB(pb proto.Message) {
	msg := pb.(*netmsg.Bag)
	slf.cap = msg.Cap
}

func (slf *Bag) Save2PB(isSave2DB bool) proto.Message {
	return &netmsg.Bag{Cap: slf.cap}
}

func (slf *Bag) PostLoad(pb proto.Message) {
}

前面代码中有使用到mod包,它是模块的管理包。

mod.go

go 复制代码
package mod

var (
	modules   = map[netmsg.ModuleID]common.IModuleFactory{}
	mapNetMsg = map[netmsg.NetCmdID]netmsg.ModuleID{}
	process   *processor.PBProcessor
	modType   netmsg.ModuleID
)

// 供各逻辑模块调用以注册模块
func RegModule(moduleID netmsg.ModuleID, module common.IModuleFactory) {
	if _, ok := modules[id]; ok {
		log.Fatalf("Repeated RegModule Module ID:%s", moduleID.String())
		return
	}
	modules[id] = module
}

// 供Service调用以注册各模块的网络消息
func RegModuleNetMsg(p *processor.PBProcessor) {
	// 记录下处理器
	process = p
	for _, m := range modules {
		m.RegNetMsg()
	}
}

// 供各模块调用以注册本模块的网络消息处理。M为模块结构指针,T为处理函数使用的网络消息结构指针
func RegNetMsg[T proto.Message, M any](cmdId netmsg.NetCmdID, handle func(M, T)) {
	f := func(p common.IRole, pb T) {
		// 根据网络消息ID查模块ID
		id, ok := mapNetMsg[cmdId]
		if !ok {
			return
		}
		// 根据模块ID获取取模块
		m := p.GetGameModule(id)
		if m != nil {
			defer func() {
				if r := recover(); r != nil {
					buf := make([]byte, 4096)
					l := runtime.Stack(buf, false)
					errString := fmt.Sprint(r)
					log.Errorf("UserID:%d RoleID:%d Module:%v NetMsg:%v Core dump info[%s]\n%s",
						p.GetUserID(), p.GetRoleID(), id, cmdId, errString, string(buf[:l]))
				}
			}()
			// 调用处理函数时,把模块接口转为实际的模块指针
			handle(m.(M), pb)
		}
	}
	if _, ok := mapNetMsg[cmdId ]; ok {
		panic("Repeated RegModule Module NetMsg:%s", id.String())
	} else {
		mapNetMsg[cmdId ] = modType
	}
	register(cmdId, f)
}

// 注册网络消息处理器,UserData为游戏逻辑模块结构的指针,T为模块网络消息处理函数中使用的网络消息结构指针
func register[T proto.Message, UserData any](cmdId netmsg.NetCmdID, handle func(UserData, T)) {
	f := func(userData interface{}, msg proto.Message) {
		// 转换为游戏逻辑模块结构的指针
		p := userData.(UserData)
		// 转换为消息处理函数中使用的网络消息结构指针
		pb := msg.(T)
		handle(p, pb)
	}
	var pb T
	// 这里调用origin框架的PB处理器,注册网络消息处理函数
	process.Register(uint16(cmdId), pb, f)
}

在各个模块中调用mod.RegModule来注册模块,如bag_mod.go所示。

然后在origin的服务中调用mod.RegModuleNetMsg来注册各模块的网络消息。比如:

go 复制代码
package myService

func init() {
	// 注册服务service
}

type service struct {
	service.Service
	process  *processor.PBProcessor
}

func (slf *myService) OnInit() error {
	slf.process = processor.NewPBProcessor()
	mod.RegModuleNetMsg(slf.process)
}

这样就可以清晰地写游戏逻辑中的模块了。

如果本文对你有帮助,欢迎点赞收藏!

相关推荐
量子联盟18 分钟前
原创-基于 PHP 和 MySQL 的证书管理系统,免费开源
开发语言·mysql·php
时来天地皆同力.2 小时前
Java面试基础:概念
java·开发语言·jvm
Code Warrior2 小时前
【Linux】基础开发工具(3)
linux·服务器
hackchen2 小时前
Go与JS无缝协作:Goja引擎实战之错误处理最佳实践
开发语言·javascript·golang
铲子Zzz3 小时前
Java使用接口AES进行加密+微信小程序接收解密
java·开发语言·微信小程序
小小小新人121234 小时前
C语言 ATM (4)
c语言·开发语言·算法
Two_brushes.4 小时前
【linux网络】网络编程全流程详解:从套接字基础到 UDP/TCP 通信实战
linux·开发语言·网络·tcp/udp
小白学大数据4 小时前
R语言爬虫实战:如何爬取分页链接并批量保存
开发语言·爬虫·信息可视化·r语言
争不过朝夕,又念着往昔4 小时前
Go语言反射机制详解
开发语言·后端·golang
Jerry Lau4 小时前
go go go 出发咯 - go web开发入门系列(二) Gin 框架实战指南
前端·golang·gin