【Skynet】Skynet项目-球球作战实例

一、拓扑结构

请添加图片描述
如图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 then
		print("id:"..umsg.id)
		print("pw:"..umsg.pw)
	else
		print("error")
	end
end

skynet.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 then
		playerdata.skin = res[1].skin
	else
		playerdata.skin = 1
	end			
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 then
		print("error:" .. res.err)
	else
		print("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].data
	print("data len:" .. string.len(data))
	local udata = pb.decode("playerdata.BaseInfo", data)
	if not udata then
		print("error")
		return false
	end
	--输出
	local playerdata = {}
	playerdata.baseinfo = udata
	print("coin:" .. playerdata.baseinfo.coin)
	print("name:" .. playerdata.baseinfo.name)
	print("time:" .. playerdata.baseinfo.last_login_time)
	print("skin:" .. playerdata.baseinfo.skin)
end

skynet.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",
	},
	--agentmgr
	agentmgr = { node = "node1" },
	--scene
	scene = {
		node1 = {1001, 1002},
		--node2 = {1003},
	},
	--节点1
	node1 = {
		gateway = {
			[1] = {port=8001},
			[2] = {port=8002},
		},
		login = {
			[1] = {},
			[2] = {},
		},
	},
	--节点2
	node2 = {
		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]
	--nodemgr
	local nodemgr = skynet.newservice("nodemgr", "nodemgr", 0)
	skynet.name("nodemgr", nodemgr)
	--集群
	cluster.reload(runconfig.cluster)
	cluster.open(mynode)
	--gate
	for i,v in pairs(nodecfg.gateway or {}) do
		local srv = skynet.newservice("gateway", "gateway", i)
		skynet.name("gateway"..i, srv)
	end
	--login
	for i,v in pairs(nodecfg.login or {}) do
		local srv = skynet.newservice("login", "login", i)
		skynet.name("login"..i, srv)
	end
	--agentmgr
	local anode = runconfig.agentmgr.node
	if mynode == anode then
		local srv = skynet.newservice("agentmgr", "agentmgr", 0)
		skynet.name("agentmgr", srv)
	else
		local proxy = cluster.proxy(anode, "agentmgr")
		skynet.name("agentmgr", proxy)
	end
	--scene
	for _, sid in pairs(runconfig.scene[mynode] or {}) do --sid->sceneid
		local srv = skynet.newservice("scene", "scene", sid)
		skynet.name("scene"..sid, srv)
	end
	--admin
	local 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
  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
### 回答1: 《Skynet框架教程-非常详细.pdf》是一本关于Skynet框架的教程,它提供了关于Skynet框架的全面和详细的介绍。 该教程首先介绍了Skynet框架的背景和起源,以及它的特点和优势。Skynet是一个高度可扩展的分布式服务框架,具有高性能和低延迟的特点。它采用了基于actor模型的并发编程模型,并提供了丰富的工具和库来帮助开发者构建和管理分布式应用。 教程的下一部分介绍了Skynet框架的基本概念和架构。它解释了Skynet节点、服务和消息传递等核心概念,并提供了示例代码来说明这些概念的使用方法。读者可以通过这一部分了解Skynet框架的基本原理和用法。 接下来的章节详细介绍了Skynet框架的各个组件和功能。其中包括服务注册与发现、负载均衡、容错机制、监控和调试等方面。每个组件和功能都有详细的说明和示例代码,读者可以通过实践来学习和理解。 教程的最后一部分是一些实际应用案例的介绍。这些案例涵盖了不同领域和规模的应用,包括游戏服务器、在线教育平台、电子商务网站等。每个案例都详细介绍了Skynet框架在该应用中的具体应用和实现过程,对于读者来说是一个很好的参考和借鉴资料。 总之,《Skynet框架教程-非常详细.pdf》是一本很好的Skynet框架学习资料,它提供了全面而详细的内容,涵盖了Skynet框架的各个方面。无论是初学者还是有一定经验的开发者,都可以通过这本教程来学习和掌握Skynet框架的使用和开发技巧。 ### 回答2: 《Skynet框架教程-非常详细.pdf》是一本关于Skynet框架的详细教程文档,其中包含了关于Skynet框架的基本概念、核心功能、使用方法等内容。 Skynet框架是一个高性能、轻量级的分布式服务框架,适用于开发网络游戏、实时通信等高并发场景。该框架基于事件驱动模型,通过异步消息传递和多线程技术实现高并发处理能力。 在《Skynet框架教程-非常详细.pdf》中,首先介绍了Skynet框架的背景与发展历程,帮助读者了解该框架的起源和应用领域。接着详细介绍了Skynet框架的核心架构,包括节点管理、服务管理、消息传递等模块的设计与实现原理。 教程还详细介绍了Skynet框架的安装和配置,包括环境准备、编译与安装等步骤。然后,通过一系列实际案例演示了如何使用Skynet框架进行开发,包括创建节点、注册服务、消息处理、资源管理等方面的内容。 此外,教程还介绍了Skynet框架的调试和优化技巧,包括日志查看、性能测试工具的使用等方面的内容。最后,给出了一些常见问题的解答,帮助读者更好地解决在使用Skynet框架过程中遇到的困惑。 总的来说,《Skynet框架教程-非常详细.pdf》是一本适合初学者和有一定经验的开发人员的教程,通过阅读该教程可以深入了解Skynet框架的原理和使用方法,从而更好地应用于实际项目中。 ### 回答3: 《Skynet框架教程-非常详细.pdf》是一本非常详细的Skynet框架教程。Skynet框架是一个高性能、高可靠性的分布式服务框架,用于构建可扩展的游戏服务器、物联网平台等分布式应用。 这本教程从Skynet框架的基础知识讲起,介绍了Skynet框架的特点、架构和设计理念。然后详细介绍了Skynet框架的安装和配置,包括环境准备、编译安装和启动流程等。 接下来,教程深入讲解了Skynet框架的核心概念和基本用法,包括服务、消息、协议等。这些内容帮助读者理解Skynet框架的工作原理,并能够快速上手开发。 教程还介绍了Skynet框架的高级特性和扩展功能,如集群部署、负载均衡、动态扩容等。这些内容使读者能够在实际应用中解决复杂的问题,并提升系统的性能和可扩展性。 此外,教程还提供了大量的示例和实战案例,帮助读者将理论应用到实际项目中。通过这些实例,读者可以学习到如何使用Skynet框架构建真实的分布式应用,同时也能够了解到Skynet框架的一些最佳实践和常见错误。 综上所述,《Skynet框架教程-非常详细.pdf》是一本非常全面的Skynet框架教程,适合初学者和有一定经验的开发者阅读,对于了解Skynet框架的原理和使用方法都有很大帮助。无论是想要学习Skynet框架的基础知识,还是进一步提升Skynet框架的应用技巧,这本教程都是一个不错的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值