置顶 使用GoWorld游戏服务器引擎轻松实现分布式聊天服务器
发布于 14 天前 作者 xiaonanln 82 次浏览 来自 分享

GoWorld游戏服务器引擎简介

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

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

聊天室是游戏里非常常见的一个功能,例如一个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

回到顶部