/op

Primary LanguageGo

Description

这是使用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。原因在于内源消息的类型太少了。

目前提供了两个函数用于发送内源消息:sendInnerMsgcall,前者只发送消息不期待返回,而后者会阻塞直至消息处理返回。

需要明确的一点是,内源消息通常只用于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进行格式化。

Comparision

总的来说,这是一个多线程的事件驱动模型框架,多线程的好处在于充分利用多核,难点在于线程间的同步,得益于业务逻辑的分离以及go语言本身对多核编译的良好支持,这个难点在这里并不存在。同时goroutine可以放心地阻塞,这为rpc之类的实现提供了可能。对比其他框架,多线程的没有我们简单,当然这更多的是针对框架的实现而言,因为通常多线程的框架对上层都表现为单线程。多进程的没有我们高效,进程间通信不会比线程间通信快。单线程的没有我们强大。使用动态语言的没有我们的速度。

当然,我们不及多进程灵活(跨机步骤,单个进程崩溃不影响全局),没有单线程来得简单,没有动态语言的热更新。

根据经验,本框架适用于中小型的MMO游戏(限时战略及动作类游戏慎用)。

TODO

提供Service的外源消息支持,有可能统一Agent与Service的处理;抽离强加的游戏逻辑;与protobuf解耦。

Build

安装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

Test

现在server及client目录下分别生成了server及client可执行文件,在不同终端分别执行:

./server
./client

client可执行的指令有请求角色列表(rolelist),创建角色(createrole),登录(login),登出(logout),echo以及chat,具体用法可参考cmd.go。

Usage

在正式的场合应该将server进程变为精灵进程,由于go语言本身不支持daemonize,所以需要daemonize之类的工具的支持。作为例子,安装daemonize之后可使用control.sh脚本启动及关闭服务端进程。

默认的服务端配置在config.json,正式场合切记修改。