GoLang EASY 游戏框架 之 应用项目+教程 02
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。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!