/skynet_fly

基于云风的skynet,搭建开箱即用的微服务框架,提供优雅的服务热更新

Primary LanguageLuaMIT LicenseMIT

skynet_fly(1)


致力于服务端对skynet的最佳实践

skynet_fly简介

skynet_fly是基于skynet扩展的可以快速开发web,游戏,和需要rpc调用的框架。
使用skynet_fly的好处:
* 支持不停服更新。
* 一键生成skynet的配置文件和skynet_fly的配置文件以及配套shell脚本。
* 对匹配房间类游戏做了gate,ws_gate的基础设施封装以及pb,json协议的支持,开发游戏只需要实现相关业务逻辑。
* 对redis,mysql,timer,log 使用封装。
* 基于skynet cluster封装出简单易用的远程rpc调用。
* 支持服务发现。
* 支持http服务长连接。
* 支持http服务路由,中间件模式。
* 支持jwt鉴权。
* 内置日志分割。
* 支持快进时间。
* 支持orm(数据关系映射)目前适配了(mysql,mongo),数据库可无缝切换。

第三方依赖来源

关于热更新方案

热更新方案二的实现 运行 examples/hot_module2 示例 运行 examples/hot_module3 示例

热更新方案三的实现 运行 examples/hot_module4 示例

官方示例domo

API 文档

快速开始 http服务 (运行examples/webapp)

  1. 编译skynet 参考了涵曦的 skynet_demo
    • git clone https://github.com/huahua132/skynet_fly
    • 安装MakeRequests.sh中依赖的库。
    • 根据系统手动安装。
    • make linux
  2. 构建skynet_config, webapp运维脚本
    • cd examples/webapp/
    • sh ../../binshell/make_server.sh ../../
    • 如果一些顺利的话将会生成script文件夹,文件夹下有:
      • run.sh 运行并配置日志分割
      • stop.sh 停止
      • restart.sh 重启
      • reload.sh 热更某个可热更模块。
      • kill_mod.sh 干掉某个可热更模块(不是强行kill,是通知服务可以退出了)
      • check_reload.sh 检查可热更模块是否有文件或者配置修改,有就更新。
      • fasttime.sh 快进时间。 sh script/fasttime.sh "2023:11:19 11:10:59" 1
      • try_again_reload.sh 当热更失败,可以解决相关错误之后进行重试热更。
    • 还会生成webapp_config.lua,也就是skynet启动用的配置文件。
  3. 运行
    • sh script/run.sh load_mods.lua 0
  4. 访问
    • 浏览器打开 x.x.x.x:80
    • 如果一切顺利的话,网页将会显示内容。
  5. 热更
    • 修改 webapp/lualib/webapp_dispatch.lua 中的任意代码。
    • 之后执行 sh script/check_reload.sh
    • 再次访问网站就更新了。
    • 也可以观察webapp/logs/server.log

http服务已经接入了涵曦的wlua,扩展了路由和中间件模式,完整示例请看运行examples/webapp 源码。 默认webapp运行的是webapp_dispatch.lua,想要切换其他示例,只需要更改load_mods.lua中的dispatch即可。

return {
	web_agent_m = {
		launch_seq = 1,
		launch_num = 6,
		default_arg = {
			protocol = 'http',
			dispatch = 'webapp_dispatch',
		}
	},

	web_master_m = {
		launch_seq = 2,
		launch_num = 1,
		default_arg = {
			protocol = 'http',
			port = 80,
		}
	}
}
  • 处理没有命中路由
--初始化一个纯净版
local app = engine_web:new()
--请求处理
M.dispatch = engine_web.dispatch(app)

--初始化
function M.init()
	--注册全局中间件
	app:use(logger_mid())

	--注册没有找到的路径处理函数
	app:set_no_route(function(c)
		local method = c.req.method
		log.error("no route handle begin 1:",method)

		c:next()
	
		log.error("not route handle end 1:",c.res.status,c.res.resp_header,c.res.body)
	end,
	function(c)
		local method = c.req.method
		log.error("no route handle begin 2:",method)

		c:next()
	
		log.error("not route handle end 2:",c.res.status,c.res.resp_header,c.res.body)
	end)
	
	app:run()
end

--服务退出
function M.exit()

end
  • params路径方式
--初始化一个纯净版
local app = engine_web:new()
--请求处理
M.dispatch = engine_web.dispatch(app)

--初始化
function M.init()
	--注册全局中间件
	app:use(logger_mid())
	
	--/login 路径不会命中
	--/login/123 会命中
	app:get("/login/:player_id/*",function(c)
		local params = c.params
		local player_id = params.player_id

		log.error("params:",params)
		log.error("path:",c.req.path)
		log.error("body:",c.req.body,c.req.body_raw)

		c.res:set_rsp("hello " .. player_id,HTTP_STATUS.OK)
	end)

	app:run()
end

--服务退出
function M.exit()

end
  • query 和 post from
--初始化一个纯净版
local app = engine_web:new()
--请求处理
M.dispatch = engine_web.dispatch(app)

--初始化
function M.init()
	--注册全局中间件
	app:use(logger_mid())

	app:post("/login",function(c)
		local player_id = c.req.query.player_id
		assert(player_id)

		log.error("query:",c.req.query)
		log.error("post from:",c.req.body)

		c.res:set_rsp("hello " .. player_id,HTTP_STATUS.OK)
	end)

	app:run()
end

--服务退出
function M.exit()

end
  • json请求
--初始化一个纯净版
local app = engine_web:new()
--请求处理
M.dispatch = engine_web.dispatch(app)

--初始化
function M.init()
	--注册全局中间件
	app:use(logger_mid())

	app:post("/login",function(c)
		local player_id = c.req.query.player_id
		assert(player_id)

		log.error("query:",c.req.query)
		log.error("json body:",c.req.body)

		local rsp = {
			msg = "hello " .. player_id
		}
		c.res:set_json_rsp(rsp)
	end)

	app:run()
end

--服务退出
function M.exit()

end
  • 自定义中间件
--初始化一个纯净版
local app = engine_web:new()
--请求处理
M.dispatch = engine_web.dispatch(app)

--初始化
function M.init()
	--注册全局中间件
	app:use(logger_mid())

	--自定义中间件
	app:use(function(c)
		log.info("process begin :",c.req.path,c.req.method)

		--执行下一个中间件
		c:next()

		log.info("process end :",c.req.path,c.req.method)
	end)

	app:get("/",function(c)
		log.info("end point process ",c.req.path,c.req.method)
		c.res:set_rsp("hello skynet_fly",HTTP_STATUS.OK)
	end)

	app:run()
end

--服务退出
function M.exit()

end
  • 多路由组
--初始化一个纯净版
local app = engine_web:new()
--请求处理
M.dispatch = engine_web.dispatch(app)

--初始化
function M.init()
	--注册全局中间件
	app:use(logger_mid())
	do
		local v1 = app:group("v1")
		v1:get('/login',function(c)
			log.info("v1 login ")
		end)

		v1:get('/logout',function(c)
			log.info("v1 logout ")
		end)
	end

	do
		local v2 = app:group("v2")
		v2:get('/login',function(c)
			log.info("v2 login ")
		end)

		v2:get('/logout',function(c)
			log.info("v2 logout ")
		end)
	end

	app:run()
end

--服务退出
function M.exit()

end
  • 多路由组中间件
--初始化一个纯净版
local app = engine_web:new()
--请求处理
M.dispatch = engine_web.dispatch(app)

--初始化
function M.init()
	--注册全局中间件
	app:use(logger_mid())
	do
		local v1 = app:group("v1")
		--注册v1路由组的中间件
		v1:use(function(c)
			log.info("process begin v1 mid ",c.req.path,c.req.method)
			c:next()
			log.info("process end v1 mid ",c.req.path,c.req.method)
		end)
		v1:get('/login',function(c)
			log.info("v1 login ")
		end)

		v1:get('/logout',function(c)
			log.info("v1 logout ")
		end)
	end

	do
		local v2 = app:group("v2")
		--注册v2路由组的中间件
		v2:use(function(c)
			log.info("process begin v2 mid ",c.req.path,c.req.method)
			c:next()
			log.info("process end v2 mid ",c.req.path,c.req.method)
		end)
		v2:get('/login',function(c)
			log.info("v2 login ")
		end)

		v2:get('/logout',function(c)
			log.info("v2 logout ")
		end)
	end

	app:run()
end

--服务退出
function M.exit()

end
  • 单文件
--初始化一个纯净版
local app = engine_web:new()
--请求处理
M.dispatch = engine_web.dispatch(app)

--初始化
function M.init()
	--注册全局中间件
	app:use(logger_mid())

	app:static_file('/login/test.webp','./static/test.webp')

	app:run()
end

--服务退出
function M.exit()

end
  • 资源文件夹
--初始化一个纯净版
local app = engine_web:new()
--请求处理
M.dispatch = engine_web.dispatch(app)

--初始化
function M.init()
	--注册全局中间件
	app:use(logger_mid())

	app:static_dir("/login","./static/imgs")

	app:run()
end

--服务退出
function M.exit()

end
  • Benchmark

skynet_fly

30 threads and 1000 connections
Thread Stats   Avg      Stdev     Max   +/- Stdev
Latency    43.07ms    5.32ms 423.34ms   95.72%
Req/Sec   761.97     93.59     1.00k    82.94%
680746 requests in 30.10s, 52.60MB read
Requests/sec:  22619.75
Transfer/sec:      1.75MB

gin

30 threads and 1000 connections
Thread Stats   Avg      Stdev     Max   +/- Stdev
Latency    10.91ms   10.15ms 421.71ms   82.49%
Req/Sec     3.43k     1.09k   30.39k    77.92%
3051430 requests in 30.11s, 325.93MB read
Requests/sec: 101354.20
Transfer/sec:     10.83MB

gin还是快啊

快速开始 游戏服务 (运行examples/digitalbomb)

  • 构建服务

    • cd examples/digitalbomb/
    • sh ../../binshell/make_server.sh ../../
  • 运行服务 sh script/run.sh

基于tcp长连接实现不停服更新 digitalbomb 数字炸弹游戏。 除了登录 login 服不能热更。 hall 大厅服。 alloc 分配服。 table 桌子服都是可行的。 内置了客户端,可以直接看到效果。

  • 业务解耦登录大厅匹配游戏,还有协议都完成了解耦,开发新游戏只需要实现对应的插件接口即可。

  • 切换示例 把digitalbomb游戏由pb协议转换到跑json协议。

    修改配置文件 load_mods.lua

client_m 配置的 net_util由pbnet_util 改为 jsonet_util

room_game_hall_m 配置的 net_util由pbnet_util 改为 jsonet_util

room_game_alloc_m 配置的 net_util由pbnet_util 改为 jsonet_util

room_game_table_m 配置的 net_util由pbnet_util 改为 jsonet_util

执行 sh script/restart.sh

  • 热更新 client_m 表写了测试用例,可以用来验证热更新。 也可以通过script/check_reload.sh的方式,不过你先修改好文件,然后开始执行。

  • 游戏热更新原理 新服替换旧服务的方案。 旧连接跟旧服务通信。 新连接跟新服务通信。 适合用于玩一把游戏就退出的微服务架构。

自己动手,实现一个石头剪刀布游戏

文档链接

如何远程rpc调用

具体使用例子可以参照examples/cluster_client examples/cluster_server_1 examples/cluster_server_2

文档链接

技术交流群

加群,能够更快速的响应解决使用中遇到的问题。 QQ群号:102993581