【Skynet】Skynet项目-球球作战实例
Skynet项目-球球作战实例
- 一、拓扑结构
- 1.1 各服务功能
- 1.2 消息流程
- 1.3 设计要点
- 二、目录结构
- 2.1 项目根目录
- 2.2 service目录
- 2.3 lualib目录
- 2.4 luaclib_src目录
- 2.5 luaclib目录
- 2.6 proto目录
- 2.7 storage目录
- 2.8 tools目录
- 2.9 etc目录
- 三、启动流程
- 四、项目地址
一、拓扑结构
如图3-3,其中圆圈代表服务,圈内文字代表服务类型和编号,比如:”gateway1“代表”gateway“类型的1号服务。
该拓扑结构支持横向拓展(增加物理机)
1.1 各服务功能
服务 | 说明 |
---|---|
gateway | 即网关,用于处理客户端连接的服务。客户端会知道所有网关地址(选服列表)。选择连接某个网关(gateway),如果玩家尚未登录,网关会把消息转发给节点内某个login服务,以处理账号校验等操作;如果登陆成功,则会把消息转发给客户端对应的agent服务。一个节点可以开启多个网关以分摊性能 |
login | 即登录服务,用于处理登录逻辑的服务,比如账号校验。一个节点可以开启多个登录服务以分摊性能 |
agent | 即代理服务,每个客户端会对应一个代理服务(agent),负责对应角色的数据加载、数据存储、单服逻辑的处理(比如强化装备、成就等)。出于性能考虑,agent必须与它对应的客户端连接(即客户端连接的gateway)处在同一个节点 |
agentmgr | 即管理代理(agent)的服务,它会记录每个agent所在的节点,避免不同的客户端登录同一账号 |
nodemgr | 即节点管理,每隔节点会开启一个nodemgr服务,用于管理该节点(新建agent服务)和监控性能 |
scene | 即场景服务,处理战斗逻辑的服务,每一局游戏由一个场景服务器负责 |
1.2 消息流程
登录过程:
①:客户端连接某个gateway,然后发送登录协议
②:gateway将登陆协议转发给login
③:如果login校验通过,转发给agentmgr校验
④:agentmgr发现玩家已在线,通知另一客户端agent踢下线
⑤:另一客户端的agent通知gateway和客户端断开socket连接
⑥:agentmgr通知nodemgr新建一个agent服务
⑦:新建一个agent服务
⑧:新建的agent通知login,login再通知gateway告诉玩家登陆成功
游戏过程:
⑨:客户端发消息给gateway,gateway直接转发给对应的agent
1.3 设计要点
1.gateway
这套服务端系统采用传统C++服务器架构方案。gateway只做消息转发,启用gateway服务有以下的好处:
- 隔离客户端和服务端系统。如果要更改客户端协议(比如改用json协议或者protobuf),仅需更改gateway,不会对系统内部产生影响。
- 预留了断线重连功能,如果客户端断线,仅影响gateway,不影响agent
然而引入gateway意味着客户端消息需经过一层转发,会带来一定的延迟。将同一个客户端连接的gateway、login、agent置于同一节点,有助于减少延迟。
2.agent和scene的关系
agent可以和任意一个scene通信,但跨节点通信的开销比较大。一个节点可以支撑数千名玩家,足以支撑各种段位的匹配,玩家应尽可能地进入同一节点的战斗场景服务器(scene)。
3.agentmgr
agentmgr仅记录agent的状态、处理玩家登录、登出功能,所有对它的访问都以玩家id为索引。它是个单点,但很容易拓展成分布式。
二、目录结构
2.1 项目根目录
- etc:存放服务配置的文件夹
- example:测试用例
- luaclib:存放一些C模块(.so文件)
- luaclib_src:存放C模块的源代码(.c、.h)
- lualib:存放Lua模块
- service:存放各服务的Lua代码
- skynet:skynet框架,我们不会改动skynet的任何内容。如果后续skynet有更新,直接替换该文件夹即可
- proto:存放通信协议文件(.proto)
- storage:存放数据库协议文件(.proto)
- tools:存放工具文件
- start.sh:启动服务的脚本(本质就是./skynet [配置])
2.2 service目录
- admin:类似skynet的debug_console编写的一个”管理控制台“服务,服务器管理者可以通过telnet登入控制台,然后输入指令。如”stop“妥善关闭服务器,把玩家全部踢下线。如”mail“给在线玩家发邮件等。
- agent:agent服务
- agentmgr:agentmgr服务
- gateway:gateway服务
- login:login服务
- nodemgr:nodemgr服务
- scene:scene服务
- main.lua:main服务是节点启动后第一个被加载的服务,用于启动其他各个服务
2.3 lualib目录
- protobuf.lua:protobuf用到的Lua模块代码
- service.lua:是agent、agentmgr、gateway、login、nodemgr、scene服务的父类。这些服务都继承自service。service里封装了一些skynet的API和一些自有的属性,方便子服务的创建、通信、辨别不同服务类型等,减少代码编写。
- register.lua:提供一个注册类,用于把模块方法注册进里面,然后通过register模块API取出模块里函数,实现代码简洁,不用再多处维护不同的函数列表
- extension目录:该目录下存放扩展lua标准库方法的文件。例如./extension/table.lua文件,存放扩展标准库table的函数:如PrintTable()打印表的内容、update()更新表内容,然后把自己实现的方法注册到标准库table表里即可,即table.PrintTable = PrintTable、table.update = update。
2.4 luaclib_src目录
- lua-cjson:cjson的源代码,用于json和lua之间的转换。
- pbc:pbc的源代码,用于protobuf。
lua-cjson下载与编译:
cd luaclib_src #进入luaclib_src目录
git clone https://github.com/mpx/lua-cjson #下载第三方库lua-cjson的源码
cd lua-cjson #进入lua-cjson源码目录
make #编译,成功后会多出名为cjson.so的文件
cp cjson.so ../../luaclib #将cjson.so复制到存放C模块的luaclib目录中
pbc使用protoc 2.5.0参与编译,下载安装protoc 2.5.0
cd ~
wget https://github.com/protocolbuffers/protobuf/archive/refs/tags/v2.5.0.zip
unzip v2.5.0.zip
cd protobuf-2.5.0/
./autogen.sh
./configure
make
make install
pbc下载与编译:
cd luaclib_src #进入项目工程luaclib_src目录
git clone https://github.com/cloudwu/pbc #下载第三方库pbc的源码
cd pbc #进入pbc源码目录
make #编译pbc
cd pbc/binding/lua53 #进入pbc的binding目录,它包含skynet'可用的C库源码
make #工具编译。成功后会在同目录下生成 库文件protobuf.so 和 Lua模块protobuf.lua
cp protobuf.so ../../../../luaclib/ #将protobuf.so复制到存放C模块的luaclib目录中
cp protobuf.lua ../../../../lualib/ #将protobuf.lua复制到存放Lua模块的lualib目录中
注意:编译pbc、pbc工具和cjson时,用的Lua版本和Skynet/3rd目录下的Lua版本要一致。目前新版的skynet支持了lua5.4.2,因此保证lua -v
版本要和skynet支持的lua版本一致。否则会导致protobuf.so和cjson.so使用时会报错:
报错内容:如上图所示,
error loading module ‘protobuf.c’ from file ‘./luaclib/protobuf.so’
原因:编译时未 link 5.4 的 .h 的缘故。lua_newuserdata 在 5.4 是以宏的方式提供。
2.5 luaclib目录
- cjson:cjson用到的C模块动态库
- protobuf.so:protobuf用到的C模块动态库
2.6 proto目录
- login.proto:描述文件。使用protobuf的第一步是编写描述文件。
- login.pb:根据proto描述文件生成的二进制文件。
protobuf使用过程步骤:
- 编写proto文件,如:
package login;message Login {required int32 id = 1;required string pw = 2;optional int32 result = 3;
}
- 编译proto文件:
cd proto #进入proto目录
protoc --descriptor_set_out login.pb login.proto
- 使用pbc模块API编码解码:
local skynet = require "skynet"
local pb = require "protobuf"--protobuf编码解码
function test()pb.register_file("../proto/login.pb") --注册编译文件(.pb文件)--编码local msg = {id = 101,pw = "123456",}local buff = pb.encode("login.Login", msg) --参数1:协议名,由proto描述文件的包名和协议名组成。参数2:协议对象。返回值:二进制数据print("len:" .. string.len(buff))--解码local umsg = pb.decode("login.Login", buff)--参数1:协议名,由proto描述文件的包名和协议名组成。参数2:二进制数据。返回值:失败返回nil,成功返回协议对象。if umsg thenprint("id:"..umsg.id)print("pw:"..umsg.pw)elseprint("error")end
endskynet.start(function ()test()
end)
2.7 storage目录
- playerdata.proto:描述文件。
- playerdata.pb:根据proto描述文件生成的二进制文件
传统数据库:难以应付版本迭代
一个表有playerid(作为索引)、name、coin、level、last_login_time等几个栏位。
如果有策划需要增加skin栏位, 可能拓展数据库导致十几小时停服,而且,策划要求玩家上线后赠送id为1的皮肤,代码会如下:
function test()local playerdata = {}local res = db:query("select * from player where playerid = 105")if res[1].skin thenplayerdata.skin = res[1].skinelseplayerdata.skin = 1end
end
经历多次版本迭代后,这些“判断历史数据的代码”会变得冗长而混乱,后期接手项目的同事,他们没有经历过前期版本迭代,很难理解这些代码的用意,很难做维护。
key-value表结构:
将玩家数据序列化,数据库仅存储序列化后的二进制数据。它类似于“Key-Value”(键值对)数据库,以玩家id为键,以序列化数据为值,其中的playerid代表玩家id,用作索引,data存储序列化后的数据。
使用Key-Value数据表,可以构造稳定的数据库结构,还能兼顾NoSQL,让服务端系统拥有无缝切换MySQL和MongoDB这两种数据库的潜力。
数据存储过程实例:
playerdata.proto
package playerdata;message BaseInfo {required int32 playerid = 1;required int32 coin = 2;required string name = 3;required int32 level = 4;required int32 last_login_time = 5;required int32 skin = 6 [default = 10];
}message Bag {}
message Task {}
message friend {}
message mail {}
message achieve {}
message title {}
storage_test.lua
local skynet = require "skynet"
local mysql = require "skynet.db.mysql"
local pb = require "protobuf"local db = nil--创角
function test()--注册编译文件(.pb文件)pb.register_file("../storage/playerdata.pb")--创角(按照功能模块划分的玩家数据)--playerdata表里每个key对应一个表,如baseinfo对应baseinfo表、bag对应bag表--拆分数据表的原因是查询表需要加载,数据越大加载的时间越长,因此做数据表拆分local playerdata = {baseinfo = {playerid = 109,coin = 97,name = "Tiny",level = 3,last_login_time = os.time(),}, --基本信息bag = {}, --背包task = {}, --任务friend = {}, --朋友mail = {}, --邮件achieve = {}, --成就title = {}, --称号}--序列化local data = pb.encode("playerdata.BaseInfo", playerdata.baseinfo)print("data len:" .. string.len(data))--存入数据库(这里仅示例存入baseinfo表,其他如bag、mail等表的存储略)local sql = string.format("insert into baseinfo(playerid, data) values (%d, %s)", 109, mysql.quote_sql_str(data)) --由于变量data是二进制数据,因此,拼接成SQL语句时,需用mysql.quote_sql_str做转换。local res = db:query(sql)--查看存储结果if res.err thenprint("error:" .. res.err)elseprint("ok")end
end--读取角色数据
function test2()pb.register_file("../storage/playerdata.pb")--读取数据库(忽略读取失败的情况)local sql = string.format("select * from baseinfo where playerid = 109")local res = db:query(sql)--反序列化local data = res[1].dataprint("data len:" .. string.len(data))local udata = pb.decode("playerdata.BaseInfo", data)if not udata thenprint("error")return falseend--输出local playerdata = {}playerdata.baseinfo = udataprint("coin:" .. playerdata.baseinfo.coin)print("name:" .. playerdata.baseinfo.name)print("time:" .. playerdata.baseinfo.last_login_time)print("skin:" .. playerdata.baseinfo.skin)
endskynet.start(function ()--连接数据库db = mysql.connect({host="192.168.184.130",port=3306,database="message_board",user="root",password="123456",max_packet_size=1024*1024,on_connect=nil})test()test2()
end)
2.8 tools目录
- genProtold.py:python文件,给run_on_chang.sh使用。作用是生成message_define.bytes文件,这个文件是用于和客户端的通信协议id找协议名对应关系表
- run_on_change.sh:当写好新的和客户端通信的.proto文件后,使用该shell脚本,会把./proto目录下所有的.proto文件集中生成为一个.pb文件(all.bytes),和生成一个协议id和协议名对照的文件message_define.bytes,方便pbc模块的api解析协议内容。
run_on_chang.sh:
#!/bin/sh
cd ../proto #进入./proto目录
protoc -o../service/proto/all.bytes *.proto #把./proto目录下所有.proto文件生成为一个.pb文件(all.bytes)
cd .. #回到项目根目录
python tools/genProtoId.py --output=./service/proto/message_define.bytes ./proto/*.proto #执行python文件genProtoId.py,生成协议id和协议名对应的文件message_define.bytes
2.9 etc目录
- runconfig.lua:服务配置,提供服务所需的一些参数
- config.node1:节点1的启动配置,供./skynet用的配置
- config.node2:节点2的启动配置,供./skynet用的配置
三、启动流程
sh start.sh [num]
start.sh
./skynet/skynet ./etc/config.node$1
例如:在项目根目录,输入命令sh start.sh 1
。意思是:载入./etc/config.node1
的配置给./skynet/skynet
程序使用。
config.node1
--必须配置
thread = 8 --启用多少个工作线程
cpath = "./skynet/cservice/?.so" --用c编写的服务模块位置
bootstrap = "snlua bootstrap" --启动的第一个服务--bootstrap配置
start = "main" --主服务入口
harbor = 0 --不使用主从节点模式--lua配置项
lualoader = "./skynet/lualib/loader.lua"
luaservice = "./service/?.lua;" .. "./service/?/init.lua;" .. "./skynet/service/?.lua;"
lua_path = "./etc/?.lua;" .. "./lualib/?.lua;" .. "./skynet/lualib/?.lua;" .. "./skynet/lualib/?/init.lua"
lua_cpath = "./luaclib/?.so;" .. "./skynet/luaclib/?.so"--后台模式
--daemon = "./skynet.pid"
--logger = "./userlog"--节点
node = "node1"
config.node1配置解析:
- /skynet/lualib/loader.lua这个loader加载Lua服务时,会到
"luaservice= "
配置配好的地址去查找。 - 在lua文件中require时,会到
"lua_path ="
和"lua_cpath ="
配好的地址查找对应的Lua模块和C模块
- 然后skynet会启动bootstrap等一系列skynet启动的服务。最后启动如上配置(
./etc/config.node1
)start指定的的main服务(./service/main.lua)。这个main.lua是我们自己写的服务程序入口。
runconfig.lua
return {--集群cluster = {node1 = "127.0.0.1:7771",node2 = "127.0.0.1:7772",},--agentmgragentmgr = { node = "node1" },--scenescene = {node1 = {1001, 1002},--node2 = {1003},},--节点1node1 = {gateway = {[1] = {port=8001},[2] = {port=8002},},login = {[1] = {},[2] = {},},},--节点2node2 = {gateway = {[1] = {port=8011},[2] = {port=8022},},login = {[1] = {},[2] = {},},},
}
main.lua
local skynet = require "skynet"
local skynet_manager = require "skynet.manager"
local cluster = require "skynet.cluster"
local runconfig = require "runconfig"skynet.start(function ()--初始化local mynode = skynet.getenv("node")local nodecfg = runconfig[mynode]--nodemgrlocal nodemgr = skynet.newservice("nodemgr", "nodemgr", 0)skynet.name("nodemgr", nodemgr)--集群cluster.reload(runconfig.cluster)cluster.open(mynode)--gatefor i,v in pairs(nodecfg.gateway or {}) dolocal srv = skynet.newservice("gateway", "gateway", i)skynet.name("gateway"..i, srv)end--loginfor i,v in pairs(nodecfg.login or {}) dolocal srv = skynet.newservice("login", "login", i)skynet.name("login"..i, srv)end--agentmgrlocal anode = runconfig.agentmgr.nodeif mynode == anode thenlocal srv = skynet.newservice("agentmgr", "agentmgr", 0)skynet.name("agentmgr", srv)elselocal proxy = cluster.proxy(anode, "agentmgr")skynet.name("agentmgr", proxy)end--scenefor _, sid in pairs(runconfig.scene[mynode] or {}) do --sid->sceneidlocal srv = skynet.newservice("scene", "scene", sid)skynet.name("scene"..sid, srv)end--adminlocal admin = skynet.newservice("admin", "admin", 0)skynet.name("admin", admin)--退出自身skynet.exit()
end)
如上代码,main服务的流程:
- reload集群,打开自己的集群接口(cluster.open(mynode))
- 创建一个nodemgr服务
- 根据runconfig配置信息,创建多个gate服务
- 根据runconfig配置信息,创建多个login服务
- 根据runconfig配置信息,创建一个agentmgr服务
- 根据runconfig配置信息,创建多个scene服务
- 创建一个admin服务
- 退出自身,即关闭main服务
最后,skynet进程除了有bootstrap等skynet启动的服务外,还有我们写的服务:多个gate服务、多个login服务、一个nodemgr服务、一个agentmgr服务、多个scene服务。客户端登陆成功后,每个客户端还会对应生成一个agent服务。
注意:
skynet创建服务(newservice)其实调用了底层C代码创建了一个服务类对象(struct),并且这个服务类对象拥有一个Lua虚拟机(luaState,其实就是一个C-union结构体)。
因此,nodemgr服务、gate服务、login服务、agentmgr服务、scene服务、agent服务等,每个服务都有自己的Lua虚拟机(luaState,其实就是一个C-union结构体),也就是说,每个服务都有自己的全局表,Upvalue表、常量表等。因此每个服务之间的Lua代码是独立的,生成的对象也是独立的。
举例:
gateway.lua
local s = require "service"s.hehe = 1
agent.lua
local s = require "service"
如上代码,两个文件在调用require后,"service"文件的内容被装在了全局表的一个地址里,如_G[0x7f29fe7d7e00]
,返回值s是全局表的那个对应的地址(即0x7f29fe7d7e00)。
因为gateway和agent是不同的服务对象(即不同的struct),因此,他们有自己的Lua虚拟机,这两个文件内的全局表是各自维护的,互相不能访问。所以agent里的s索引不到gateway的”hehe“。
就算开启了两个gateway服务,这两个服务也是不同的服务对象(即不同的struct),因此,他们也有各自的Lua虚拟机,各自维护自己的全局表、Upvalue表、常量表等(注:全局表、Upvalue表、常量表都是Lua的概念,可以自行去看Lua书籍熟悉相关概念)。
四、项目地址
github:
https://github.com/hhhhhhh12123/BoxGameServer
gitee:
https://gitee.com/smallppppig/boxgame
【Skynet】Skynet项目-球球作战实例相关推荐
- python球球大作战简易版详解
在玩很多游戏的时候,我们可以发现游戏里面的世界很大,但是整个窗口却最大不过我们屏幕大小,为了观察到整个世界,我们的视角窗口就会随着里面人物的移动不断的移动. 比如说游戏球球大作战,在玩这款游戏的时候我 ...
- 【从零开始学Skynet】实战篇《球球大作战》(一):功能设计
为了能把之前在基础篇中学习到的Skynet的各种知识结合起来,所以在实战篇中,我们准备开发一个完整的游戏案例<球球大作战>,介绍分布式游戏服务端的实现方法. 1.功能需求 <球球大作 ...
- c语言编程球球大作战,C/C++项目源码——球球大作战
C/C++项目源码--球球大作战 这是一个球球大作战的小程序,能够运行,需要下载一个easyx库 初始产生一个小球,可以慢慢吃零食长大 游戏没有写完整,不能吃别的玩家(单机初始化产生的玩家) 有兴趣可 ...
- C/C++--球球大作战项目(简单版)
C/C++--球球大作战项目(简单版) 在bilibili上扒了个C++的小游戏项目的视频,自己看了几遍,跟着老师学习了这个项目. 项目名称:C/C++--球球大作战项目(简单版) 项目简介:模拟球球 ...
- 全球首届“AI球球大作战:Go-Bigger多智能体决策智能挑战赛”开启
<球球大作战>是一款风靡全球的休闲电子竞技游戏,以大球吃小球为目标,简单有趣却又斗智斗勇. 你不知道的是,AI世界也拥有了自己的<球球大作战>. 前不久,OpenDILab开源 ...
- 《球球大作战》游戏优化之路(上)
自从2015年<球球大作战>发布以来,现已拥有五亿多的玩家.如此庞大的玩家群体,对游戏的画面,性能要求是非常高的.在Unite Shanghai 2019大会中,<球球大作战> ...
- Java小程序之球球大作战(基于Java线程实现)
Java小程序之球球大作战(基于Java线程实现) 一.游戏基本功能: 1.自己的小球可以随着鼠标的移动而改变坐标: 2.敌方小球不断的在界面中移动 3.当检测到敌方小球相互碰撞时,小球会弹开 4.当 ...
- java球球大作战小游戏代码_windows游戏编程:球球大作战(吃鸡版)源码
#include "stdafx.h"是win32程序系统生成的 创建项目时选择win32程序项目 除了下面代码外,无其他改动 #include "stdafx.h&qu ...
- 基于C++的简易版《球球大作战》游戏设计
全套资料下载地址:https://download.csdn.net/download/sheziqiong/85602628 全套资料下载地址:https://download.csdn.net/d ...
最新文章
- 转: telnet命令学习
- [html] a标签的默认事件禁用后,如何实现跳转?
- WebSSH2安装过程可实现WEB可视化管理SSH工具
- Hbase简介及常用命令相关知识总结
- bzoj 3190 赛车 半平面交
- html dom事件不包括,HTML DOM - 事件
- percona-toolkit源码编译安装
- this.$router.push相关的vue-router的导航方法
- [华为19实习面试]语言能力优秀的我,是怎么拿下勇敢星实习offer的?华为硬件类面试经历经验分享(大三已拿offer)
- 关于JAVA输入输出流造成的Runtime线程阻塞问题【新人笔记】
- 当代中国社会划分为十大阶层
- oracle inst 自动重启,oracle rac 节点自动重启
- 泰森多面体Voronoi 3D-V5.0 功能介绍
- 计算机二级c语言改错,2010年计算机二级C语言上机改错题答题总结
- 维乐VELO副总陈安荣:宽容对待美学,会让生活更幸福
- 图谱实战 | ​鲍捷:知识图谱技术在金融领域的分析和应用
- 牛逼!Unix之父密码耗时4天终于破解了
- 杰理之带内杂散超标【篇】
- 怎么查看自己的外网IP?用自己的电脑发布网站,连接公网
- 直播需要多大的网络带宽?