GoLang EASY 游戏框架 之 应用项目+教程 02

2023-12-13 05:54:50

1 Program Examples Overview

用easy?实现的?服务端?和客户端样例。

simple 项目构建了比较完备的目录结构,可以作为空项目拿到项目中直接应用。

传送门:https://github.com/slclub/easy

位置:

  • ????????examples/simple
  • ? ? ? ? examples/simple_client

????????

2 Simple

比较简单的源码样例;

这是一个简单的服务端,你可以直接拿它做项目,扩展开发即可。

  • 最基本的easy框架使用
  • 简单的游戏架构,不包含数据层;
  • simple代码以极简化为主,项目扩展要 结构化一些
  • 目录多是空的

2.1 simple.Server

项目名:simple

目录结构:

-conf // 配置
--controller 控制器也就是解读消息的入口
    --callback 放置一些基本的回调函数,如链接创建,服务平滑关闭等
    --login 登陆模块
    --player 用户玩家
    --store 商铺
    --world 大世界相关
--initialize 初始化,工程启动执行一次;与运行时无关
--lservers 接入easy监听服务 l 是 ```listen``` 当让也可以接入其他的监听服务
--message 消息定义
--models 数据模型,尽量只有数据结构的定义,和基本验证
--services 游戏逻辑存放区域,主要的逻辑都可以放在这里
--vendors 您项目的一些必要基础功能性的包,或者接入第三方包(且这个包需要配置等);// 并非是替代 go mod
    注意:
    go mod 中也有一个verdor 且会产生vendor文件夹
    我们这里的vendors 仅仅是common 通用,基础,标准等的意思
    这里的包之间互相依赖也少,或者说机会是无
    比较大(功能性)的包引入后,总需要配置一些东西,甚至和自己的配置参数相关,那么放在这里改造一下(符合工程写法,结构要求等)就比较合适了

运行命令:go build && ./simple

2.2 simple.Client

项目名:simple_client

运行命令:go build && ./simple_client

测试simple服务端对应的客户端样例工程

3 项目代码

3.1 main

在main中Start() 中easy.Serv 可以传入多个服务组件。lservers.Server1()监听的是websocket,lservers.Server2()监听的是TCP。一个应用服务程序,可以容易的监听多个端口服务,且任意多个,多个同一类型的协议也是支持的。

我们在easy.Serv统一阻断主goroutine,其他可以直接使用内置的go 方法直接调用,没有任何高级花哨的调用方式。


import (
	"github.com/slclub/easy"
	"simple/initialize"
	"simple/lservers"
)

func main() {
	initialize.Init()
	Start()
}

func Start() {
	easy.Serv(
		lservers.Server1(), // websocket 监听服务 可以有多个
		lservers.Server2(), // tcp 服务
	)
}

3.2 lservers(listening service)

监听端口的服务,代码也很少,这里样例代码就没有按服务分文件。直接上源码:

func Server1() servers.ListenServer {
	return server1
}

func Server2() servers.ListenServer {
	return server2
}

func InitListenServer() {
	server1 = servers.NewWSServer()

	server1.Init(option.OptionWith(&agent.Gate{
		Addr:            ":18080",
		Protocol:        typehandle.ENCRIPT_DATA_JSON,
		PendingWriteNum: 2000,
		LittleEndian:    true,
		MaxConnNum:      2000,
	}).Default(option.DEFAULT_IGNORE_ZERO))

	server2 = servers.NewTCPServer()
	server2.Init(option.OptionWith(&agent.Gate{
		Addr:            ":18081",
		Protocol:        typehandle.ENCRIPT_DATA_JSON,
		PendingWriteNum: 2000,
		LittleEndian:    true,
		MaxConnNum:      2000,
	}).Default(option.DEFAULT_IGNORE_ZERO))
}
  • servers 是easy的监听服务基础包package
  • server1 :=servers.NewWSServer() 是new一个websocket 服务
  • server1.Init()初始化
  • option.OptionWith 是我们的一个开放配置选择包,为了易用和方便,配置方式多样,默认值等而开发。

参数:

? ? ? ? Addr:监听地址

? ? ? ? Protocol:选用编码组件(快捷换编码的方式,换自定义编码组件在后续章节会说明)

? ? ? ? PendingWriteNum:排队消息长度

? ? ? ? LittleEndian:true=小端,false=大端

? ? ? ? MaxConnNum:最大链接数

? ? ? ? option.DEFAULT_IGNORE_ZERO: 如果赋值0值,或者没有给相应字段赋值,则使用默认值。其中Default方法等于use,类似中间件

3.3 controller

handle?

控制器层面,MVC中的C,接收消息的下一步就是它了。用controller/login模块举例



import (
	"github.com/slclub/easy/nets/agent"
	"reflect"
	"simple/vendors/log8q"
)

func HandleLogin(agent1 agent.Agent, arg any) {

	log8q.Log().Info("WS controller.Handle.Login info: ", reflect.TypeOf(arg).Elem().Name())
}

func HandleLoginTcp(agent2 agent.Agent, arg any) {
	log8q.Log().Info("TCP controller.Handle.Login info: ", reflect.TypeOf(arg).Elem().Name())
}

分别是websocket 和Tcp 的login handle,它们做的事情是一样。写handle接收消息就是这样简单。

  • agent.Agent : 理解成连接,可以绑定到你的对象上,业务逻辑所用的handle,你也可以再次封装下,函数签名类似:HandleXXX(player *Player, msg Any) 。
  • arg any :是接收客户端的消息,我们直接粗暴的用reflect,查出它的结构体名字,以做测试验证。

binding

做完handle需要将它与消息以及监听的服务绑定,绑定方法也很简单。直接上代码


import (
	"github.com/slclub/easy/typehandle"
	"simple/controller/login"
	"simple/lservers"
	"simple/message/ID"
	"simple/message/json"
)

func InitBindingRoute() {
	r1 := lservers.Server1().Router()
	r1.Register(ID.LOGIN_REQ, &json.LoginReq{}, typehandle.HandleMessage(login.HandleLogin))
}

func InitBindingRouteServer2() {
	r2 := lservers.Server2().Router()
	r2.Register(ID.LOGIN_REQ, &json.LoginReq{}, typehandle.HandleMessage(login.HandleLoginTcp))
}

直接使用监听服务的Router() 获取路由,使用路由Register()绑定 消息ID,消息体,和消息handle,其中handle是可选的(response返回给客户端的消息是不需要handle的)。
这样哪个监听服务对应哪个handle也是一目了然。

callback

这仅仅是笔者自己起的模块名字,目的是为了给连接Open和Close做监听回调的handle。与业务handle有点不同,少了个消息参数。


import (
	"github.com/slclub/easy/nets/agent"
	"github.com/slclub/easy/servers"
	"simple/lservers"
	"simple/vendors/log8q"
)

func RegisterCallerToLservers() {
	lservers.Server1().Hook().Append(servers.CONST_AGENT_NEW, handleOnConnNew)
	lservers.Server1().Hook().Append(servers.CONST_AGENT_CLOSE, handleOnConnClose)
	lservers.Server1().Hook().Append(servers.CONST_SERVER_CLOSE, handleOnServerClose)

	lservers.Server2().Hook().Append(servers.CONST_AGENT_NEW, handleOnConnNew)
	lservers.Server2().Hook().Append(servers.CONST_AGENT_CLOSE, handleOnConnClose)
}

func handleOnConnNew(ag agent.Agent) {
	log8q.Log().Info("[CONNECTION.NEW] server create an new connection")
}

func handleOnConnClose(ag agent.Agent) {
	log8q.Log().Info("[CONNECTION.CLOSE] server closed an old connection")
}

// the current listening server is closing
// smoothly shutdown the server
func handleOnServerClose(ag agent.Agent) {
	// ag == nil
	// 执行一些 平滑停服务的逻辑
}

同样需要我们用具体的监听服务,去调用钩子对象去Append添加链接的handle。需要注意我们使用的是Append,一个钩子可以添加多个handle。笔者相信为了性能大多数人仅仅会用一个,来完善自己链接在线逻辑。

长链接的监听服务,可以共用此handle。

其他的controller

就是我们按照逻辑流程划分的业务模块,不再赘述了。

4 vendors

笔者把一些引用第三方,需要我们简单封装,或配置等的,或者自己实现单独功能依赖少的包,可以放在vendors 下面作为third package存放之地。这个目录名不要用vendor,它是go默认使用vendor,这以前是一套项目部署方案,有了go mod 它就不香了。后来的很多人甚至没见过它。记得不要混淆vendors 和vendor。

4.1 log8q

笔者使用了自己写的log8q这个日志库,zip虽然性能高且强大,总有些记不住,特别依赖goland等IDE才好用一些。

笔者按照log4j的思想撸了个日志的轮子,可配置级别,支持官方的log接口,也有自己简单的使用Info,Debug,Warn,Error,Fatal等级别,每一个传参数的方式都与fmt.Println类似。可以设置日志保留时间(30 * 86400)=30天。

主打一个简单,易用,组件化,性能够用就好。

5 Message 消息

由于go的import特性等,建议将消息定义为单独一个package,减少loop,import的概率。

在message/register.go 中也是注册消息,是不是熟悉,在controller中我们也有消息注册绑定,其实全放在controller里注册也是可以的,在这里我们主要就是注册哪些没有handle的消息


import (
	"simple/lservers"
	"simple/message/ID"
	"simple/message/json"
)

// 将不需要handle 处理的消息 尽量放在这里注册
// 可以将所有注册消息都放在这里也可以
func Init() {
	InitJson()
	InitProtobuf()
}

func InitJson() {
	r1 := lservers.Server1().Router()
	r1.Register(ID.LOGIN_RES, &json.LoginRes{}, nil)

	r2 := lservers.Server2().Router()
	r2.Register(ID.LOGIN_RES, &json.LoginRes{}, nil)
}

func InitProtobuf() {
	//r2 := lservers.SimpleServ1.Router()
}

6 总结

致于其它的目录结构也没什么内容,介绍目录的结构的tree中说明就足够了。你有自己习惯可以改吗,不是硬性要求。整体看下来代码量很少的吧。

一个完整的单机游戏工程,就构建完毕了。数据库缓存等就用gorm等即可。

致于说单线程开发,在golang中使用 go 和channel 可以轻易的实现,安全稳定的goroutine。并不需要我们过多的给予定式封装,反而难用且性能低下。不同的需求用不同的方式去控制线程就好了。

go是一个高并发语言,开携程像吃饭喝水一样简单,控制好携程数量可能稍有难度。所以我们不要被单线程思想限制住,并发控制好共有资源,代码也不见的就一定复杂,一定多。性能强力,资源占用少,开发方便,稳定性高就好了,服务器也就这点追求了。当然你能让你的代码变现金,那是比较实在的最求出发点不太一样是吧。

后期我会发布Aoi的package,单线程的,且与handle多线程互通有无。位置 EASY.vendors/aoi。

文章来源:https://blog.csdn.net/Kaitiren/article/details/134944554
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。