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使用过程步骤:

  1. 编写proto文件,如:
package login;message Login {required int32 id = 1;required string pw = 2;optional int32 result = 3;
}
  1. 编译proto文件:
cd proto #进入proto目录
protoc --descriptor_set_out login.pb login.proto
  1. 使用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用的配置

三、启动流程

  1. 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模块
  1. 然后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服务的流程

  1. reload集群,打开自己的集群接口(cluster.open(mynode))
  2. 创建一个nodemgr服务
  3. 根据runconfig配置信息,创建多个gate服务
  4. 根据runconfig配置信息,创建多个login服务
  5. 根据runconfig配置信息,创建一个agentmgr服务
  6. 根据runconfig配置信息,创建多个scene服务
  7. 创建一个admin服务
  8. 退出自身,即关闭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项目-球球作战实例相关推荐

  1. python球球大作战简易版详解

    在玩很多游戏的时候,我们可以发现游戏里面的世界很大,但是整个窗口却最大不过我们屏幕大小,为了观察到整个世界,我们的视角窗口就会随着里面人物的移动不断的移动. 比如说游戏球球大作战,在玩这款游戏的时候我 ...

  2. 【从零开始学Skynet】实战篇《球球大作战》(一):功能设计

    为了能把之前在基础篇中学习到的Skynet的各种知识结合起来,所以在实战篇中,我们准备开发一个完整的游戏案例<球球大作战>,介绍分布式游戏服务端的实现方法. 1.功能需求 <球球大作 ...

  3. c语言编程球球大作战,C/C++项目源码——球球大作战

    C/C++项目源码--球球大作战 这是一个球球大作战的小程序,能够运行,需要下载一个easyx库 初始产生一个小球,可以慢慢吃零食长大 游戏没有写完整,不能吃别的玩家(单机初始化产生的玩家) 有兴趣可 ...

  4. C/C++--球球大作战项目(简单版)

    C/C++--球球大作战项目(简单版) 在bilibili上扒了个C++的小游戏项目的视频,自己看了几遍,跟着老师学习了这个项目. 项目名称:C/C++--球球大作战项目(简单版) 项目简介:模拟球球 ...

  5. 全球首届“AI球球大作战:Go-Bigger多智能体决策智能挑战赛”开启

    <球球大作战>是一款风靡全球的休闲电子竞技游戏,以大球吃小球为目标,简单有趣却又斗智斗勇. 你不知道的是,AI世界也拥有了自己的<球球大作战>. 前不久,OpenDILab开源 ...

  6. 《球球大作战》游戏优化之路(上)

    自从2015年<球球大作战>发布以来,现已拥有五亿多的玩家.如此庞大的玩家群体,对游戏的画面,性能要求是非常高的.在Unite Shanghai 2019大会中,<球球大作战> ...

  7. Java小程序之球球大作战(基于Java线程实现)

    Java小程序之球球大作战(基于Java线程实现) 一.游戏基本功能: 1.自己的小球可以随着鼠标的移动而改变坐标: 2.敌方小球不断的在界面中移动 3.当检测到敌方小球相互碰撞时,小球会弹开 4.当 ...

  8. java球球大作战小游戏代码_windows游戏编程:球球大作战(吃鸡版)源码

    #include "stdafx.h"是win32程序系统生成的 创建项目时选择win32程序项目 除了下面代码外,无其他改动 #include "stdafx.h&qu ...

  9. 基于C++的简易版《球球大作战》游戏设计

    全套资料下载地址:https://download.csdn.net/download/sheziqiong/85602628 全套资料下载地址:https://download.csdn.net/d ...

最新文章

  1. 转: telnet命令学习
  2. [html] a标签的默认事件禁用后,如何实现跳转?
  3. WebSSH2安装过程可实现WEB可视化管理SSH工具
  4. Hbase简介及常用命令相关知识总结
  5. bzoj 3190 赛车 半平面交
  6. html dom事件不包括,HTML DOM - 事件
  7. percona-toolkit源码编译安装
  8. this.$router.push相关的vue-router的导航方法
  9. [华为19实习面试]语言能力优秀的我,是怎么拿下勇敢星实习offer的?华为硬件类面试经历经验分享(大三已拿offer)
  10. 关于JAVA输入输出流造成的Runtime线程阻塞问题【新人笔记】
  11. 当代中国社会划分为十大阶层
  12. oracle inst 自动重启,oracle rac 节点自动重启
  13. 泰森多面体Voronoi 3D-V5.0 功能介绍
  14. 计算机二级c语言改错,2010年计算机二级C语言上机改错题答题总结
  15. 维乐VELO副总陈安荣:宽容对待美学,会让生活更幸福
  16. 图谱实战 | ​鲍捷:知识图谱技术在金融领域的分析和应用
  17. 牛逼!Unix之父密码耗时4天终于破解了
  18. 杰理之带内杂散超标【篇】
  19. 怎么查看自己的外网IP?用自己的电脑发布网站,连接公网
  20. 直播需要多大的网络带宽?

热门文章

  1. C语言常用运算符与函数汇总
  2. Linux history -n 与 history -r 功能区别
  3. STM32F103标准库开发----CAN总线通信实验----发送和接收
  4. Linux下的DNS服务器的搭建实战
  5. 两步教你用Vue3+TS实现input组件的v-model属性
  6. 【二次开发】基于KLayout的PDK开发记录
  7. 当下互联网环境下物理主机和云主机到底哪个好?
  8. LCE:一个结合了随机森林和XGBoost优势的新的集成方法
  9. ATool软件使用实验(22)
  10. ABP项目备忘(报错及配置)