使用GoWorld游戏服务器引擎轻松实现分布式聊天服务器

GoWorld游戏服务器引擎简介

GoWorld是一款开源的分布式可扩展的游戏服务器引擎,使用Go语言(Golang)编写。它采用类似BigWorld的结构,使用了简化的场景-对象框架。以一个典型的MMORPG为例,每个服务器上会有多个场景,每个场景里可以包含多个对象,这些对象包括玩家、NPC、怪物等。GoWorld服务器可以将场景分配到在不同的进程甚至不同的机器上,从而使得游戏服务器的负载是可扩展的。

开源分布式游戏服务器引擎:https://github.com/xiaonanln/goworld,欢迎赏星,共同学习 也可在GitHub Wiki查看本文

聊天室是游戏里非常常见的一个功能,例如一个MMORPG游戏里会有世界聊天室、职业聊天室、帮派聊天室等。GoWorld为此提供了非常简单且高效率的支持,使得开发者可以轻松实现分布式的聊天室功能。这里我们就是尝试使用GoWorld所提供的功能实现一个分布式可扩展的聊天服务器。

聊天室功能说明

我们要实现的聊天室包含以下的一些功能:

  • 注册
  • 登录
  • 说话
  • 切换聊天室

为了面向更广大的游戏开发者,我们使用Cocos Creater 1.5.2开发聊天室客户端,客户端编程语言使用Javascript。 由于这仅仅只是一个Demo,因此服务端和客户端的功能都相对简单,但是依然用到了GoWorld的一些非常卓越的特性。

使用GoWorld开发分布式聊天服务器

安装Go 1.8.3

编译GoWorld需要安装Golang 1.8.3,请确保自己的Go的版本足够新。

获得GoWorld开源游戏服务器

运行以下的命令获得GoWorld游戏服务器引擎。由于GoWorld依赖其他较多的外部库,因此这个过程可能需要花费一点时间。

go get -u github.com/xiaonanln/goworld

编写聊天服务端代码

我们在%GOPATH%/src/github.com/xiaonanln/goworld/examples/chatroom_demo目录中开发聊天室服务端。可以通过地址 https://github.com/xiaonanln/goworld/tree/master/examples/chatroom_demo 查看所有的服务端代码。

聊天服务器主函数

使用GoWorld开发服务端需要开发者提供main函数入口。main函数一般会注册几个自定义的类型,然后调用goworld.Run进入游戏服务端主循环。

// serverDelegate 定义一些游戏服务器的回调函数(必须提供)
type serverDelegate struct {
	game.GameDelegate
}

func main() {
	goworld.RegisterSpace(&MySpace{}) // 注册自定义的Space类型(必须提供)

	// 注册Account类型
	goworld.RegisterEntity("Account", &Account{}, false, false)
	// 注册Avatar类型,并定义属性
	goworld.RegisterEntity("Avatar", &Avatar{}, true, true).DefineAttrs(map[string][]string{
		"name":     {"Client", "Persistent"},
		"chatroom": {"Client"},
	})

	// 运行游戏服务器
	goworld.Run(&serverDelegate{})
}

如上所示,main函数的逻辑非常简单。首先注册一个自定义的场景对象类型MySpace,这个是GoWorld强制要求的,否则运行会报错。然后main注册了两个实现聊天服务器逻辑的对象类型(Account和Avatar)。Account负责注册和登录流程,Avatar负责玩家聊天逻辑。这两个类型的具体实现在下文继续详述。最后main调用goworld.Run运行游戏服务器的主循环。

自定义场景类型MySpace

GoWorld引擎要求我们必须在main里注册一个自定义的场景类型。由于聊天服务器没有任何场景逻辑,因此这个类型也没有任何具体的代码实现。场景可以帮助实现MMORPG游戏、或者开房间类型的游戏,但是对于一个简单的聊天服务器来说并没有什么作用。因此我们只定义并注册一个空的场景类型即可。

// MySpace 是一个自定义的场景类型
//
// 由于聊天服务器没有任何场景逻辑,因此这个类型也没有任何具体的代码实现
type MySpace struct {
	entity.Space // 自定义的场景类型必须继承一个引擎所提供的entity.Space类型
}

账号对象类型Account

账号类型定义如下。所有的自定义对象类型都必须继承entity.Entity。当有玩家客户端连接服务器的时候,服务器就会自动创建一个Account对象,并且将新的客户端作为Account对象的客户端。在GoWorld引擎中,每个对象都可以有最多一个客户端对象。这样,服务端对象就可以通过RPC调用客户端对象的一些函数,并通过属性机制更新客户端对象的一些属性。

// Account 是账号对象类型,用于处理注册、登录逻辑。
type Account struct {
	entity.Entity // 自定义对象类型必须继承entity.Entity
	logining      bool
}

账号注册

当客户端点击注册的时候,就会给服务端发送一个注册的RPC请求。Account对象需要定义一个函数(Register_Client)来接受这个RPC请求,如下所示。函数名末尾的_Client代表这是一个可以由客户端调用的RPC函数。Register_Client函数使GoWorld引擎提供的方便的KVDB模块进行账号-密码数据的存储和读取。当账号密码不存在的时候,就在KVDB中插入新的账号和密码。注册过程在创建新账号的同时,创建一个Avatar对象,然后立刻销毁。这是为了在数据库中生成新的Avatar对象的数据,并获得其唯一的ID(avatarID)并将Avatar的ID也存入到KVDB中,和这个账号进行绑定。

func (a *Account) Register_Client(username string, password string) {
	goworld.GetKVDB("password$"+username, func(val string, err error) {
		if val != "" {
			a.CallClient("ShowError", "这个账号已经存在")
			return
		}
		goworld.PutKVDB("password$"+username, password, func(err error) {
			avatarID := goworld.CreateEntityLocally("Avatar") // 创建一个Avatar对象然后立刻销毁,产生一次存盘
			avatar := goworld.GetEntity(avatarID)
			avatar.Attrs.Set("name", username)
			avatar.Destroy()
			goworld.PutKVDB("avatarID$"+username, string(avatarID), func(err error) {
				a.CallClient("ShowInfo", "注册成功,请点击登录")
			})
		})
	})
}

账号登录

Account对象使用Login_Client处理来自客户端的登录请求,如下所示。 首先,从KVDB中获得正确的账号和密码并和玩家所提供的密码进行比较。如果密码正确,我们再次使用KVDB获得账号所对应的Avatar ID,并使用这个Avatar ID开始从数据库里载入Avatar对象。

func (a *Account) Login_Client(username string, password string) {
	goworld.GetKVDB("password$"+username, func(correctPassword string, err error) {
		if correctPassword == "" {
			a.CallClient("ShowError", "账号不存在")
			return
		}

		if password != correctPassword {
			a.CallClient("ShowError", "密码错误")
			return
		}

		goworld.GetKVDB("avatarID$"+username, func(_avatarID string, err error) {
			avatarID := common.EntityID(_avatarID)
			goworld.LoadEntityAnywhere("Avatar", avatarID)
			a.Call(avatarID, "GetSpaceID", a.ID)
		})
	})
}

这里我们使用goworld.LoadEntityAnywhere函数载入Avatar对象。在一个分布式服务器中,Avatar对象可能在任意一个服务端逻辑进程中创建。因此在这种情况下,Account向刚载入的Avatar对象发起一次GetSpaceID请求,试图获得Avatar对象所在的场景。Avatar对象需要定义GetSpaceID函数来处理请求,并把自己所在的场景ID发送给Account对象,代码如下所示。和上面的_Client结尾函数不同的是,这里的RPC调用者和接受者都是服务端的对象,因此不需要提供_Client标记。

func (a *Avatar) GetSpaceID(callerID EntityID) {
	a.Call(callerID, "OnGetAvatarSpaceID", a.ID, a.Space.ID)
}

Account对象在收到OnGetAvatarSpaceID回调之后,可以通过EnterSpace请求让自己迁移到Avatar对象所在的进程,代码如下所示。场景切换是GoWorld所提供的强大的对象操作功能,它使得服务端的对象可以在各个场景里方便的切换,大幅度简化了开发者实现分布式服务的开发难度。

func (a *Account) OnGetAvatarSpaceID(avatarID common.EntityID, spaceID common.EntityID) {
	// 如果发现Avatar对象和Account对象在同一个服务器,则不需要进行场景切换
	avatar := goworld.GetEntity(avatarID)
	if avatar != nil {
		a.onAvatarEntityFound(avatar)
		return
	}

	a.Attrs.Set("loginAvatarID", avatarID)
	a.EnterSpace(spaceID, entity.Position{})
}

Account对象在切换场景结束之后,再次在当前逻辑进程里寻找指定的Avatar对象。然后调用onAvatarEntityFound函数完成最后的登录逻辑,也就是通过GiveClientTo函数把Account当前的客户端连接移交给Avatar对象,然后Account对象因为失去客户端而被销毁。

func (a *Account) OnMigrateIn() {
	loginAvatarID := common.EntityID(a.Attrs.GetStr("loginAvatarID"))
	avatar := goworld.GetEntity(loginAvatarID)

	if avatar != nil {
		a.onAvatarEntityFound(avatar)
	} else {
		// failed
		a.CallClient("ShowError", "登录失败,请重试")
		a.logining = false
	}
}

func (a *Account) onAvatarEntityFound(avatar *entity.Entity) {
	a.GiveClientTo(avatar)
}

// OnClientDisconnected 会在对象失去客户端的时候被调用
func (a *Account) OnClientDisconnected() {
	a.Destroy()
}

Account对象在载入Avatar对象并完成登录的过程似乎有些复杂,涉及到Avatar对象载入,两次RPC调用以及一次对象迁移。不过GoWorld所提供的机制使得我们可以方便地将Avatar对象创建到各个不同的服务器进程中。

Avatar对象逻辑

Avatar对象代表一名已经登录的聊天室玩家。和上述的Account对象一样,我们首先需要定义一个Avatar类型。

定义与初始化

// Avatar 对象代表一名玩家
type Avatar struct {
	entity.Entity
}

// OnCreated 函数会在对象创建结束的时候调用
func (a *Avatar) OnCreated() {
	a.Entity.OnCreated()
	a.setDefaultAttrs()
}

func (a *Avatar) setDefaultAttrs() {
	a.Attrs.Set("chatroom", "1")
	a.SetFilterProp("chatroom", "1")
}

当Avatar对象载入成功之后,我们会为它设置默认的聊天室。

通过使用a.Attrs.Set将Avatar对象的chatroom属性设置为1。属性机制是GoWorld所提供的一种存储对象信息,并提供对象数据自动存盘、自动同步到客户端的机制。因此服务端对象在设置chatroom属性的同时,客户端也会收到这个属性的更新,并同步到UI界面上。

然后Avatar对象使用SetFilterProp函数设置自己的一个filter属性:chatroom = 1。Filter属性机制是GoWorld为了高效率地实现游戏里各种聊天室所提供的客户端过滤和通知机制。服务端可以使用Filter属性机制向所有满足filter属性要求的对象的客户端发起广播,这样的效率要远远优于一个个对一个对象进行扫描并发送客户端RPC。

说话和切换聊天室

Avatar对象提供SendChat_Client函数来处理来自客户端的说话请求,如下所示。

func (a *Avatar) SendChat_Client(text string) {
	text = strings.TrimSpace(text)
	if text[0] == ‘/‘ {
		// this is a command
		cmd := spaceSep.Split(text[1:], -1)
		if cmd[0] == "join" {
			a.enterRoom(cmd[1])
		} else {
			a.CallClient("ShowError", "无法识别的命令:"+cmd[0])
		}
	} else {
		a.CallFitleredClients("chatroom", a.GetStr("chatroom"), "OnRecvChat", a.GetStr("name"), text)
	}
}

SendChat_Client把以/开头的内容当作一个命令,并进行特殊处理。其他内容则作为普通的说话内容,并通过调用引擎Filter属性机制所提供的CallFitleredClients函数将说话人的名字和内容都发送到所有在当前聊天室的玩家客户端。

如果玩家发了一个/join ...的命令,则会被看成一个切换聊天室的请求。切换聊天室的逻辑非常简单,只需要将聊天室名字设置为新的Filter属性值,并设置为玩家属性从而更新到客户端即可。

func (a *Avatar) enterRoom(name string) {
	a.SetFilterProp("chatroom", name)
	a.Attrs.Set("chatroom", name)
}

聊天室客户端

聊天室客户端的代码都在:https://github.com/xiaonanln/goworld-chatroom-demo-client ,由Javascript编写。客户端代码除了对服务端通信协议进行解析和封装之外,其他界面逻辑非常简单,因此这里不再详述。另外在http://goworldgs.com/chatclient/上也有一个可运行的客户端和服务端实现,有兴趣的可以点开查看。

编译运行服务端

一个完整的GoWorld服务器包含三个部分:中心分发器、网关服务器和逻辑服务器。我们刚才所编写的代码全是逻辑服务器的代码,中心分发器和网关服务器是固定的程序,直接编译运行即可。

编译中心分发器dispatcher

cd %GOPATH%/src/github.com/xiaonanln/goworld/components/dispatcher
go build

编译网关服务器

cd %GOPATH%/src/github.com/xiaonanln/goworld/components/gate
go build

编译chatroomdemo游戏服务器

cd %GOPATH%/src/github.com/xiaonanln/goworld/examples/chatroom_demo
go build

设置GoWorld配置文件

我们使用goworld根目录下的goworld.ini.sample作为游戏服务器的配置文件。

cd %GOPATH%/src/github.com/xiaonanln/goworld
cp goworld.ini.sample goworld.ini

配置文件设置了KVDB所使用的数据库类型(默认为MongoDB)、Avatar对象数据库所使用的数据库类型(默认为MongoDB),以及dispatcher、gate、game所使用的各种配置。如果使用MongoDB作为KVDB和对象数据库,请另外安装和运行MongoDB 3.x。

运行服务器

cd %GOPATH%/src/github.com/xiaonanln/goworld
nohup components/dispatcher/dispatcher &
nohup components/gate/gate -gid 1 &
nohup examples/chatroom_demo/chatroom_demo -gid 1 &

运行客户端

在Cocos Creater中设置所有Scene中的GoWorld对象的地址为localhost,端口为网关服务器(gate1)的http端口,默认15012,然后运行客户端即可连接到本地服务器。

总结

如上所述,在使用GoWorld所提供的分布式场景-对象框架和其他功能的情况下,我们可以轻松开发出一个分布式可扩展的聊天室服务端。不过GoWorld所提供的功能更适合开发分场景、分房间的游戏类型。另外GoWorld所提供的热更新功能对于大型的游戏服务端项目来说也是必不可少的。

开源分布式游戏服务器引擎:https://github.com/xiaonanln/goworld,欢迎赏星,共同学习

对Go语言服务端开发感兴趣的朋友欢迎加入QQ讨论群:662182346

时间: 2024-10-19 03:21:45

使用GoWorld游戏服务器引擎轻松实现分布式聊天服务器的相关文章

MMO 游戏服务器引擎设计

一. 网络游戏开发的基本流程 ◆ 项目文档 ◆ 开发的进行和文档准备流程 ◆ 技术人员文档 二. MMO游戏架构 ◆ MMO游戏特点 ◆ MMO架构的特有内容 三. 策划文档 ◆ 考虑示例游戏的题材 ◆ 详细设计文档 ◆ MMO庞大的游戏设定 ◆ 5种设计文档 系统的基本结构图 进程关系图 资源评估文档 协议定义文档 数据库设计图 ◆ 设计上的重要判断 四. 系统基本结构图 ◆ 系统基本结构图的基础 ◆ 服务器必须具有可扩展性 ---- 商业模式的确认 ◆ 各瓶颈 ---- 扩展方式的选择 ◆ 

在线聊天室的实现(4)--分布式聊天室的基础架构

前言: 前面都在讲述如何实现一个简单的聊天室, 并回顾了websocket的协议, 以及Netty 4.x的简单使用. 但如果仅局限于单机的聊天室实现, 那显然难登"大雅之堂". 借这个机会, 想尝试聊一下千万级聊天室的实现. 同时浅谈一下游戏中, 公共的聊天室资源服务定位. 本系列的文章链接如下: 1). websocket协议和javascript版的api 2). 基于Netty 4.x的Echo服务器实现  3). 简易聊天室的实现 架构演进: 这边讲述一下聊天室服务的思考过程

手机游戏服务器引擎Scut免费开源

scut 官网:http://www.scutgame.com/ Scut是一个开源.免费.稳定.快速开发的手机游戏服务器引擎,支持开发人员使用Python脚本语言或C#语言开发:底层采用C#编写,基于MVC框架思想设计, 开发人员只需要关注如何定义数据实体类及属性,不再需要关注多据库(MSSQL.MySql等)及表设计,Scut会帮助你自动检测生成相应数据库的表结构:它还提供了丰富的AIP和成熟的游戏模块中间件,快速开发你的游戏服务器应用,和Cocos2d-x完美结合,提供基于Cocos2d-

legend分布式服务器集群中的数据库服务器的性能测试

今天将把如下图所示测试用例进行测试: MainCache代表主线程是缓存操作DaemonORM代表守护线程是ORM入库操作,其中ORM开启了事务处理OnlyORM代表仅仅使用ORM直接入库操作 本框架采用的是MainCache+DaemonORM的机制,即所有玩家对DB的任何操作都是在内存中进行,任何变更都会由守护线程后台通过ORM同步到DB中以下测试结果对MainCache+DaemonORM与OnlyORM进行了对比 在里面有一万条记录的基础上做单条记录的操作: 7.让数据库插入一条记录时的

分布式Web服务器架构

最开始,由于某些想法,于是在互联网上搭建了一个网站,这个时候甚至有可能主机都是租借的,但由于这篇文章我们只关注架构的演变历程,因此就假设这个时候已经是托管了一台主机,并且有一定的带宽了,这个时候由于网站具备了一定的特色,吸引了部分人访问,逐渐你发现系统的压力越来越高,响应速度越来越慢,而这个时候比较明显的是数据库和应用互相影响,应用出问题了,数据库也很容易出现问题,而数据库出问题的时候,应用也容易出问题,于是进入了第一步演变阶段:将应用和数据库从物理上分离,变成了两台机器,这个时候技术上没有什么

分布式数据库服务器的四层架构

分布式数据库服务器的四层架构: 访问层:接收访问信息并按负荷智能的分配给中转服务器,接受数据结果并返回客户端. 中转层:接收访问服务器发来的数据访问指令,从总储存服务器寻找数据分布所在的储存服务器,发送指令. 表头层:储存数据的表头信息,以确定储存服务器位置. 处理层:分布式数据储存服务器,接收指令并执行,然后返回数据给访问服务器. 功能分布: 访问服务器只做四件事:接收客户端的访问数据,接收中转服务器的负荷状态信息,并且把数据分配给负荷最低 的中转服务器,接收结果后返回客户端. 中转服务器只做

Photon服务器引擎(二)socket/TCP/UDP基础及Unity聊天室的实现

Photon服务器引擎(二)socket/TCP/UDP基础及Unity聊天室的实现 我们平时说的最多的socket是什么呢,实际上socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API). 通过Socket,我们才能使用TCP/IP协议.实际上,Socket跟TCP/IP协议没有必然的联系.Socket编程接口在设计的时候,就希望也能适应其他的网络协议.所以说,Socket的出现只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象,

IOKING真正无锁服务器引擎之消息引擎模块Demo(no-lock)

 关键词: no-lock interlocked lock-free tcp/ip socket server engine epoll iocp server out-of-orderexecution无锁 原子锁 原子操作 原子指令 锁无关 开放锁 通讯服务器 引擎 高并发 大数据 搜索引擎 完成端口服务器 cpu乱序并行执行 内存栅栏 IOKING 真正无锁服务器引擎之消息引擎模块Demo(no-lock) 这是继无锁iocp通讯模块以后,又一个无锁模块.下一步有时间将会把两个整合在

HTML5游戏开发引擎Pixi.js新手入门讲解

在线演示 本地下载 ?这篇文章中,介绍HTML5游戏引擎pixi.js的基本使用. 相关代码如下: Javascript 导入类库:(使用极客的cdn服务:http://cdn.gbtags.com) <scripttype="text/javascript"src="http://cdn.gbtags.com/pixi.js/1.6.1/pixi.js"></script> 引擎使用: .... .... 阅读原文:HTML5游戏开发引擎P