├── proto ├── roleagent │ ├── mail.sproto │ ├── bag.sproto │ └── role.sproto ├── base.sproto └── login.sproto ├── tools ├── etcd │ ├── start.sh │ └── docker-compose.yml ├── mongodb │ ├── start.sh │ └── docker-compose.yml ├── dist.sh ├── gen_proto.sh ├── gen_schema.sh ├── proto2spb.lua ├── run.lua ├── gen_roleagent_modules.lua └── dist.py ├── app ├── role │ ├── roleagent │ │ ├── modules │ │ │ ├── bag │ │ │ │ ├── request.lua │ │ │ │ └── init.lua │ │ │ ├── mail │ │ │ │ ├── request.lua │ │ │ │ └── init.lua │ │ │ ├── role │ │ │ │ ├── request.lua │ │ │ │ └── init.lua │ │ │ └── init.lua │ │ ├── global.lua │ │ ├── client.lua │ │ ├── rolemgr.lua │ │ └── main.lua │ ├── login │ │ ├── global.lua │ │ ├── main.lua │ │ ├── client.lua │ │ └── login_request.lua │ ├── main.lua │ ├── roleagentmgr.lua │ ├── lualib │ │ └── roleagent_api.lua │ └── watchdog.lua ├── robot │ ├── main.lua │ └── robotagent │ │ └── main.lua └── account │ ├── main.lua │ └── lualib │ └── account_router.lua ├── service ├── logger │ ├── global.lua │ ├── cmd.lua │ ├── main.lua │ ├── checker.lua │ ├── bucket.lua │ └── start.lua ├── mongo_index.lua ├── mongo_conn.lua └── sproto_loader.lua ├── test ├── test_etcd │ ├── test.conf │ └── main.lua └── test_jwt │ ├── test.conf │ └── main.lua ├── lualib ├── log │ ├── log_level.lua │ ├── bucket │ │ ├── init.lua │ │ ├── console.lua │ │ ├── service.lua │ │ └── file.lua │ ├── util.lua │ ├── init.lua │ ├── formatter.lua │ └── logger.lua ├── ptype.lua ├── rolenode_api.lua ├── time.lua ├── httpc.lua ├── preload.lua ├── util │ ├── string.lua │ └── table.lua ├── http_server │ ├── init.lua │ ├── watchdog.lua │ └── agent.lua ├── errcode.lua ├── launcher.lua ├── cmd_api.lua ├── loader.lua ├── config.lua ├── skyext.lua ├── event_channel_api.lua ├── user_db_api.lua ├── orm │ ├── schema_define.lua │ └── schema.lua ├── role_db_api.lua ├── cluster_discovery.lua ├── jwt.lua ├── id_generator.lua ├── mongo_conn.lua ├── snowflake.lua ├── sproto_api.lua ├── dbmgr.lua ├── distributed_lock.lua └── timer.lua ├── .stylua.toml ├── etc ├── monitor.conf.lua ├── account1.conf.lua ├── account2.conf.lua ├── robot.conf.lua ├── role1.conf.lua ├── role2.conf.lua └── common.conf.lua ├── .gitignore ├── schema ├── bag.sproto ├── role.sproto └── mail.sproto ├── .gitmodules ├── Makefile ├── LICENSE ├── lualib-src ├── jchash.c └── crypto.c ├── README.md └── .github └── workflows └── build-release.yml /proto/roleagent/mail.sproto: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /proto/roleagent/bag.sproto: -------------------------------------------------------------------------------- 1 | # 背包:存放玩家道具和资源 2 | -------------------------------------------------------------------------------- /tools/etcd/start.sh: -------------------------------------------------------------------------------- 1 | docker compose up -d 2 | -------------------------------------------------------------------------------- /tools/mongodb/start.sh: -------------------------------------------------------------------------------- 1 | docker compose up -d 2 | 3 | -------------------------------------------------------------------------------- /app/role/roleagent/modules/bag/request.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | return M 4 | -------------------------------------------------------------------------------- /app/role/roleagent/modules/mail/request.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | return M 4 | -------------------------------------------------------------------------------- /tools/dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # cd 到当前脚本目录的上层目录 4 | cd "$(dirname "$0")/.." 5 | 6 | python tools/dist.py 7 | -------------------------------------------------------------------------------- /proto/base.sproto: -------------------------------------------------------------------------------- 1 | .package { 2 | type 0 : integer 3 | session 1 : integer 4 | ud 2 : integer 5 | } 6 | 7 | -------------------------------------------------------------------------------- /service/logger/global.lua: -------------------------------------------------------------------------------- 1 | local global = {} 2 | 3 | global.bucket = nil 4 | global.log_overload = false 5 | 6 | return global 7 | -------------------------------------------------------------------------------- /app/role/login/global.lua: -------------------------------------------------------------------------------- 1 | local global = {} 2 | 3 | global.watchdog_service = nil 4 | global.roleagentmgr_service = nil 5 | 6 | return global 7 | -------------------------------------------------------------------------------- /test/test_etcd/test.conf: -------------------------------------------------------------------------------- 1 | start = "test_etcd" 2 | include "../../etc/common.conf.lua" 3 | luaservice = luaservice .. root .. "test/?/main.lua;" 4 | -------------------------------------------------------------------------------- /test/test_jwt/test.conf: -------------------------------------------------------------------------------- 1 | start = "test_jwt" 2 | include "../../etc/common.conf.lua" 3 | luaservice = luaservice .. root .. "test/?/main.lua;" 4 | -------------------------------------------------------------------------------- /app/role/roleagent/global.lua: -------------------------------------------------------------------------------- 1 | local global = {} 2 | 3 | global.watchdog_service = nil 4 | global.roleagentmgr_service = nil 5 | 6 | return global 7 | -------------------------------------------------------------------------------- /tools/gen_proto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # cd 到当前脚本目录的上层目录 4 | cd "$(dirname "$0")/.." 5 | 6 | mkdir -p ../build/proto 7 | ./bin/lua tools/run.lua tools/proto2spb.lua proto/*.sproto proto/*/*.sproto 8 | -------------------------------------------------------------------------------- /lualib/log/log_level.lua: -------------------------------------------------------------------------------- 1 | -- level priority 2 | local LOG_LEVEL = { 3 | DEBUG = 4, 4 | INFO = 3, 5 | WARN = 2, 6 | ERROR = 1, 7 | FATAL = 0, 8 | } 9 | 10 | return LOG_LEVEL 11 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 120 2 | line_endings = "Unix" 3 | indent_type = "Spaces" 4 | indent_width = 4 5 | quote_style = "AutoPreferDouble" 6 | collapse_simple_statement = "Never" 7 | call_parentheses = "Input" 8 | -------------------------------------------------------------------------------- /proto/roleagent/role.sproto: -------------------------------------------------------------------------------- 1 | .role { 2 | rid 0 : integer 3 | name 1 : string 4 | } 5 | 6 | # 角色登录信息 7 | login_info 200 { 8 | request {} 9 | response { 10 | info 0 : role 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tools/mongodb/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mongodb: 3 | image: mongo:6-jammy 4 | ports: 5 | - '27017:27017' 6 | volumes: 7 | - mongodb-data:/data/db 8 | 9 | volumes: 10 | mongodb-data: 11 | -------------------------------------------------------------------------------- /lualib/ptype.lua: -------------------------------------------------------------------------------- 1 | -- 预定义的协议号及协议名字 2 | -- [0, 128) skynet,read skynet.lua 3 | local p = { 4 | PTYPE_LOG = 128, 5 | PTYPE_LOG_NAME = "log", 6 | 7 | PTYPE_LOG_ERR = 129, 8 | PTYPE_LOG_ERR_NAME = "log-err", 9 | } 10 | 11 | return p 12 | -------------------------------------------------------------------------------- /etc/monitor.conf.lua: -------------------------------------------------------------------------------- 1 | cluster_node_name = "monitor" 2 | cluster_listen_port = "7021" 3 | cluster_host = "127.0.0.1" 4 | 5 | start = "monitor" -- main script 6 | 7 | -- 负责管理role节点数量,使用etcd同步节点信息,使用jhash决定role应该在哪个节点上 8 | 9 | 10 | include "common.conf.lua" 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | build/ 3 | 4 | *.so 5 | 6 | tools/etcd/etcd1_data 7 | tools/etcd/etcd2_data 8 | tools/etcd/etcd3_data 9 | 10 | tools/mongodb/db 11 | 12 | logs/ 13 | dist/ 14 | 15 | skyext.zip 16 | skyext/ 17 | luaclib/ 18 | 19 | .DS_Store 20 | .aone_copilot/ 21 | -------------------------------------------------------------------------------- /schema/bag.sproto: -------------------------------------------------------------------------------- 1 | # 存放玩家道具和资源 2 | .role_bag { 3 | bags 0 : *bag(res_type) # 背包列表 4 | } 5 | 6 | .bag { 7 | res_type 0 : integer # 资源分类 : 区分不同类型的道具和资源 8 | res 1 : *resource(res_id) # 资源列表 9 | } 10 | 11 | # 资源 12 | .resource { 13 | res_id 0 : integer # 资源id 14 | res_size 1 : integer # 资源数量 15 | } 16 | -------------------------------------------------------------------------------- /tools/gen_schema.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # cd 到当前脚本目录的上层目录 4 | cd "$(dirname "$0")/.." 5 | 6 | mkdir -p lualib/orm 7 | ./bin/lua tools/run.lua 3rd/sproto-orm/tools/sproto2lua.lua lualib/orm/schema_define.lua schema/*.sproto 8 | ./bin/lua tools/run.lua 3rd/sproto-orm/tools/gen_schema.lua lualib/orm/schema.lua lualib/orm/schema_define.lua 9 | 10 | -------------------------------------------------------------------------------- /lualib/rolenode_api.lua: -------------------------------------------------------------------------------- 1 | local config = require "config" 2 | 3 | local M = {} 4 | 5 | local g_self_nodename = config.get("cluster_node_name") or "undefined_node" 6 | 7 | function M.calc_rolenode(rid) 8 | -- TODO: 使用jchash 计算 9 | return "rolenode1" 10 | end 11 | 12 | function M.self_rolenode() 13 | return g_self_nodename 14 | end 15 | 16 | return M 17 | -------------------------------------------------------------------------------- /service/logger/cmd.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local global = require "global" 3 | 4 | local M = {} 5 | 6 | function M.put(record) 7 | global.bucket:put(record) 8 | end 9 | 10 | function M.close() 11 | global.bucket:close() 12 | return skynet.self() 13 | end 14 | 15 | function M.reload() 16 | global.bucket:reload() 17 | return skynet.self() 18 | end 19 | 20 | return M 21 | -------------------------------------------------------------------------------- /app/role/roleagent/modules/mail/init.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local log = require "log" 3 | 4 | local M = {} 5 | M.__index = M 6 | 7 | function M.new(role_obj, data) 8 | local obj = { 9 | role_obj = role_obj, 10 | data = data, 11 | } 12 | setmetatable(obj, M) 13 | log.info("creating mail object", "rid", role_obj.rid) 14 | return obj 15 | end 16 | 17 | return M 18 | -------------------------------------------------------------------------------- /lualib/time.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local os_date = os.date 3 | 4 | local M = {} 5 | 6 | function M.now() 7 | return skynet.time() // 1 8 | end 9 | 10 | function M.time() 11 | return skynet.time() 12 | end 13 | 14 | function M.now_ms() 15 | return (skynet.time() * 1000) // 1 16 | end 17 | 18 | function M.format(sec) 19 | return os_date("%Y-%m-%d %H:%M:%S", sec) 20 | end 21 | 22 | return M 23 | -------------------------------------------------------------------------------- /lualib/httpc.lua: -------------------------------------------------------------------------------- 1 | local httpc = require "http.httpc" 2 | local cjson = require "cjson.safe" 3 | 4 | function httpc.post_json(host, url, data, recvheader) 5 | local header = { 6 | ["content-type"] = "application/json", 7 | } 8 | local status, body = httpc.request("POST", host, url, recvheader, header, cjson.encode(data)) 9 | if status == 200 then 10 | return status, cjson.decode(body) 11 | end 12 | return status, body 13 | end 14 | 15 | 16 | return httpc 17 | -------------------------------------------------------------------------------- /lualib/preload.lua: -------------------------------------------------------------------------------- 1 | -- 服务独有的库 2 | package.path = package.path .. SERVICE_PATH .. SERVICE_NAME .. "/lualib/?.lua;" 3 | package.path = package.path .. SERVICE_PATH .. SERVICE_NAME .. "/lualib/?/init.lua;" 4 | 5 | -- 进程独有的库 6 | local skynet = require "skynet" 7 | local root = skynet.getenv("root") 8 | local start = skynet.getenv("start") 9 | package.path = package.path .. root .. "app/" .. start .. "/lualib/?.lua;" 10 | package.path = package.path .. root .. "app/" .. start .. "/lualib/?/init.lua;" 11 | -------------------------------------------------------------------------------- /service/logger/main.lua: -------------------------------------------------------------------------------- 1 | -- logger 服务需要特殊处理,防止无法捕捉导启动失败导错误日志 2 | local ok, err_msg = xpcall(function() 3 | require "start" 4 | end, debug.traceback) 5 | 6 | if not ok then 7 | print(err_msg) 8 | 9 | local skynet = require "skynet" 10 | local bootfaillogpath = skynet.getenv("bootfaillogpath") 11 | local file = io.open(bootfaillogpath, "w+") 12 | if file then 13 | file:write(err_msg, "\n") 14 | file:flush() 15 | end 16 | skynet.abort() 17 | end 18 | -------------------------------------------------------------------------------- /etc/account1.conf.lua: -------------------------------------------------------------------------------- 1 | account_http_port = 8080 2 | account_agent_count = 1 3 | 4 | start = "account" -- main script account/main.lua 5 | 6 | debug_console_port = 6003 7 | 8 | log_config = [[ 9 | { 10 | { 11 | name = "file", 12 | filename = "logs/account.log", 13 | split = "size", -- size/line/day/hour 14 | maxsize = "100M", -- 每个文件最大尺寸 size split 有效 15 | }, 16 | { 17 | name = "console", 18 | } 19 | } 20 | ]] 21 | 22 | 23 | 24 | include "common.conf.lua" 25 | snowflake_machine_id = 2 26 | -------------------------------------------------------------------------------- /etc/account2.conf.lua: -------------------------------------------------------------------------------- 1 | account_http_port = 8081 2 | account_agent_count = 1 3 | 4 | start = "account" -- main script account/main.lua 5 | 6 | debug_console_port = 6004 7 | 8 | log_config = [[ 9 | { 10 | { 11 | name = "file", 12 | filename = "logs/account.log", 13 | split = "size", -- size/line/day/hour 14 | maxsize = "100M", -- 每个文件最大尺寸 size split 有效 15 | }, 16 | { 17 | name = "console", 18 | } 19 | } 20 | ]] 21 | 22 | 23 | 24 | include "common.conf.lua" 25 | snowflake_machine_id = 3 26 | -------------------------------------------------------------------------------- /schema/role.sproto: -------------------------------------------------------------------------------- 1 | .role { 2 | _version 0 : integer 3 | rid 1 : integer 4 | name 2 : string 5 | account 3 : string 6 | server 4 : string # 客户端看到的服务器 7 | game 5 : string # 框定玩法范围的游戏服务器:合服就是把game改成相同的 8 | create_time 6 : integer 9 | last_login_time 7 : integer 10 | modules 8 : role_modules # 模块列表 11 | } 12 | 13 | .role_modules { 14 | bag 0 : role_bag # 资源列表 15 | mail 1 : role_mail # 邮件列表 16 | } 17 | 18 | # TODO: lazy load 模块 19 | 20 | # TODO: 版本检查工具: 21 | # - 不允许删字段 22 | # - 不允许改字段类型 23 | # - 不允许改字段名字 24 | -------------------------------------------------------------------------------- /lualib/util/string.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | function M.fromhex(str) 4 | return (str:gsub("..", function(cc) 5 | return string.char(tonumber(cc, 16)) 6 | end)) 7 | end 8 | 9 | function M.tohex(str) 10 | return (str:gsub(".", function(c) 11 | return string.format("%02X", string.byte(c)) 12 | end)) 13 | end 14 | 15 | function M.split(s, delim) 16 | local sp = {} 17 | local pattern = "[^" .. delim .. ']+' 18 | string.gsub(s, pattern, function(v) table.insert(sp, v) end) 19 | return sp 20 | end 21 | 22 | return M 23 | -------------------------------------------------------------------------------- /proto/login.sproto: -------------------------------------------------------------------------------- 1 | # 网关向游戏报告连接信息 2 | report_remote_addr 100 { 3 | request { 4 | remote_addr 0 : string # 客户端ip 5 | local_addr 1 : string 6 | } 7 | } 8 | 9 | # 角色登录 10 | login 101 { 11 | request { 12 | token 0 : string # jwt token {account} 13 | rid 1 : integer # role id 直接登录角色 14 | proto_checksum 2 : string # 协议文件 checksum 15 | server 3 : string # 区服ID 16 | } 17 | response { 18 | code 0 : integer 19 | rid 1 : integer 20 | rolenode 2 : string # 需要连的游戏节点 21 | } 22 | } 23 | 24 | # 角色登出 25 | logout 102 {} 26 | -------------------------------------------------------------------------------- /app/role/roleagent/modules/role/request.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local log = require "log" 3 | local time = require "time" 4 | 5 | local M = {} 6 | 7 | function M:login_info(fd, client_obj) 8 | log.info("login_info", "fd", fd, "client_fd", client_obj.fd, "obj_fd", client_obj.role_obj.fd, "role_obj", client_obj.role_obj) 9 | client_obj.role_obj.data.last_login_time = time.now_ms() 10 | return { 11 | info = { 12 | rid = client_obj.role_obj.rid, 13 | name = client_obj.role_obj.data.name, 14 | }, 15 | } 16 | end 17 | 18 | return M 19 | -------------------------------------------------------------------------------- /app/role/roleagent/modules/bag/init.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local log = require "log" 3 | 4 | local M = {} 5 | M.__index = M 6 | 7 | function M.new(role_obj, data) 8 | local obj = { 9 | role_obj = role_obj, 10 | data = data, 11 | } 12 | setmetatable(obj, M) 13 | log.info("creating bag object", "rid", role_obj.rid) 14 | 15 | data.bags = {} 16 | data.bags[101] = { 17 | res_type = 101, 18 | res = { 19 | [10086] = { 20 | res_size = 1, 21 | }, 22 | }, 23 | } 24 | return obj 25 | end 26 | 27 | return M 28 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "skynet"] 2 | path = skynet 3 | url = https://github.com/cloudwu/skynet.git 4 | [submodule "3rd/sproto-orm"] 5 | path = 3rd/sproto-orm 6 | url = git@github.com:hanxi/sproto-orm.git 7 | [submodule "3rd/lua-cjson"] 8 | path = 3rd/lua-cjson 9 | url = https://github.com/cloudwu/lua-cjson.git 10 | [submodule "3rd/binaryheap.lua"] 11 | path = 3rd/binaryheap.lua 12 | url = https://github.com/Tieske/binaryheap.lua.git 13 | [submodule "3rd/luafilesystem"] 14 | path = 3rd/luafilesystem 15 | url = https://github.com/lunarmodules/luafilesystem.git 16 | [submodule "3rd/libsodium"] 17 | path = 3rd/libsodium 18 | url = https://github.com/jedisct1/libsodium.git 19 | -------------------------------------------------------------------------------- /app/robot/main.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local config = require "config" 3 | local log = require "log" 4 | 5 | skynet.start(function() 6 | log.info("robot start") 7 | if not config.get("daemon") then 8 | local console = skynet.newservice("console") 9 | end 10 | 11 | local robot_count = config.get_number("robot_count") or 1 12 | for i = 1, robot_count do 13 | local robotagent = skynet.newservice("robot/robotagent") 14 | skynet.call(robotagent, "lua", "start", { 15 | id = i, 16 | name = "robot_" .. i, 17 | }) 18 | log.info("robot service started", "id", i) 19 | end 20 | 21 | skynet.exit() 22 | end) 23 | -------------------------------------------------------------------------------- /etc/robot.conf.lua: -------------------------------------------------------------------------------- 1 | start = "robot" -- main script robot/main.lua 2 | 3 | robot_count = 1 4 | 5 | account_host = "http://127.0.0.1:8080" 6 | 7 | -- TODO: 从 platform 中获取 8 | gate_nodes = [[ 9 | { 10 | rolenode1 = { 11 | ip = "127.0.0.1", 12 | port = 7012, 13 | }, 14 | rolenode2 = { 15 | ip = "127.0.0.1", 16 | port = 7022, 17 | }, 18 | } 19 | ]] 20 | 21 | log_config = [[ 22 | { 23 | { 24 | name = "file", 25 | filename = "logs/robot.log", 26 | split = "size", -- size/line/day/hour 27 | maxsize = "100M", -- 每个文件最大尺寸 size split 有效 28 | }, 29 | { 30 | name = "console", 31 | } 32 | } 33 | ]] 34 | 35 | include "common.conf.lua" 36 | -------------------------------------------------------------------------------- /lualib/http_server/init.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local service = require "skynet.service" 3 | local watchdog = require "http_server.watchdog" 4 | local log = require "log" 5 | 6 | local M = {} 7 | 8 | function M.start(conf) 9 | local watchdog = service.new("http_watchdog", watchdog) 10 | skynet.call(watchdog, "lua", "start", conf) 11 | end 12 | 13 | function M.register_router(router_name) 14 | local watchdog = service.query("http_watchdog") 15 | if not watchdog then 16 | log.error("http_watchdog not exist", "router_name", router_name) 17 | return 18 | end 19 | skynet.call(watchdog, "lua", "register_router", router_name) 20 | end 21 | 22 | return M 23 | -------------------------------------------------------------------------------- /lualib/errcode.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | M.OK = 0 4 | M.NOT_THIS_NODE = 1 -- 非当前游戏节点,自动切到对应的游戏节点 5 | M.IN_OTHER_NODE = 2 -- 角色已在其他游戏节点,弹提示稍后重试 6 | M.ROLE_NOT_EXIST = 3 -- 角色不存在 7 | M.LOCAK_FAILED = 4 -- 角色锁定服务失败,弹提示稍后重试 8 | M.ROLE_TOO_MANY = 5 -- 角色数量超过限制 9 | M.DB_ERROR = 6 -- 数据库操作错误 10 | M.TOKEN_ERROR = 7 -- token 错误 11 | M.PROTO_CHECKSUM = 8 -- proto_checksum 错误 12 | M.SERVER_NOT_EXIST = 9 -- 服务器不存在 13 | 14 | M.MONGO_DUPLICATE_KEY = 11000 15 | 16 | -- TODO: 读取配表 17 | 18 | setmetatable(M, { 19 | __index = function(t, k) 20 | local v = rawget(t, k) 21 | if v ~= nil then 22 | return v 23 | end 24 | error("Invalid error code: " .. tostring(k)) 25 | end, 26 | }) 27 | 28 | return M 29 | -------------------------------------------------------------------------------- /lualib/launcher.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local config = require "config" 3 | local log = require "log" 4 | 5 | local M = {} 6 | 7 | function M.launcher_node() 8 | log.info("launcher_node begin") 9 | 10 | local debug_console_port = config.get_number("debug_console_port") 11 | log.info("debug console listen", "debug_console_port", debug_console_port) 12 | skynet.newservice("debug_console", debug_console_port) 13 | 14 | -- 创建 mongodb 索引 15 | local mongo_index = skynet.newservice("mongo_index") 16 | local all_ok = skynet.call(mongo_index, "lua", "create_indexes") 17 | assert(all_ok, "auto create indexes failed") 18 | skynet.kill(mongo_index) 19 | log.info("auto create indexes success") 20 | end 21 | 22 | return M 23 | -------------------------------------------------------------------------------- /etc/role1.conf.lua: -------------------------------------------------------------------------------- 1 | cluster_node_name = "rolenode1" 2 | cluster_listen_port = "7011" 3 | cluster_host = "127.0.0.1" 4 | 5 | debug_console_port = 6001 6 | 7 | gate_port = "7012" 8 | 9 | start = "role" -- main script role/main.lua 10 | maxclient = 1024 11 | -- daemon = "./role1.pid" 12 | 13 | agent_count = 2 14 | login_timeout_sec = 60 -- 登录连接验证超时,单位秒 15 | 16 | role_offline_unload_sec = 5 -- 角色离线后多久卸载,单位秒 17 | 18 | log_config = [[ 19 | { 20 | { 21 | name = "file", 22 | filename = "logs/role1.log", 23 | split = "size", -- size/line/day/hour 24 | maxsize = "100M", -- 每个文件最大尺寸 size split 有效 25 | }, 26 | { 27 | name = "console", 28 | } 29 | } 30 | ]] 31 | 32 | snowflake_machine_id = 0 33 | include "common.conf.lua" 34 | -------------------------------------------------------------------------------- /lualib/log/bucket/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local buckets = {} 4 | 5 | function M.new(conf) 6 | local name = conf.name 7 | local mod = buckets[name] 8 | if not mod then 9 | local ok 10 | ok, mod = pcall(require, "log.bucket." .. name) 11 | if not ok then 12 | error("bucket name not found: " .. name .. mod) 13 | end 14 | buckets[name] = mod 15 | end 16 | 17 | local ok, bucket_obj = pcall(mod.new, conf) 18 | if not ok then 19 | error("bucket new failed: " .. bucket_obj) 20 | end 21 | return bucket_obj 22 | end 23 | 24 | -- 保底bucket 25 | local default_bucket = { name = "console" } 26 | function M.get_default() 27 | return M.new(default_bucket) 28 | end 29 | 30 | return M 31 | -------------------------------------------------------------------------------- /etc/role2.conf.lua: -------------------------------------------------------------------------------- 1 | cluster_node_name = "rolenode2" 2 | cluster_listen_port = "7021" 3 | cluster_host = "127.0.0.1" 4 | 5 | debug_console_port = 6002 6 | 7 | gate_port = "7022" 8 | 9 | start = "role" -- main script role/main.lua 10 | maxclient = 1024 11 | -- daemon = "./role2.pid" 12 | 13 | agent_count = 2 14 | login_timeout_sec = 60 -- 登录连接验证超时,单位秒 15 | 16 | role_offline_unload_sec = 5 * 60 -- 角色离线后多久卸载,单位秒 17 | 18 | log_config = [[ 19 | { 20 | { 21 | name = "file", 22 | filename = "logs/role2.log", 23 | split = "size", -- size/line/day/hour 24 | maxsize = "100M", -- 每个文件最大尺寸 size split 有效 25 | }, 26 | { 27 | name = "console", 28 | } 29 | } 30 | ]] 31 | 32 | snowflake_machine_id = 1 33 | include "common.conf.lua" 34 | -------------------------------------------------------------------------------- /lualib/log/util.lua: -------------------------------------------------------------------------------- 1 | local log_level = require "log.log_level" 2 | 3 | local assert = assert 4 | local smatch = string.match 5 | local supper = string.upper 6 | 7 | local M = {} 8 | 9 | function M.parse_level(input) 10 | if not input then 11 | return 12 | end 13 | local upper, lower = smatch(input, "^(%a*)-?(%a*)$") 14 | upper = assert(log_level[#upper > 0 and supper(upper) or "DEBUG"], upper) 15 | lower = assert(log_level[#lower > 0 and supper(lower) or "FATAL"], lower) 16 | assert(lower <= upper, "invalid log level setting") 17 | return { lower = lower, upper = upper } 18 | end 19 | 20 | function M.should_log(level, record_level) 21 | return not (level and (record_level < level.lower or record_level > level.upper)) 22 | end 23 | 24 | return M 25 | -------------------------------------------------------------------------------- /app/role/main.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local config = require "config" 3 | local log = require "log" 4 | local launcher = require "launcher" 5 | 6 | local function launcher_rolenode() 7 | local watchdog = skynet.newservice("watchdog") 8 | local max_client = config.get_number("maxclient") 9 | local gate_port = config.get_number("gate_port") 10 | skynet.call(watchdog, "lua", "start", { 11 | port = gate_port, 12 | maxclient = max_client, 13 | nodelay = true, 14 | }) 15 | log.info("watchdog listen", "gate_port", gate_port) 16 | end 17 | 18 | skynet.start(function() 19 | log.info("role start begin") 20 | launcher.launcher_node() 21 | launcher_rolenode() 22 | log.info("role start finished") 23 | skynet.exit() 24 | end) 25 | -------------------------------------------------------------------------------- /schema/mail.sproto: -------------------------------------------------------------------------------- 1 | # 存放玩家邮件 2 | .role_mail { 3 | _version 0 : integer 4 | mails 1 : *mail(mail_id) # 邮件列表 5 | } 6 | 7 | # 一封邮件 8 | .mail { 9 | mail_id 0 : integer # 邮件id 10 | cfg_id 1 : integer # 邮件配置id 11 | title 2 : *str2str() # 邮件标题 12 | detail 3 : *str2str() # 邮件内容 13 | send_time 4 : integer # 发送时间 14 | send_role 5 : mail_role # 发送人 15 | attach 6 : mail_attach # 附件 16 | } 17 | 18 | # format 参数 19 | .str2str { 20 | key 0 : string 21 | value 1 : string 22 | } 23 | 24 | .mail_role { 25 | rid 0 : integer # 玩家id 26 | name 1 : string # 玩家名字 27 | } 28 | 29 | # 资源 30 | .mail_attach { 31 | res_type 0 : integer # 资源分类 : 区分不同类型的道具和资源 32 | res_id 1 : integer # 资源id 33 | res_size 2 : integer # 资源数量 34 | } 35 | 36 | ## TODO: 全局邮件存储 另外定义? 37 | -------------------------------------------------------------------------------- /app/account/main.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local http_server = require "http_server" 3 | local config = require "config" 4 | local launcher = require "launcher" 5 | local log = require "log" 6 | 7 | local function launcher_accountnode() 8 | local port = config.get_number("account_http_port") or 8080 9 | local agent_count = config.get_number("account_agent_count") or 8 10 | local conf = { 11 | port = port, 12 | agent_count = agent_count, 13 | } 14 | http_server.start(conf) 15 | http_server.register_router("account_router") 16 | end 17 | 18 | skynet.start(function() 19 | log.info("account start begin") 20 | launcher.launcher_node() 21 | launcher_accountnode() 22 | log.info("account start finished") 23 | skynet.exit() 24 | end) 25 | 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all : help 2 | 3 | .PHONY: proto copy schema autocode dist 4 | 5 | help: 6 | @echo "支持下面命令:" 7 | @awk '/^#/{comment=substr($$0,2)} /^[a-zA-Z0-9_-]+:/{gsub(/:/, ""); if(comment)printf "make %-12s #%s\n", $$1, comment; comment=""}' $(MAKEFILE_LIST) 8 | 9 | include build.mk 10 | 11 | # 初始化 submodule 12 | init: 13 | git submodule update --init --recursive 14 | 15 | # 拷贝 3rd 里必要的 lua 文件 16 | copy: 17 | # 这些lua文件直接拷贝,因此如果需要修改,则应该取改 3rd 目录下的原文件 18 | cp -rf 3rd/sproto-orm/orm $(LUA_LIB_PATH)/ 19 | cp -f 3rd/binaryheap.lua/src/binaryheap.lua $(LUA_LIB_PATH)/ 20 | 21 | # 编译协议 22 | proto: 23 | ./tools/gen_proto.sh 24 | 25 | # 编译 ORM 模式文件 26 | schema: 27 | ./tools/gen_schema.sh 28 | 29 | # 生成代码 30 | autocode: 31 | ./bin/lua tools/run.lua tools/gen_roleagent_modules.lua 32 | 33 | # 打包 34 | dist: build 35 | ./tools/dist.sh 36 | -------------------------------------------------------------------------------- /lualib/cmd_api.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | 3 | local M = {} 4 | 5 | function M.dispatch(CMD) 6 | skynet.dispatch("lua", function(_, _, cmd, ...) 7 | local f = CMD[cmd] 8 | if not f then 9 | error(string.format("Unknown command: %s", cmd)) 10 | end 11 | skynet.ret(skynet.pack(f(...))) 12 | end) 13 | end 14 | 15 | function M.dispatch_socket(CMD, SOCKET) 16 | skynet.dispatch("lua", function(_, _, cmd, subcmd, ...) 17 | if cmd == "socket" then 18 | local f = SOCKET[subcmd] 19 | f(...) 20 | -- socket api don't need return 21 | else 22 | local f = CMD[cmd] 23 | if not f then 24 | error("Unknown command:", cmd) 25 | end 26 | skynet.ret(skynet.pack(f(subcmd, ...))) 27 | end 28 | end) 29 | end 30 | 31 | return M 32 | -------------------------------------------------------------------------------- /test/test_jwt/main.lua: -------------------------------------------------------------------------------- 1 | local jwt = require "jwt" 2 | local log = require "log" 3 | 4 | 5 | -- HS256 6 | local token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ind3dy5iZWpzb24uY29tIiwic3ViIjoiZGVtbyIsImlhdCI6MTc1OTkzMDg5MCwibmJmIjoxNzU5OTMwODkwLCJleHAiOjE3NjAwMTcyOTB9.K-k4Rbw_rmfUubjWokBXWQyExsGMM0BiWj1yIdmnnTg" 7 | local secret = "bejson858364" 8 | local ret,msg = jwt.verify(token, secret) 9 | if not ret then 10 | error(msg) 11 | end 12 | log.info("jwt hs256 ok", "ret", ret) 13 | 14 | -- HS512 15 | local token = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ind3dy5iZWpzb24uY29tIiwic3ViIjoiZGVtbyIsImlhdCI6MTc1OTkzMDg5MCwibmJmIjoxNzU5OTMwODkwLCJleHAiOjE3NjAwMTcyOTB9.XFnqU9IKIVEyL0em5nCLwf5C-FYNEEb1JTNz9WIHzVNz1p9QgkH0aY3FSoe_lLYv4gPSrDngph5CVvPuQ2SHbA" 16 | local secret = "bejson858364" 17 | local ret,msg = jwt.verify(token, secret) 18 | if not ret then 19 | error(msg) 20 | end 21 | log.info("jwt hs512 ok", "ret", ret) 22 | 23 | -------------------------------------------------------------------------------- /tools/proto2spb.lua: -------------------------------------------------------------------------------- 1 | local parse_core = require "core" 2 | local util = require "util" 3 | 4 | local function _gen_trunk_list(sproto_file, namespace) 5 | local trunk_list = {} 6 | for i, v in ipairs(sproto_file) do 7 | namespace = namespace and util.file_basename(v) or nil 8 | table.insert(trunk_list, { util.read_file(v), v, namespace }) 9 | end 10 | return trunk_list 11 | end 12 | 13 | local sproto_file = {} 14 | for i = 1, #arg do 15 | local file = arg[i] 16 | if file:match("%.sproto$") then 17 | table.insert(sproto_file, file) 18 | print("Adding sproto file: " .. file) 19 | else 20 | print("Invalid file format: " .. file) 21 | return 22 | end 23 | end 24 | local m = require "module.spb" 25 | local trunk_list = _gen_trunk_list(sproto_file, true) 26 | local trunk, build = parse_core.gen_trunk(trunk_list) 27 | local param = { 28 | outfile = "build/proto/sproto.spb", 29 | } 30 | m(trunk, build, param) 31 | -------------------------------------------------------------------------------- /lualib/log/bucket/console.lua: -------------------------------------------------------------------------------- 1 | -- 输出到屏幕 2 | local log_formatter = require "log.formatter" 3 | local log_util = require "log.util" 4 | 5 | local should_log = log_util.should_log 6 | local parse_level = log_util.parse_level 7 | 8 | local M = {} 9 | 10 | local console_mt = {} 11 | console_mt.__index = console_mt 12 | 13 | function console_mt:put(record) 14 | if not should_log(self.level, record.level) then 15 | return nil 16 | end 17 | local msg = self.formatter(record) 18 | self.handle:write(msg, "\n") 19 | return true 20 | end 21 | 22 | function M.new(params) 23 | local console_obj = {} 24 | console_obj.params = { format = "text", color = true } 25 | for k, v in pairs(params) do 26 | console_obj.params[k] = v 27 | end 28 | params = console_obj.params 29 | 30 | console_obj.level = parse_level(params.level) 31 | console_obj.formatter = log_formatter.get_formatter(params.format, params.color, params.style) 32 | console_obj.handle = io.stdout 33 | console_obj = setmetatable(console_obj, console_mt) 34 | return console_obj 35 | end 36 | 37 | return M 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 涵曦 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/role/roleagentmgr.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local cmd_api = require "cmd_api" 3 | local cluster_discovery = require "cluster_discovery" 4 | local errcode = require "errcode" 5 | local config = require "config" 6 | local log = require "log" 7 | local roleagent_api = require "roleagent_api" 8 | 9 | log.config { 10 | name = "roleagentmgr", 11 | } 12 | 13 | local CMD = {} 14 | local g_agents = {} 15 | local g_watchdog_service 16 | 17 | function CMD.start(conf) 18 | g_watchdog_service = conf.watchdog 19 | local agent_count = config.get_number("agent_count") or 2 20 | for i = 1, agent_count do 21 | local agent_name = roleagent_api.format_agent_name(i) 22 | local agent = skynet.newservice(agent_name, i) 23 | skynet.call(agent, "lua", "start", { 24 | watchdog = g_watchdog_service, 25 | roleagentmgr = skynet.self(), 26 | }) 27 | g_agents[i] = agent 28 | log.info("role agent service started", "agent", agent, "i", i) 29 | end 30 | end 31 | 32 | skynet.start(function() 33 | cmd_api.dispatch(CMD) 34 | cluster_discovery.register({ "roleagentmgr" }) 35 | end) 36 | -------------------------------------------------------------------------------- /app/role/roleagent/modules/role/init.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local timer = require "timer" 3 | local config = require "config" 4 | local log = require "log" 5 | 6 | local M = {} 7 | M.__index = M 8 | local role_offline_unload_sec = config.get_number("role_offline_unload_sec") 9 | 10 | function M.new(role_mgr, rid, data) 11 | local obj = { 12 | role_mgr = role_mgr, 13 | rid = rid, 14 | data = data, 15 | fd = nil, -- 绑定的客户端 fd 16 | } 17 | setmetatable(obj, M) 18 | log.info("creating role object", "rid", rid, "name", data.name) 19 | return obj 20 | end 21 | 22 | function M:bind_fd(fd) 23 | self.fd = fd 24 | log.info("binding role", "rid", self.rid, "fd", fd) 25 | if self.offline_unload_timer then 26 | self.offline_unload_timer:cancel() 27 | end 28 | end 29 | 30 | function M:unbind_fd() 31 | self.fd = nil 32 | log.info("unbinding role", "rid", self.rid) 33 | 34 | self.offline_unload_timer = timer.timeout("role_offline_unload", role_offline_unload_sec, function() 35 | if self.fd == nil then 36 | self.role_mgr.unload_role(self.rid) 37 | end 38 | end) 39 | end 40 | 41 | return M 42 | -------------------------------------------------------------------------------- /lualib-src/jchash.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Jump Consistent Hash from Google 3 | * http://arxiv.org/pdf/1406.2294.pdf 4 | */ 5 | 6 | #define LUA_LIB 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | int32_t jump_consistent_hash(uint64_t key, int32_t num_buckets) { 13 | int64_t b = -1; 14 | int64_t j = 0; 15 | while (j < num_buckets) { 16 | b = j; 17 | key = key * 2862933555777941757ULL + 1; 18 | j = (b + 1) * ((double)(1LL << 31) / (double)((key >> 33) + 1)); 19 | } 20 | return b; 21 | } 22 | 23 | static int 24 | ljchash(lua_State *L) { 25 | uint64_t key = (uint64_t)luaL_checkinteger(L, 1); 26 | int32_t num_buckets = luaL_checkinteger(L, 2); 27 | 28 | if (num_buckets <= 0) { 29 | return luaL_error(L, "num_buckets must be positive"); 30 | } 31 | 32 | int32_t result = jump_consistent_hash(key, num_buckets); 33 | lua_pushinteger(L, result); 34 | return 1; 35 | } 36 | 37 | LUAMOD_API int 38 | luaopen_jchash(lua_State *L) { 39 | luaL_checkversion(L); 40 | 41 | luaL_Reg l[] = { 42 | { "hash", ljchash }, 43 | { NULL, NULL }, 44 | }; 45 | 46 | luaL_newlib(L,l); 47 | 48 | return 1; 49 | } 50 | -------------------------------------------------------------------------------- /app/role/login/main.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local sproto_api = require "sproto_api" 3 | local cmd_api = require "cmd_api" 4 | local client = require "client" 5 | local login_request = require "login_request" 6 | local global = require "global" 7 | local log = require "log" 8 | 9 | log.config { 10 | name = "login", 11 | } 12 | 13 | local CMD = {} 14 | 15 | function CMD.start(conf) 16 | global.watchdog_service = conf.watchdog 17 | global.roleagentmgr_service = conf.roleagentmgr 18 | log.info("login service started", "watchdog", global.watchdog_service, "roleagentmgr", global.roleagentmgr_service) 19 | end 20 | 21 | function CMD.open(client_conf) 22 | log.info("new client", "fd", client_conf.fd, "addr", client_conf.addr) 23 | client.new(client_conf) 24 | skynet.call(global.watchdog_service, "lua", "forward", client_conf.fd, skynet.self()) 25 | log.info("opened client", "fd", client_conf.fd, "addr", client_conf.addr) 26 | end 27 | 28 | function CMD.disconnect(fd) 29 | log.info("closing client", "fd", fd) 30 | client.unbind(fd) 31 | end 32 | 33 | skynet.start(function() 34 | sproto_api.register_module("login", client, login_request) 35 | cmd_api.dispatch(CMD) 36 | end) 37 | -------------------------------------------------------------------------------- /app/role/roleagent/modules/init.lua: -------------------------------------------------------------------------------- 1 | -- Code generated from tools/gen_roleagent_modules.lua 2 | -- DO NOT EDIT! 3 | 4 | local schema = require "orm.schema" 5 | local sproto_api = require "sproto_api" 6 | local bag_request = require "modules.bag.request" 7 | local bag = require "modules.bag" 8 | local role_request = require "modules.role.request" 9 | local mail_request = require "modules.mail.request" 10 | local mail = require "modules.mail" 11 | 12 | local M = {} 13 | function M.init(client) 14 | sproto_api.register_module("bag", client, bag_request) 15 | sproto_api.register_module("role", client, role_request) 16 | sproto_api.register_module("mail", client, mail_request) 17 | end 18 | 19 | function M.load(role_obj, role_data) 20 | if role_data.modules == nil then 21 | role_data.modules = {} 22 | end 23 | 24 | role_obj.modules = {} 25 | 26 | if role_data.modules.bag == nil then 27 | role_data.modules.bag = {} 28 | end 29 | role_obj.modules.bag = bag.new(role_obj, role_data.modules.bag) 30 | 31 | if role_data.modules.mail == nil then 32 | role_data.modules.mail = {} 33 | end 34 | role_obj.modules.mail = mail.new(role_obj, role_data.modules.mail) 35 | end 36 | return M 37 | -------------------------------------------------------------------------------- /lualib/log/init.lua: -------------------------------------------------------------------------------- 1 | local logger = require "log.logger" 2 | 3 | local M = {} 4 | 5 | local default_logger = logger.new() 6 | 7 | M.logger = default_logger 8 | 9 | function M.config(...) 10 | return default_logger:config(...) 11 | end 12 | 13 | function M.debug(...) 14 | return default_logger:debug(...) 15 | end 16 | 17 | function M.info(...) 18 | return default_logger:info(...) 19 | end 20 | 21 | function M.warn(...) 22 | return default_logger:warn(...) 23 | end 24 | 25 | function M.error(...) 26 | return default_logger:error(...) 27 | end 28 | 29 | function M.xpcall_msgh(...) 30 | return default_logger:xpcall_msgh(...) 31 | end 32 | 33 | function M.sys_assert(...) 34 | return default_logger:sys_assert(...) 35 | end 36 | 37 | function M.sys_error(...) 38 | return default_logger:sys_error(...) 39 | end 40 | 41 | local config = require "config" 42 | xpcall(default_logger.config, default_logger.error, { 43 | level = config.get_number("log_level"), 44 | log_src = config.get_boolean("log_src"), 45 | log_table = config.get_boolean("log_print_table"), 46 | }) 47 | 48 | _G.raw_assert = assert 49 | _G.raw_error = error 50 | assert = M.sys_assert 51 | error = M.sys_error 52 | pcall = logger.safe_pcall 53 | xpcall = logger.safe_xpcall 54 | 55 | return M 56 | -------------------------------------------------------------------------------- /app/role/roleagent/client.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local global = require "global" 3 | local log = require "log" 4 | 5 | local M = {} 6 | 7 | local g_clients = {} 8 | 9 | local client = {} 10 | client.__index = client 11 | 12 | local function new_client_obj(fd, role_obj) 13 | local obj = { 14 | fd = fd, 15 | role_obj = role_obj, 16 | } 17 | return setmetatable(obj, client) 18 | end 19 | 20 | function M.unbind(fd) 21 | local client_obj = g_clients[fd] 22 | log.info("unbinding begin client", "fd", fd, "client_obj", client_obj) 23 | if client_obj and client_obj.role_obj then 24 | log.info("unbinding client fd", "fd", fd) 25 | client_obj.role_obj:unbind_fd() 26 | end 27 | g_clients[fd] = nil 28 | end 29 | 30 | function M.unbind_kick(fd) 31 | M.unbind(fd) 32 | -- 断开客户端连接 33 | skynet.call(global.watchdog_service, "lua", "close_client", fd) 34 | end 35 | 36 | function M.bind(fd, role_obj) 37 | assert(not g_clients[fd], fd .. " exists") 38 | -- bind fd to role_obj 39 | role_obj:bind_fd(fd) 40 | g_clients[fd] = new_client_obj(fd, role_obj) 41 | log.info("binding client", "fd", fd, "rid", role_obj.rid, "name", role_obj.data.name) 42 | return g_clients[fd] 43 | end 44 | 45 | function M.get_obj(fd) 46 | return g_clients[fd] 47 | end 48 | 49 | return M 50 | -------------------------------------------------------------------------------- /service/logger/checker.lua: -------------------------------------------------------------------------------- 1 | --- 通过队列长度判断日志服务过载,避免堆积日志过大造成内存 oom 2 | local skynet = require "skynet" 3 | local config = require "config" 4 | local global = require "global" 5 | local log = require "log" 6 | local timer = require "timer" 7 | 8 | local xpcall_msgh = log.xpcall_msgh 9 | 10 | local HEARTBEAT_INTERVAL = 10 11 | 12 | local checkers = {} 13 | 14 | local overload_mqlen = config.get_number("log_overload_mqlen") or 1000000 15 | local overload_off_mqlen = overload_mqlen * 0.8 16 | checkers.check_mqlen = function() 17 | local mqlen = skynet.mqlen() 18 | 19 | if global.log_overload and mqlen < overload_off_mqlen then 20 | global.log_overload = false 21 | log.info("log overload off", "mqlen", mqlen) 22 | return 23 | end 24 | 25 | if mqlen >= overload_mqlen then 26 | log.error("log overload on", "mqlen", mqlen) 27 | global.log_overload = true 28 | end 29 | end 30 | 31 | checkers.check_bucket = function() 32 | for _, bucket in pairs(global.bucket.buckets) do 33 | if bucket.check_error and bucket.reload and bucket:check_error() then 34 | bucket:reload() 35 | end 36 | end 37 | end 38 | 39 | skynet.init(function() 40 | xpcall(timer.repeat_delayed, xpcall_msgh, "log_heartbeat", HEARTBEAT_INTERVAL, function() 41 | for _, f in pairs(checkers) do 42 | pcall(f) 43 | end 44 | end) 45 | end) 46 | -------------------------------------------------------------------------------- /lualib/loader.lua: -------------------------------------------------------------------------------- 1 | SERVICE_ARGS = ... 2 | 3 | local args = {} 4 | for word in string.gmatch(SERVICE_ARGS, "%S+") do 5 | table.insert(args, word) 6 | end 7 | 8 | -- :后面的部分会被忽略 9 | SERVICE_NAME = string.gsub(args[1], ":.*", "", 1) 10 | 11 | local main, pattern 12 | 13 | local err = {} 14 | for pat in string.gmatch(LUA_SERVICE, "([^;]+);*") do 15 | local filename = string.gsub(pat, "?", SERVICE_NAME) 16 | local f, msg = loadfile(filename) 17 | if not f then 18 | table.insert(err, msg) 19 | else 20 | pattern = pat 21 | main = f 22 | break 23 | end 24 | end 25 | 26 | if not main then 27 | error(table.concat(err, "\n")) 28 | end 29 | 30 | LUA_SERVICE = nil 31 | package.path , LUA_PATH = LUA_PATH, nil 32 | package.cpath , LUA_CPATH = LUA_CPATH, nil 33 | local service_path = string.match(pattern, "(.*/)[^/?]+$") 34 | 35 | if service_path then 36 | service_path = string.gsub(service_path, "?", SERVICE_NAME) 37 | package.path = service_path .. "?.lua;" .. package.path 38 | package.path = service_path .. "?/init.lua;" .. package.path 39 | SERVICE_PATH = service_path 40 | else 41 | local p = string.match(pattern, "(.*/).+$") 42 | SERVICE_PATH = p 43 | end 44 | 45 | require("skyext") 46 | 47 | if LUA_PRELOAD then 48 | local f = assert(loadfile(LUA_PRELOAD)) 49 | f(table.unpack(args)) 50 | LUA_PRELOAD = nil 51 | end 52 | 53 | _G.require = (require "skynet.require").require 54 | 55 | main(select(2, table.unpack(args))) 56 | -------------------------------------------------------------------------------- /lualib/log/bucket/service.lua: -------------------------------------------------------------------------------- 1 | -- 推送到 logger 服务 2 | local skynet = require "skynet" 3 | local ptype = require "ptype" 4 | local log_level = require "log.log_level" 5 | 6 | local PTYPE_LOG_NAME = ptype.PTYPE_LOG_NAME 7 | local PTYPE_LOG_ERR_NAME = ptype.PTYPE_LOG_ERR_NAME 8 | 9 | skynet.register_protocol { 10 | name = ptype.PTYPE_LOG_NAME, 11 | id = ptype.PTYPE_LOG, 12 | pack = skynet.pack, 13 | } 14 | 15 | skynet.register_protocol { 16 | name = ptype.PTYPE_LOG_ERR_NAME, 17 | id = ptype.PTYPE_LOG_ERR, 18 | pack = skynet.pack, 19 | } 20 | 21 | local M = {} 22 | local bucket = {} 23 | 24 | function bucket:put(record) 25 | if record.level <= log_level.WARN then 26 | skynet.send(self.service, PTYPE_LOG_ERR_NAME, record) 27 | else 28 | skynet.send(self.service, PTYPE_LOG_NAME, record) 29 | end 30 | return true 31 | end 32 | 33 | function M.new(conf) 34 | if bucket.service then 35 | return bucket 36 | end 37 | 38 | local service = skynet.localname(".logger") 39 | if not service then 40 | return nil 41 | end 42 | 43 | -- 在logger服务获取service bucket则返回其自己的实例 44 | if service == skynet.self() then 45 | local ok, mod = pcall(require, "global") 46 | if ok and mod then 47 | return mod.bucket 48 | end 49 | return nil 50 | end 51 | 52 | bucket.service = service 53 | return bucket 54 | end 55 | 56 | return M 57 | -------------------------------------------------------------------------------- /app/role/lualib/roleagent_api.lua: -------------------------------------------------------------------------------- 1 | local config = require "config" 2 | local skynet = require "skynet" 3 | 4 | local M = {} 5 | local agent_count = config.get_number("agent_count") or 2 6 | 7 | function M.calc_agent_index(rid) 8 | local agent_index = rid % agent_count + 1 9 | return agent_index 10 | end 11 | 12 | function M.format_agent_name(agent_index) 13 | return string.format("roleagent:%d", agent_index) 14 | end 15 | 16 | function M.format_agent_service_name(agent_index) 17 | return string.format(".roleagent:%d", agent_index) 18 | end 19 | 20 | local agent_name_cache = {} 21 | function M.calc_agent_name(rid) 22 | local agent_index = M.calc_agent_index(rid) 23 | if agent_name_cache[agent_index] then 24 | return agent_name_cache[agent_index] 25 | end 26 | local agent_name = M.format_agent_service_name(agent_index) 27 | agent_name_cache[agent_index] = agent_name 28 | return agent_name 29 | end 30 | 31 | -- TODO: 如果 agent 重启,这里需要更新缓存 32 | local agent_addr_cache = {} 33 | function M.calc_agent_addr(rid) 34 | local agent_name = M.calc_agent_name(rid) 35 | if agent_addr_cache[agent_name] then 36 | return agent_addr_cache[agent_name] 37 | end 38 | 39 | local addr = skynet.localname(agent_name) 40 | if not addr then 41 | error(string.format("agent_name %s not exist", agent_name)) 42 | end 43 | agent_addr_cache[agent_name] = addr 44 | return addr 45 | end 46 | 47 | return M 48 | -------------------------------------------------------------------------------- /service/mongo_index.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local mongo = require "skynet.db.mongo" 3 | local config = require "config" 4 | local cmd_api = require "cmd_api" 5 | local log = require "log" 6 | 7 | local CMD = {} 8 | 9 | local mongo_config = config.get_table("mongo_config") 10 | 11 | function CMD.create_indexes() 12 | local all_ok = true 13 | for dbname, db_config in pairs(mongo_config) do 14 | local dbs = mongo.client(db_config.cfg) 15 | local db = dbs[dbname] 16 | for coll_name, coll_config in pairs(db_config.collections) do 17 | local collection = db[coll_name] 18 | if collection then 19 | for _, index in ipairs(coll_config.indexes or {}) do 20 | local ok, err = pcall(collection.createIndex, collection, index) 21 | if not ok then 22 | log.error("failed to create index", "dbname", dbname, "coll_name", coll_name, "err", err) 23 | all_ok = false 24 | else 25 | log.info("index created successfully", "dbname", dbname, "coll_name", coll_name, "index", index) 26 | end 27 | end 28 | else 29 | log.error("collection not found", "dbname", dbname, "coll_name", coll_name) 30 | all_ok = false 31 | end 32 | end 33 | end 34 | return all_ok 35 | end 36 | 37 | skynet.start(function() 38 | cmd_api.dispatch(CMD) 39 | end) 40 | -------------------------------------------------------------------------------- /app/role/roleagent/rolemgr.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local role = require "modules.role" 3 | local client = require "client" 4 | local dbmgr = require "dbmgr" 5 | local config = require "config" 6 | local log = require "log" 7 | local modules = require "modules" 8 | 9 | local M = {} 10 | 11 | local g_role_db_name = config.get("role_db_name") 12 | local g_role_db_coll = config.get("role_db_coll") 13 | 14 | local g_roles = {} 15 | 16 | function M.load_role(rid) 17 | if g_roles[rid] then 18 | log.info("load role already exists", "rid", rid) 19 | return g_roles[rid] 20 | end 21 | 22 | local role_data = dbmgr.load(g_role_db_name, g_role_db_coll, "rid", rid) 23 | local role_obj = role.new(M, rid, role_data) 24 | g_roles[rid] = role_obj 25 | modules.load(role_obj, role_data) 26 | return role_obj 27 | end 28 | 29 | function M.get_role(rid) 30 | return g_roles[rid] 31 | end 32 | 33 | function M.unload_role(rid) 34 | local role_obj = g_roles[rid] 35 | if not role_obj then 36 | log.warn("unload role not exists", "rid", rid) 37 | return 38 | end 39 | if role_obj.fd then 40 | client.unbind_kick(role_obj.fd) 41 | end 42 | 43 | -- save_obj 44 | dbmgr.unload(g_role_db_name, g_role_db_coll, "rid", rid) 45 | 46 | -- TODO: role_obj 支持 lazy_load 数据,在默认情况下不加载,在使用时才加载. 47 | -- lazy_load_data 加载晚,卸载早 48 | -- 一般用于一些不常用的数据 49 | 50 | g_roles[rid] = nil 51 | 52 | log.info("unloading role", "rid", rid) 53 | end 54 | 55 | return M 56 | -------------------------------------------------------------------------------- /lualib/http_server/watchdog.lua: -------------------------------------------------------------------------------- 1 | return function() 2 | local skynet = require "skynet" 3 | local service = require "skynet.service" 4 | local socket = require "skynet.socket" 5 | local log = require "log" 6 | local agent = require "http_server.agent" 7 | local cmd_api = require "cmd_api" 8 | 9 | local CMD = {} 10 | 11 | local agents = {} 12 | 13 | function CMD.start(conf) 14 | local port = conf.port or 8080 15 | local agent_count = conf.agent_count or 8 16 | for agent_id = 1, agent_count do 17 | local service_name = string.format("http_agent_%d", agent_id) 18 | local agent = service.new(service_name, agent, agent_id) 19 | agents[agent_id] = agent 20 | end 21 | 22 | local host = conf.host or "0.0.0.0" 23 | local balance = 1 24 | local listen_id = socket.listen(host, port) 25 | log.info("start http", "host", host, "port", port) 26 | socket.start(listen_id, function(id, addr) 27 | skynet.send(agents[balance], "lua", "socket", "request", id, addr) 28 | balance = balance + 1 29 | if balance > #agents then 30 | balance = 1 31 | end 32 | end) 33 | end 34 | 35 | function CMD.register_router(router_name) 36 | for _, agent in pairs(agents) do 37 | skynet.send(agent, "lua", "register_router", router_name) 38 | end 39 | end 40 | 41 | skynet.start(function() 42 | cmd_api.dispatch(CMD) 43 | end) 44 | end 45 | -------------------------------------------------------------------------------- /lualib/config.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | 3 | local M = {} 4 | local conf = {} 5 | 6 | function M.get(key) 7 | if conf[key] ~= nil then 8 | return conf[key] 9 | end 10 | 11 | local value = skynet.getenv(key) 12 | if value == nil then 13 | return 14 | end 15 | 16 | conf[key] = value 17 | return conf[key] 18 | end 19 | 20 | function M.get_boolean(key) 21 | if conf[key] ~= nil then 22 | return conf[key] 23 | end 24 | 25 | local value = skynet.getenv(key) 26 | if value == nil then 27 | return 28 | end 29 | 30 | if value == "true" then 31 | value = true 32 | else 33 | value = false 34 | end 35 | 36 | conf[key] = value 37 | return conf[key] 38 | end 39 | 40 | function M.get_number(key) 41 | if conf[key] ~= nil then 42 | return conf[key] 43 | end 44 | 45 | local value = skynet.getenv(key) 46 | if value == nil then 47 | return 48 | end 49 | 50 | value = tonumber(value) 51 | conf[key] = value 52 | return conf[key] 53 | end 54 | 55 | function M.get_table(key) 56 | local s = M.get(key) 57 | if type(s) == "string" then 58 | local f,err1 = load("return " .. s, "@"..key) 59 | if not f then 60 | error("load config failed:" .. err1) 61 | end 62 | 63 | local ok, err2 = pcall(f) 64 | if not ok then 65 | error("exec config failed:" .. err2) 66 | end 67 | 68 | s = err2 69 | conf[key] = s 70 | end 71 | return s 72 | end 73 | 74 | return M 75 | -------------------------------------------------------------------------------- /lualib/skyext.lua: -------------------------------------------------------------------------------- 1 | -- 扩展skynet功能 2 | require "skynet.manager" 3 | 4 | -- 提前 require sharetable 是为了保证 next 不用下面的next 5 | require "skynet.sharetable" 6 | 7 | -- for orm serialize 8 | local old_next = next 9 | _G.next = function(t, i) 10 | local mt = getmetatable(t) 11 | if mt and mt.__next then 12 | return mt.__next(old_next, t, i) 13 | end 14 | return old_next(t, i) 15 | end 16 | _G.rawnext = old_next 17 | 18 | -- for orm operation 19 | local old_table_unpack = table.unpack 20 | table.unpack = function(t, i, j) 21 | local mt = getmetatable(t) 22 | if mt and mt.__unpack then 23 | return mt.__unpack(t, i, j) 24 | end 25 | return old_table_unpack(t, i, j) 26 | end 27 | 28 | local old_table_concat = table.concat 29 | table.concat = function(t, sep, i, j) 30 | local mt = getmetatable(t) 31 | if mt and mt.__concat then 32 | return mt.__concat(t, sep, i, j) 33 | end 34 | return old_table_concat(t, sep, i, j) 35 | end 36 | 37 | local old_table_insert = table.insert 38 | table.insert = function(t, i, v) 39 | local mt = getmetatable(t) 40 | if mt and mt.__insert then 41 | return mt.__insert(t, i, v) 42 | end 43 | if v == nil then 44 | return old_table_insert(t, i) 45 | else 46 | return old_table_insert(t, i, v) 47 | end 48 | end 49 | 50 | local old_table_remove = table.remove 51 | table.rmove = function(t, i) 52 | local mt = getmetatable(t) 53 | if mt and mt.__remove then 54 | return mt.__remove(t, i) 55 | end 56 | return old_table_remove(t, i) 57 | end 58 | 59 | -- 覆盖 assert error 60 | require "log" 61 | -------------------------------------------------------------------------------- /lualib/event_channel_api.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local mc = require "skynet.multicast" 3 | local log = require "log" 4 | 5 | local M = {} 6 | local g_client_channels = {} 7 | local g_server_channel 8 | 9 | local function channel_new(service, subscribe_cmd) 10 | local up_channel = skynet.call(service, "lua", "GET_EVENT_CHANNEL") 11 | log.info("channel new", "up_channel", up_channel) 12 | local channel = mc.new({ 13 | channel = up_channel, 14 | dispatch = function(channel, source, cmd, ...) 15 | local func = subscribe_cmd[cmd] 16 | if func then 17 | func(...) 18 | else 19 | log.error("unknown subscribe command from channel", "channel", channel, "source", source, "cmd", cmd) 20 | end 21 | end, 22 | }) 23 | channel.subscribe_cmd = subscribe_cmd 24 | channel:subscribe() 25 | return channel 26 | end 27 | 28 | -- client api 29 | function M.subscribe(service, cmd, func) 30 | local channel = g_client_channels[service] 31 | if not channel then 32 | local subscribe_cmd = {} 33 | channel = channel_new(service, subscribe_cmd) 34 | g_client_channels[service] = channel 35 | end 36 | 37 | channel.subscribe_cmd[cmd] = func 38 | end 39 | 40 | -- service api 41 | function M.init(CMD) 42 | g_server_channel = mc.new() 43 | CMD.GET_EVENT_CHANNEL = function () 44 | log.info("GET_EVENT_CHANNEL called", "channel", g_server_channel.channel) 45 | return g_server_channel.channel 46 | end 47 | end 48 | 49 | function M.publish(cmd, ...) 50 | g_server_channel:publish(cmd, ...) 51 | end 52 | 53 | return M 54 | -------------------------------------------------------------------------------- /service/logger/bucket.lua: -------------------------------------------------------------------------------- 1 | local bucket = require "log.bucket" 2 | local log = require "log" 3 | local tinsert = table.insert 4 | 5 | local bucket_mt = {} 6 | bucket_mt.__index = bucket_mt 7 | 8 | function bucket_mt:put(record) 9 | local buckets = self.buckets 10 | if not buckets then 11 | self.default:put(record) 12 | return true 13 | end 14 | 15 | local one_ok = false 16 | for i = 1, #buckets do 17 | local suc = buckets[i]:put(record) 18 | if suc then 19 | one_ok = true 20 | end 21 | end 22 | 23 | -- 一个都没成功,丢到默认桶里 24 | if not one_ok then 25 | self.default:put(record) 26 | return true 27 | end 28 | end 29 | 30 | function bucket_mt:init(config) 31 | assert(not self.buckets, "init repeated") 32 | local buckets = {} 33 | for _, conf in pairs(config) do 34 | log.debug("bucket init", "conf", conf) 35 | local ok, bucket_obj = pcall(bucket.new, conf) 36 | if ok then 37 | tinsert(buckets, bucket_obj) 38 | else 39 | log.error("bucket init failed", "conf", conf.conf, "err", bucket_obj) 40 | return false 41 | end 42 | end 43 | self.buckets = buckets 44 | return true 45 | end 46 | 47 | function bucket_mt:reload() 48 | for _, b in ipairs(self.buckets) do 49 | if b.reload then 50 | b:reload() 51 | end 52 | end 53 | end 54 | 55 | function bucket_mt:close() 56 | for _, b in ipairs(self.buckets) do 57 | if b.close then 58 | b:close() 59 | end 60 | end 61 | end 62 | 63 | local M = {} 64 | 65 | function M.new(config) 66 | local obj = {} 67 | obj.default = bucket.get_default() 68 | obj = setmetatable(obj, bucket_mt) 69 | obj:init(config) 70 | return obj 71 | end 72 | 73 | return M 74 | -------------------------------------------------------------------------------- /test/test_etcd/main.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local log = require "log" 3 | local service = require "skynet.service" 4 | 5 | local function test_etcd_service1() 6 | local skynet = require "skynet" 7 | local config = require "config" 8 | local log = require "log" 9 | local timer = require "timer" 10 | local etcd = require "etcd" 11 | 12 | skynet.start(function() 13 | local etcd_config = config.get_table("etcd_config") 14 | g_etcd_client = etcd.new(etcd_config) 15 | timer.repeat_immediately("test1", 100, function() 16 | local NODE_PREFIX = "/skynet/node/" 17 | local LEASE_TTL = 30 -- 秒 18 | 19 | local r = g_etcd_client:get("/test/a") 20 | log.info("etcd get /test/a", "r", r) 21 | skynet.fork(function() 22 | local ret, err = g_etcd_client:grant(LEASE_TTL) 23 | log.info("etcd grant", "ret", ret, "err", err) 24 | end) 25 | skynet.call(".test-etcd2", "lua", "get") 26 | end) 27 | skynet.register(".test-etcd1") 28 | end) 29 | end 30 | local function test_etcd_service2() 31 | local skynet = require "skynet" 32 | local config = require "config" 33 | local log = require "log" 34 | local cmd_api = require "cmd_api" 35 | local etcd = require "etcd" 36 | 37 | skynet.start(function() 38 | local etcd_config = config.get_table("etcd_config") 39 | g_etcd_client = etcd.new(etcd_config) 40 | local CMD = {} 41 | function CMD.get() 42 | local r = g_etcd_client:get("/test/a") 43 | log.info("etcd get /test/a", "r", r) 44 | end 45 | cmd_api.dispatch(CMD) 46 | skynet.register(".test-etcd2") 47 | end) 48 | end 49 | skynet.start(function() 50 | log.info("Test etcd start") 51 | service.new("test-etcd1", test_etcd_service1) 52 | service.new("test-etcd2", test_etcd_service2) 53 | end) 54 | -------------------------------------------------------------------------------- /app/role/login/client.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local timer = require "timer" 3 | local global = require "global" 4 | local config = require "config" 5 | local log = require "log" 6 | 7 | local M = {} 8 | 9 | local g_clients = {} 10 | 11 | local client = {} 12 | client.__index = client 13 | 14 | local function new_client_obj(conf) 15 | return setmetatable(conf, client) 16 | end 17 | 18 | function client:check_auth() 19 | return self.auth 20 | end 21 | 22 | function client:set_authed(account) 23 | self.account = account 24 | self.auth = true 25 | log.info("client authenticated", "fd", self.fd, "account", account) 26 | end 27 | 28 | function M.unbind(fd) 29 | g_clients[fd] = nil 30 | log.info("login client unbiund", "fd", fd) 31 | end 32 | 33 | function M.new(conf) 34 | local fd = conf.fd 35 | assert(not g_clients[fd], fd .. " exists") 36 | g_clients[fd] = new_client_obj(conf) 37 | log.info("new client created", fd, fd) 38 | local login_timeout_sec = config.get_number("login_timeout_sec") or 60 -- default login timeout 60 seconds 39 | timer.timeout("login_timeout", login_timeout_sec, function() 40 | if not g_clients[fd] then 41 | return 42 | end 43 | 44 | log.error("client auth timeout", "fd", fd) 45 | skynet.call(global.watchdog_service, "lua", "close_client", fd) 46 | end) 47 | return g_clients[fd] 48 | end 49 | 50 | function M.get_obj(fd) 51 | log.debug("get client object", "fd", fd, "obj", g_clients[fd]) 52 | return g_clients[fd] 53 | end 54 | 55 | local noauth_request = { 56 | ["login.login"] = true, 57 | ["login.report_remote_addr"] = true, 58 | } 59 | local function check_auth_cb(request_name, request, fd, client_obj) 60 | if noauth_request[request_name] then 61 | return true 62 | end 63 | 64 | if (not client_obj) or (not client_obj:check_auth()) then 65 | log.warn("unauthorized access", "fd", fd, "request_name", request_name) 66 | return nil, "Unauthorized" 67 | end 68 | return true 69 | end 70 | 71 | M.middleware_cbs = { 72 | check_auth_cb, 73 | } 74 | 75 | return M 76 | -------------------------------------------------------------------------------- /service/mongo_conn.lua: -------------------------------------------------------------------------------- 1 | local skynet = require("skynet") 2 | local cmd_api = require("cmd_api") 3 | local config = require("config") 4 | local mongo = require("skynet.db.mongo") 5 | local util_table = require("util.table") 6 | local bson = require("bson") 7 | local log = require("log") 8 | 9 | local bson_meta = bson.meta 10 | 11 | local g_name, g_index = ... 12 | g_index = tonumber(g_index) 13 | local g_db 14 | local mongo_config = config.get_table("mongo_config") 15 | local service_name = string.format("mongo_conn:%s:%d", g_name, g_index) 16 | 17 | -- log 配置使用 traceback 避免出错打印upvale。 18 | -- upvalue 中包含的请求,会消耗大量的内存 19 | log.config({ 20 | name = service_name, 21 | traceback = debug.traceback, 22 | }) 23 | 24 | local CMD = {} 25 | 26 | local function init_db(dbname) 27 | local db_config = mongo_config[dbname] 28 | assert(db_config, "Database not configured: " .. dbname) 29 | local dbs = mongo.client(db_config.cfg) 30 | g_db = dbs[dbname] 31 | end 32 | 33 | function CMD.find_and_modify(coll, doc) 34 | local col_obj = g_db[coll] 35 | return col_obj:findAndModify(doc) 36 | end 37 | 38 | function CMD.find(coll, doc, projection) 39 | local col_obj = g_db[coll] 40 | local it = col_obj:find(doc, projection) 41 | local all = {} 42 | while it:hasNext() do 43 | local role = it:next() 44 | all[#all + 1] = role 45 | end 46 | return all 47 | end 48 | 49 | function CMD.find_one(coll, doc, projection) 50 | log.debug("find_one", "coll", coll, "doc", doc, "projection", projection) 51 | local col_obj = g_db[coll] 52 | local ret = col_obj:findOne(doc, projection) 53 | for k, v in pairs(ret or {}) do 54 | log.debug("find_one", "k", k, "v", v, "typev", type(v)) 55 | end 56 | log.debug("find_one", "ret", ret) 57 | return ret 58 | end 59 | 60 | function CMD.raw_safe_insert(coll, bson_str) 61 | local col_obj = g_db[coll] 62 | return col_obj:raw_safe_insert(bson_str) 63 | end 64 | 65 | function CMD.raw_safe_update(coll, bson_str) 66 | local col_obj = g_db[coll] 67 | log.debug("raw_safe_update", "coll", coll, "bson_str", bson_str) 68 | return col_obj:raw_safe_update(bson_str) 69 | end 70 | 71 | skynet.start(function() 72 | init_db(g_name) 73 | cmd_api.dispatch(CMD) 74 | end) 75 | -------------------------------------------------------------------------------- /tools/run.lua: -------------------------------------------------------------------------------- 1 | -- lua 脚本工具执行入口文件 2 | 3 | -- 获取当前 run.lua 的所在目录 4 | local function get_script_directory() 5 | local script_path = arg[0] 6 | 7 | if script_path:match(".+%.lua") then 8 | -- 如果 arg[0] 是相对/绝对路径,尝试用 debug 获取真实路径 9 | local info = debug.getinfo(1, "S") 10 | script_path = info.source:sub(2) -- 去掉 '@' 11 | end 12 | 13 | -- 提取目录部分 14 | local script_dir = script_path:match("(.*/)") or "./" 15 | return script_dir 16 | end 17 | 18 | -- 设置路径为全局变量 19 | _G.SCRIPT_DIRECTORY = get_script_directory() 20 | 21 | package.path = package.path .. ";" .. SCRIPT_DIRECTORY .. "../3rd/sproto-orm/tools/sprotodump/?.lua" 22 | package.path = package.path .. ";" .. SCRIPT_DIRECTORY .. "../lualib/?.lua" 23 | package.cpath = package.cpath .. ";" .. SCRIPT_DIRECTORY .. "../skynet/luaclib/?.so" 24 | package.cpath = package.cpath .. ";" .. SCRIPT_DIRECTORY .. "../luaclib/?.so" 25 | 26 | -- 解析命令行参数 27 | local target_script = arg[1] 28 | if not target_script then 29 | print("Usage: lua run.lua [args...]") 30 | os.exit(1) 31 | end 32 | 33 | -- 检查脚本文件是否存在 34 | local function file_exists(name) 35 | local f = io.open(name, "r") 36 | if f then 37 | io.close(f) 38 | return true 39 | else 40 | return false 41 | end 42 | end 43 | 44 | if not file_exists(target_script) then 45 | print("Error: script '" .. target_script .. "' not found.") 46 | os.exit(1) 47 | end 48 | 49 | -- 调整 arg 表:让目标脚本认为自己是主脚本 50 | -- 原始 arg: { run.lua, target.lua, arg1, arg2, ... } 51 | -- 我们要构造一个新的 arg 表给目标脚本:{ target.lua, arg1, arg2, ... } 52 | local new_arg = { [0] = target_script } 53 | for i = 2, #arg do 54 | new_arg[i - 1] = arg[i] 55 | end 56 | 57 | -- 使用 dofile 加载并执行目标脚本,并设置 _G.arg = new_arg 58 | local env = { 59 | arg = new_arg, 60 | dofile = dofile, 61 | package = package, 62 | _G = _G 63 | } 64 | setmetatable(env, { __index = _G }) 65 | 66 | -- 加载并运行目标脚本 67 | local chunk, err = loadfile(target_script, "bt", env) 68 | if not chunk then 69 | print("Error loading script: " .. err) 70 | os.exit(1) 71 | end 72 | 73 | -- 执行脚本 74 | local success, result = pcall(chunk) 75 | if not success then 76 | print("Error running script: " .. result) 77 | os.exit(1) 78 | end 79 | 80 | -------------------------------------------------------------------------------- /app/role/watchdog.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local cmd_api = require "cmd_api" 3 | local log = require "log" 4 | 5 | local CMD = {} 6 | local SOCKET = {} 7 | local g_gate_service 8 | local g_login_service 9 | local g_roleagentmgr_service 10 | local g_clients = {} 11 | 12 | function SOCKET.open(fd, addr) 13 | assert(not g_clients[fd], fd .. " exists") 14 | 15 | log.info("new client", "fd", fd, "addr", addr) 16 | local client = { 17 | fd = fd, 18 | addr = addr, 19 | } 20 | g_clients[fd] = client 21 | skynet.call(g_login_service, "lua", "open", client) 22 | end 23 | 24 | local function close_client(fd) 25 | if not g_clients[fd] then 26 | log.warn("close_client client not found", "fd", fd) 27 | return 28 | end 29 | 30 | local client = g_clients[fd] 31 | g_clients[fd] = nil 32 | 33 | if client.forward_service then 34 | skynet.call(g_gate_service, "lua", "kick", fd) 35 | skynet.send(client.forward_service, "lua", "disconnect", fd) 36 | end 37 | end 38 | 39 | function SOCKET.close(fd) 40 | log.info("socket close", "fd", fd) 41 | close_client(fd) 42 | end 43 | 44 | function SOCKET.error(fd, msg) 45 | log.warn("socket error", "fd", fd, "msg", msg) 46 | close_client(fd) 47 | end 48 | 49 | function SOCKET.warning(fd, size) 50 | -- size K bytes havn't send out in fd 51 | log.warn("socket warning", "fd", fd, "size", size) 52 | end 53 | 54 | function SOCKET.data(fd, msg) 55 | log.warn("socket data why in here", "fd", fd, "size", #msg) 56 | end 57 | 58 | function CMD.start(conf) 59 | skynet.call(g_login_service, "lua", "start", { watchdog = skynet.self(), roleagentmgr = g_roleagentmgr_service }) 60 | skynet.call(g_roleagentmgr_service, "lua", "start", { watchdog = skynet.self() }) 61 | skynet.call(g_gate_service, "lua", "open", conf) 62 | end 63 | 64 | function CMD.close_client(fd) 65 | close_client(fd) 66 | end 67 | 68 | -- Forward the client to service 69 | function CMD.forward(fd, forward_service) 70 | assert(g_clients[fd], "forward: client not found " .. fd) 71 | g_clients[fd].forward_service = forward_service 72 | skynet.call(g_gate_service, "lua", "forward", fd, 0, forward_service) 73 | end 74 | 75 | skynet.start(function() 76 | cmd_api.dispatch_socket(CMD, SOCKET) 77 | 78 | g_gate_service = skynet.newservice("gate") 79 | g_login_service = skynet.newservice("login") 80 | g_roleagentmgr_service = skynet.newservice("roleagentmgr") 81 | end) 82 | -------------------------------------------------------------------------------- /service/sproto_loader.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local md5 = require "md5" 3 | local event_channel_api = require "event_channel_api" 4 | local sprotoloader = require "sprotoloader" 5 | local cmd_api = require "cmd_api" 6 | local log = require "log" 7 | 8 | local CMD = {} 9 | local g_sproto_loaded = {} 10 | 11 | local function readfile(path) 12 | local f = io.input(path) 13 | local result = f:read "a" 14 | f:close() 15 | return result 16 | end 17 | 18 | local function load_sproto_schema(proto, reload) 19 | local schema_path = proto.schema_path 20 | local sproto_index = proto.sproto_index or 1 21 | local proto_loaded = g_sproto_loaded[sproto_index] 22 | if proto_loaded and proto_loaded.schema_path ~= schema_path then 23 | return false, 24 | string.format( 25 | "proto index conflict:%s proto_loaded:%s, proto:%s", 26 | sproto_index, 27 | proto_loaded.schema_path, 28 | schema_path 29 | ) 30 | end 31 | 32 | if proto_loaded and not reload then 33 | return true, proto_loaded 34 | end 35 | 36 | log.info("loading sproto schema", "schema_path", schema_path, "index", sproto_index) 37 | local ok, ret = pcall(readfile, schema_path) 38 | if not ok then 39 | return false, ret 40 | end 41 | 42 | local cs = md5.sumhexa(ret) 43 | if proto_loaded and proto_loaded.checksum == cs then 44 | return true, proto_loaded 45 | end 46 | 47 | ok, ret = pcall(sprotoloader.save, ret, sproto_index) 48 | if not ok then 49 | return false, ret 50 | end 51 | 52 | proto.checksum = cs 53 | g_sproto_loaded[sproto_index] = proto 54 | 55 | event_channel_api.publish(schema_path) 56 | collectgarbage("collect") 57 | return true, proto 58 | end 59 | 60 | local function reload_sproto_schema() 61 | for _, proto in pairs(g_sproto_loaded) do 62 | local ok, errmsg = load_sproto_schema(proto, true) 63 | if not ok then 64 | return false, errmsg 65 | end 66 | end 67 | return true 68 | end 69 | 70 | function CMD.load_proto(schema_path, proto_index) 71 | return load_sproto_schema({ 72 | schema_path = schema_path, 73 | sproto_index = proto_index, 74 | }) 75 | end 76 | 77 | function CMD.reload_proto() 78 | return reload_sproto_schema() 79 | end 80 | 81 | skynet.start(function() 82 | event_channel_api.init(CMD) 83 | cmd_api.dispatch(CMD) 84 | end) 85 | -------------------------------------------------------------------------------- /service/logger/start.lua: -------------------------------------------------------------------------------- 1 | print("fuck", package.path) 2 | local skynet = require "skynet" 3 | local c = require "skynet.core" 4 | local cmd_api = require "cmd_api" 5 | local cmd = require "cmd" 6 | local global = require "global" 7 | local bucket = require "bucket" 8 | local logger = require "log.logger" 9 | local config = require "config" 10 | local ptype = require "ptype" 11 | require "checker" 12 | 13 | local skynet_unpack = skynet.unpack 14 | 15 | local PTYPE_LOG = ptype.PTYPE_LOG 16 | local PTYPE_LOG_ERR = ptype.PTYPE_LOG_ERR 17 | 18 | local kernel_logger = logger.new() 19 | kernel_logger:config({ 20 | name = "kernel", 21 | log_src = false, 22 | }) 23 | 24 | -- 捕捉sighup信号(kill -1) 25 | skynet.register_protocol { 26 | name = "SYSTEM", 27 | id = skynet.PTYPE_SYSTEM, 28 | unpack = function(...) 29 | return ... 30 | end, 31 | dispatch = function() 32 | global.bucket.reload() 33 | kernel_logger:info("SIGHUP") 34 | end, 35 | } 36 | 37 | skynet.register_protocol { 38 | name = "text", 39 | id = skynet.PTYPE_TEXT, 40 | unpack = skynet.tostring, 41 | dispatch = function(_, address, msg) 42 | local found = msg:find("[Ee]rror") 43 | if found then 44 | -- 删除 msg 中的颜色字符 45 | msg = msg:gsub("\x1B%[%d+m", "") 46 | kernel_logger:warn(msg, "address", address) 47 | else 48 | kernel_logger:info(msg, "address", address) 49 | end 50 | end, 51 | } 52 | 53 | local function logger_dispatch_callback(prototype, msg, sz, session, source) 54 | if prototype == PTYPE_LOG then 55 | -- 定时检查服务过载情况, 过载时丢弃LOG日志 56 | if global.log_overload then 57 | return 58 | end 59 | 60 | return global.bucket:put(skynet_unpack(msg, sz)) 61 | elseif prototype == PTYPE_LOG_ERR then 62 | --- ERROR 日志不执行流控 63 | return global.bucket:put(skynet_unpack(msg, sz)) 64 | else 65 | return skynet.dispatch_message(prototype, msg, sz, session, source) 66 | end 67 | end 68 | 69 | local function start_func() 70 | local log_config = config.get_table("log_config") 71 | global.bucket = bucket.new(log_config) 72 | cmd_api.dispatch(cmd) 73 | skynet.register(".logger") 74 | end 75 | 76 | c.callback(logger_dispatch_callback) 77 | skynet.timeout(0, function() 78 | skynet.init_service(function() 79 | local ok, err = pcall(start_func) 80 | if not ok then 81 | print(err) 82 | end 83 | end) 84 | end) 85 | -------------------------------------------------------------------------------- /lualib/user_db_api.lua: -------------------------------------------------------------------------------- 1 | local mongo_conn = require "mongo_conn" 2 | local config = require "config" 3 | local skynet = require "skynet" 4 | local errcode = require "errcode" 5 | local log = require "log" 6 | local time = require "time" 7 | 8 | local M = {} 9 | local g_coll_obj 10 | local g_default_projection = { _id = false } 11 | 12 | function M.get(account, projection) 13 | assert(account) 14 | projection = projection or g_default_projection 15 | log.debug("get account", "account", account, "projection", projection) 16 | return g_coll_obj:find_one({ account = account }, projection) 17 | end 18 | 19 | function M.get_rids(account) 20 | local user = M.get(account, { _id = false, rids = 1 }) 21 | log.debug("get_rids", "account", account, "user", user) 22 | assert(user, account) 23 | return user.rids 24 | end 25 | 26 | function M.add_rid(account, rid) 27 | local update = { 28 | ["$addToSet"] = { 29 | rids = rid, 30 | }, 31 | } 32 | return g_coll_obj:safe_update({account = account}, update) 33 | end 34 | 35 | function M.remove_rid(account, rid) 36 | local update = { 37 | ["$pull"] = { 38 | rids = rid, 39 | }, 40 | } 41 | return g_coll_obj:safe_update({account = account}, update) 42 | end 43 | 44 | function M.create(account) 45 | local obj = { 46 | account = account, 47 | create_time = time.now_ms(), 48 | } 49 | log.debug("begin create account", "account", account, "obj", obj) 50 | local ok, err, r = g_coll_obj:safe_insert(obj) 51 | log.info("end create account", "account", "ok", ok, "err", err, "r", r) 52 | if not ok then 53 | return false, err, r 54 | end 55 | return obj 56 | end 57 | 58 | function M.ensure_get_user(account) 59 | -- 已有账号,直接返回 60 | local user = M.get(account) 61 | if user then 62 | return user 63 | end 64 | 65 | -- 创建账号 66 | local err, r 67 | user, err, r = M.create(account) 68 | if user then 69 | return user 70 | end 71 | 72 | -- 创建失败,如果是因为并发创建,则重试读取 73 | local werror = r.writeErrors 74 | if werror and werror[1].code == errcode.MONGO_DUPLICATE_KEY then 75 | return M.get(account) 76 | end 77 | log.error("get_user failed", "account", account, "err", err, "r", r) 78 | end 79 | 80 | skynet.init(function() 81 | local name = config.get("user_db_name") 82 | local coll = config.get("user_db_coll") 83 | log.info("user_db_api init", "db", name, "coll", coll) 84 | g_coll_obj = mongo_conn.get_collection(name, coll) 85 | end) 86 | 87 | return M 88 | -------------------------------------------------------------------------------- /lualib/orm/schema_define.lua: -------------------------------------------------------------------------------- 1 | -- Code generated from schema/bag.sproto schema/mail.sproto schema/role.sproto 2 | -- DO NOT EDIT! 3 | return { 4 | bag = { 5 | res = { 6 | key = "integer", 7 | type = "map", 8 | value = "resource" 9 | }, 10 | res_type = { 11 | type = "integer" 12 | } 13 | }, 14 | mail = { 15 | attach = { 16 | type = "mail_attach" 17 | }, 18 | cfg_id = { 19 | type = "integer" 20 | }, 21 | detail = { 22 | key = "string", 23 | type = "map", 24 | value = "string" 25 | }, 26 | mail_id = { 27 | type = "integer" 28 | }, 29 | send_role = { 30 | type = "mail_role" 31 | }, 32 | send_time = { 33 | type = "integer" 34 | }, 35 | title = { 36 | key = "string", 37 | type = "map", 38 | value = "string" 39 | } 40 | }, 41 | mail_attach = { 42 | res_id = { 43 | type = "integer" 44 | }, 45 | res_size = { 46 | type = "integer" 47 | }, 48 | res_type = { 49 | type = "integer" 50 | } 51 | }, 52 | mail_role = { 53 | name = { 54 | type = "string" 55 | }, 56 | rid = { 57 | type = "integer" 58 | } 59 | }, 60 | resource = { 61 | res_id = { 62 | type = "integer" 63 | }, 64 | res_size = { 65 | type = "integer" 66 | } 67 | }, 68 | role = { 69 | _version = { 70 | type = "integer" 71 | }, 72 | account = { 73 | type = "string" 74 | }, 75 | create_time = { 76 | type = "integer" 77 | }, 78 | game = { 79 | type = "string" 80 | }, 81 | last_login_time = { 82 | type = "integer" 83 | }, 84 | modules = { 85 | type = "role_modules" 86 | }, 87 | name = { 88 | type = "string" 89 | }, 90 | rid = { 91 | type = "integer" 92 | }, 93 | server = { 94 | type = "string" 95 | } 96 | }, 97 | role_bag = { 98 | bags = { 99 | key = "integer", 100 | type = "map", 101 | value = "bag" 102 | } 103 | }, 104 | role_mail = { 105 | _version = { 106 | type = "integer" 107 | }, 108 | mails = { 109 | key = "integer", 110 | type = "map", 111 | value = "mail" 112 | } 113 | }, 114 | role_modules = { 115 | bag = { 116 | type = "role_bag" 117 | }, 118 | mail = { 119 | type = "role_mail" 120 | } 121 | }, 122 | str2str = { 123 | key = { 124 | type = "string" 125 | }, 126 | value = { 127 | type = "string" 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /lualib/role_db_api.lua: -------------------------------------------------------------------------------- 1 | local mongo_conn = require "mongo_conn" 2 | local user_db_api = require "user_db_api" 3 | local skynet = require "skynet" 4 | local config = require "config" 5 | local log = require "log" 6 | 7 | local M = {} 8 | local g_coll_obj 9 | local g_default_projection = { _id = false, rid = 1, server = 1 } 10 | 11 | -- 从数据库读取角色列表 12 | function M.get_roles(account, query, projection) 13 | if projection then 14 | projection._id = false 15 | else 16 | projection = g_default_projection 17 | end 18 | 19 | local rids = user_db_api.get_rids(account) 20 | local ret = {} 21 | if not rids or #rids == 0 then 22 | return ret 23 | end 24 | 25 | query = query or {} 26 | if #rids == 1 then 27 | local rid = rids[1] 28 | query["rid"] = rid 29 | log.debug("get_roles", "account", account, "rid", rid) 30 | local data = g_coll_obj:find_one(query, projection) 31 | if data then 32 | ret[1] = data 33 | else 34 | log.info("role not found by account", "account", account, "rid", rid) 35 | end 36 | log.debug("get_roles", "account", account, "rid", rid, "ret", ret) 37 | return ret 38 | end 39 | 40 | query["rid"] = { ["$in"] = rids } 41 | return g_coll_obj:find(query, projection) 42 | end 43 | 44 | function M.has_role(rid, account, server) 45 | local projection = g_default_projection 46 | local query = { 47 | rid = rid, 48 | } 49 | local data = g_coll_obj:find_one(query, projection) 50 | if data and data.account == account and data.server == server then 51 | return true 52 | end 53 | return true 54 | end 55 | 56 | function M.create(rid, account, data) 57 | local ok, err = user_db_api.add_rid(account, rid) 58 | if not ok then 59 | log.error("create role failed by add rid", "account", account, "rid", rid, "err", err) 60 | return false 61 | end 62 | data = data or {} 63 | data.rid = rid 64 | data.account = account 65 | data._version = 0 66 | ok, err = g_coll_obj:safe_insert(data) 67 | if not ok then 68 | log.warn("create role failed", "account", account, "rid", rid, "err", err) 69 | assert(user_db_api.remove_rid(account, rid)) 70 | return false 71 | end 72 | 73 | log.info("create role success", "account", account, "rid", rid) 74 | return true 75 | end 76 | 77 | skynet.init(function() 78 | local name = config.get("role_db_name") 79 | local coll = config.get("role_db_coll") 80 | log.info("role_db_api init", "db", name, "coll", coll) 81 | g_coll_obj = mongo_conn.get_collection(name, coll) 82 | end) 83 | 84 | return M 85 | -------------------------------------------------------------------------------- /tools/etcd/docker-compose.yml: -------------------------------------------------------------------------------- 1 | networks: 2 | etcd-net: 3 | driver: bridge 4 | 5 | volumes: 6 | etcd1-data: 7 | etcd2-data: 8 | etcd3-data: 9 | 10 | services: 11 | etcd1: 12 | image: gcr.io/etcd-development/etcd:v3.6.5 13 | container_name: etcd1 14 | ports: 15 | - "2379:2379" 16 | - "2380:2380" 17 | volumes: 18 | - etcd1-data:/etcd-data 19 | networks: 20 | - etcd-net 21 | command: 22 | - /usr/local/bin/etcd 23 | - --name=etcd1 24 | - --data-dir=/etcd-data 25 | - --auth-token=simple 26 | - --listen-client-urls=http://0.0.0.0:2379 27 | - --advertise-client-urls=http://etcd1:2379 28 | - --listen-peer-urls=http://0.0.0.0:2380 29 | - --initial-advertise-peer-urls=http://etcd1:2380 30 | - --initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380 31 | - --initial-cluster-token=tkn 32 | - --initial-cluster-state=new 33 | - --log-level=info 34 | - --logger=zap 35 | - --log-outputs=stderr 36 | 37 | etcd2: 38 | image: gcr.io/etcd-development/etcd:v3.6.5 39 | container_name: etcd2 40 | ports: 41 | - "2378:2379" 42 | - "2376:2380" 43 | volumes: 44 | - etcd2-data:/etcd-data 45 | networks: 46 | - etcd-net 47 | command: 48 | - /usr/local/bin/etcd 49 | - --name=etcd2 50 | - --data-dir=/etcd-data 51 | - --listen-client-urls=http://0.0.0.0:2379 52 | - --advertise-client-urls=http://etcd2:2379 53 | - --listen-peer-urls=http://0.0.0.0:2380 54 | - --initial-advertise-peer-urls=http://etcd2:2380 55 | - --initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380 56 | - --initial-cluster-token=tkn 57 | - --initial-cluster-state=new 58 | - --log-level=info 59 | - --logger=zap 60 | - --log-outputs=stderr 61 | 62 | etcd3: 63 | image: gcr.io/etcd-development/etcd:v3.6.5 64 | container_name: etcd3 65 | ports: 66 | - "2377:2379" 67 | - "2374:2380" 68 | volumes: 69 | - etcd3-data:/etcd-data 70 | networks: 71 | - etcd-net 72 | command: 73 | - /usr/local/bin/etcd 74 | - --name=etcd3 75 | - --data-dir=/etcd-data 76 | - --listen-client-urls=http://0.0.0.0:2379 77 | - --advertise-client-urls=http://etcd3:2379 78 | - --listen-peer-urls=http://0.0.0.0:2380 79 | - --initial-advertise-peer-urls=http://etcd3:2380 80 | - --initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380 81 | - --initial-cluster-token=tkn 82 | - --initial-cluster-state=new 83 | - --log-level=info 84 | - --logger=zap 85 | - --log-outputs=stderr 86 | 87 | -------------------------------------------------------------------------------- /lualib/cluster_discovery.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local queue = require "skynet.queue" 3 | local event_channel_api = require "event_channel_api" 4 | local log = require "log" 5 | 6 | local M = {} 7 | 8 | local g_cluster_discovery_service 9 | 10 | -- 注册服务 11 | function M.register(services) 12 | assert(g_cluster_discovery_service, "cluster_discovery service not initialized") 13 | skynet.send(g_cluster_discovery_service, "lua", "register", services) 14 | end 15 | 16 | -- 注销服务 17 | function M.unregister(services) 18 | assert(g_cluster_discovery_service, "cluster_discovery service not initialized") 19 | skynet.send(g_cluster_discovery_service, "lua", "unregister", services) 20 | end 21 | 22 | local g_services_cache = {} -- service -> service_obj 23 | local service_mt = {} 24 | service_mt.__index = service_mt 25 | 26 | local function get_service_obj(service) 27 | local obj = g_services_cache[service] 28 | if not obj then 29 | obj = setmetatable({ 30 | service = service, 31 | lock = queue(), 32 | }, service_mt) 33 | g_services_cache[service] = obj 34 | log.info("cluster_discovery create service object", "service", service) 35 | end 36 | obj:ensure_init() 37 | return obj 38 | end 39 | 40 | function service_mt:ensure_init() 41 | if not self.inited then 42 | self.lock(self.init, self) 43 | end 44 | end 45 | 46 | function service_mt:init() 47 | if self.inited then 48 | return 49 | end 50 | 51 | local nodes = skynet.call(g_cluster_discovery_service, "lua", "query_service", self.service) 52 | self.inited = true 53 | self:update(nodes) 54 | log.info("cluster_discovery service init", "service", self.service, "nodes", self.nodes) 55 | end 56 | 57 | function service_mt:update(nodes) 58 | self.nodes = nodes 59 | end 60 | 61 | -- 随机一个节点 62 | function M.random(service) 63 | local service_obj = get_service_obj(service) 64 | local nodes = service_obj.nodes 65 | if #nodes == 0 then 66 | return nil 67 | end 68 | local node = nodes[math.random(#nodes)] 69 | return node 70 | end 71 | 72 | -- 所有节点 73 | function M.nodes(service) 74 | local service_obj = get_service_obj(service) 75 | return service_obj.nodes 76 | end 77 | 78 | -- 订阅服务变更 79 | local function on_service_change(service, nodes) 80 | log.info("on_service_change", "service", service, "nodes", nodes) 81 | 82 | local service_obj = g_services_cache[service] 83 | if not service_obj then 84 | return 85 | end 86 | service_obj:update(nodes) 87 | end 88 | 89 | skynet.init(function() 90 | g_cluster_discovery_service = skynet.uniqueservice("cluster_discovery") 91 | event_channel_api.subscribe(g_cluster_discovery_service, "service_change", on_service_change) 92 | end) 93 | 94 | return M 95 | -------------------------------------------------------------------------------- /lualib/jwt.lua: -------------------------------------------------------------------------------- 1 | local cjson = require "cjson" 2 | local crypto = require "crypto" 3 | local time = require "time" 4 | 5 | local cjson_decode = cjson.decode 6 | local cjson_encode = cjson.encode 7 | local base64urldecode = crypto.base64urldecode 8 | local base64urlencode = crypto.base64urlencode 9 | 10 | local M = {} 11 | 12 | local supported_alg = { 13 | HS256 = crypto.hmac_sha256, 14 | HS512 = crypto.hmac_sha512, 15 | } 16 | 17 | -- 验证 JWT token 18 | function M.verify(token, secret) 19 | -- 1. 分割 token 20 | local segments = {} 21 | for segment in string.gmatch(token, "[^%.]+") do 22 | table.insert(segments, segment) 23 | end 24 | 25 | if #segments ~= 3 then 26 | return nil, "invalid token format" 27 | end 28 | 29 | -- 2. 解码 header 和 payload 30 | local ok, header = pcall(cjson_decode, base64urldecode(segments[1])) 31 | if not ok or type(header) ~= "table" then 32 | return nil, "invalid header" 33 | end 34 | if header.typ ~= "JWT" then 35 | return nil, "invalid type" 36 | end 37 | local hmac_func = supported_alg[header.alg] 38 | if not hmac_func then 39 | return nil, "unsupport alg" 40 | end 41 | local ok, payload = pcall(cjson_decode, base64urldecode(segments[2])) 42 | if not ok or type(payload) ~= "table" then 43 | return nil, "invalid payload" 44 | end 45 | 46 | -- 3. 验证签名 47 | local signing_input = segments[1] .. "." .. segments[2] 48 | local signature = base64urldecode(segments[3]) 49 | local expected_sig = hmac_func(secret, signing_input) 50 | 51 | if signature ~= expected_sig then 52 | return nil, "invalid signature" 53 | end 54 | 55 | -- 4. 验证时间 56 | local now = time.now() 57 | if payload.nbf and now < payload.nbf then 58 | return nil, "token not yet valid" 59 | end 60 | if payload.iat and payload.iat > now then 61 | return nil, "invalid issued time" 62 | end 63 | if payload.exp and now >= payload.exp then 64 | return nil, "token expired" 65 | end 66 | return payload 67 | end 68 | 69 | -- 生成 JWT token 70 | function M.sign(payload, secret, alg, exp_secs) 71 | alg = alg or "HS256" 72 | local hmac_func = supported_alg[alg] 73 | if not hmac_func then 74 | return nil, "unsupport alg" 75 | end 76 | 77 | -- header 固定结构 78 | local header = { 79 | typ = "JWT", 80 | alg = alg, 81 | } 82 | 83 | -- 加入 iat 和 exp 84 | local now = time.now() 85 | payload.iat = payload.iat or now 86 | if exp_secs then 87 | payload.exp = payload.iat + exp_secs 88 | end 89 | 90 | -- 编码 91 | local header_b64 = base64urlencode(cjson_encode(header)) 92 | local payload_b64 = base64urlencode(cjson_encode(payload)) 93 | 94 | -- 签名 95 | local signing_input = header_b64 .. "." .. payload_b64 96 | local signature = hmac_func(secret, signing_input) 97 | local sig_b64 = base64urlencode(signature) 98 | 99 | return signing_input .. "." .. sig_b64 100 | end 101 | 102 | return M 103 | -------------------------------------------------------------------------------- /tools/gen_roleagent_modules.lua: -------------------------------------------------------------------------------- 1 | local lfs = require "lfs" 2 | 3 | local function fetch_modules() 4 | local modules = {} 5 | local modules_path = SCRIPT_DIRECTORY .. "../service/game/roleagent/modules" 6 | for file in lfs.dir(modules_path) do 7 | if file ~= "." and file ~= ".." then 8 | local f = modules_path .. "/" .. file 9 | local attr = lfs.attributes(f) 10 | assert(type(attr) == "table") 11 | if attr.mode == "directory" then 12 | local request_file = f .. "/request.lua" 13 | if lfs.attributes(request_file) then 14 | modules[#modules + 1] = file 15 | end 16 | end 17 | end 18 | end 19 | return modules 20 | end 21 | 22 | -- 排序遍历表的键值对(按键排序) 23 | local function pairs_by_key(t, sort_func) 24 | local keys = {} 25 | for k in pairs(t) do 26 | table.insert(keys, k) 27 | end 28 | 29 | -- 默认按升序排序(支持字符串或数字 key) 30 | table.sort(keys, sort_func or function(a, b) 31 | return a < b 32 | end) 33 | 34 | -- 返回迭代器 35 | local i = 0 36 | return function() 37 | i = i + 1 38 | local k = keys[i] 39 | if k then 40 | return k, t[k] 41 | end 42 | end 43 | end 44 | 45 | local function write_file(path, data, mode) 46 | local handle = io.open(path, mode) 47 | handle:write(data) 48 | handle:close() 49 | end 50 | 51 | local function interp(s, tab) 52 | return (s:gsub("($%b{})", function(w) 53 | return tab[w:sub(3, -2)] or w 54 | end)) 55 | end 56 | 57 | local sformat = string.format 58 | 59 | local requires = {} 60 | local inits = {} 61 | local mod_loads = {} 62 | 63 | local modules = fetch_modules() 64 | for _, m in pairs_by_key(modules) do 65 | requires[#requires + 1] = interp([[local ${m}_request = require "modules.${m}.request"]], { m = m }) 66 | inits[#inits + 1] = interp([[ sproto_api.register_module("${m}", client, ${m}_request)]], { m = m }) 67 | 68 | if m ~= "role" then 69 | requires[#requires + 1] = interp([[local ${m} = require "modules.${m}"]], { m = m }) 70 | mod_loads[#mod_loads + 1] = interp( 71 | [[ 72 | if role_data.modules.${m} == nil then 73 | role_data.modules.${m} = {} 74 | end 75 | role_obj.modules.${m} = ${m}.new(role_obj, role_data.modules.${m}) 76 | ]], 77 | { m = m } 78 | ) 79 | end 80 | end 81 | 82 | local requires_str = table.concat(requires, "\n") 83 | local head = sformat( 84 | [[ 85 | -- Code generated from tools/gen_roleagent_modules.lua 86 | -- DO NOT EDIT! 87 | 88 | local schema = require "orm.schema" 89 | local sproto_api = require "sproto_api" 90 | %s 91 | 92 | local M = {} 93 | ]], 94 | requires_str 95 | ) 96 | 97 | local inits_str = table.concat(inits, "\n") 98 | local body = sformat( 99 | [[ 100 | function M.init(client) 101 | %s 102 | end 103 | 104 | ]], 105 | inits_str 106 | ) 107 | 108 | local mod_loads_str = table.concat(mod_loads, "\n") 109 | local load_mod = sformat( 110 | [[ 111 | function M.load(role_obj, role_data) 112 | if role_data.modules == nil then 113 | role_data.modules = {} 114 | end 115 | 116 | role_obj.modules = {} 117 | 118 | %send 119 | ]], 120 | mod_loads_str 121 | ) 122 | 123 | local foot = [[ 124 | return M 125 | ]] 126 | 127 | local output_filename = SCRIPT_DIRECTORY .. "../service/game/roleagent/modules/init.lua" 128 | local content = head .. body .. load_mod .. foot 129 | write_file(output_filename, content, "w") 130 | -------------------------------------------------------------------------------- /lualib/id_generator.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local snowflake = require "snowflake" 3 | 4 | local M = {} 5 | 6 | -- 缓存配置 7 | local CACHE_CAPACITY = 1000 -- 缓存容量 8 | local REFILL_THRESHOLD = 0.2 -- 补充阈值(20%) 9 | local REFILL_SIZE = CACHE_CAPACITY -- 每次补充数量 10 | 11 | -- ID缓存(使用哈希表) 12 | local id_cache = {} 13 | local cache_size = 0 14 | 15 | -- 补充状态 16 | local is_refilling = false 17 | 18 | -- 获取缓存中的ID数量 19 | local function get_cache_size() 20 | return cache_size 21 | end 22 | 23 | -- 检查是否需要补充缓存 24 | local function should_refill() 25 | return get_cache_size() <= (CACHE_CAPACITY * REFILL_THRESHOLD) 26 | end 27 | 28 | -- 补充缓存 29 | local function refill_cache() 30 | -- 避免重复补充 31 | if is_refilling then 32 | return 33 | end 34 | 35 | is_refilling = true 36 | 37 | local function do_refill() 38 | -- 批量获取新ID 39 | local ok, new_ids = pcall(M.raw_newids, REFILL_SIZE) 40 | if not ok then 41 | skynet.error("refill_cache failed:", new_ids) 42 | is_refilling = false 43 | return 44 | end 45 | 46 | -- 添加到缓存中 47 | for _, id in ipairs(new_ids) do 48 | id_cache[id] = true 49 | cache_size = cache_size + 1 50 | end 51 | 52 | skynet.error(string.format("Cache refilled, current size: %d", cache_size)) 53 | is_refilling = false 54 | end 55 | 56 | -- 异步执行补充,避免阻塞 57 | skynet.fork(do_refill) 58 | end 59 | 60 | -- 从缓存中获取一个ID 61 | local function get_cached_id() 62 | -- 获取任意一个ID 63 | for id in pairs(id_cache) do 64 | id_cache[id] = nil 65 | cache_size = cache_size - 1 66 | 67 | -- 检查是否需要补充缓存 68 | if should_refill() then 69 | refill_cache() 70 | end 71 | 72 | return id 73 | end 74 | 75 | -- 缓存为空,直接从原始接口获取 76 | return M.raw_newid() 77 | end 78 | 79 | -- 从缓存中获取多个ID 80 | local function get_cached_ids(count) 81 | local result = {} 82 | local found = 0 83 | 84 | -- 先从现有缓存中获取 85 | for id in pairs(id_cache) do 86 | table.insert(result, id) 87 | id_cache[id] = nil 88 | cache_size = cache_size - 1 89 | found = found + 1 90 | if found >= count then 91 | break 92 | end 93 | end 94 | 95 | local remaining = count - found 96 | 97 | -- 如果还需要更多ID 98 | if remaining > 0 then 99 | -- 检查是否需要补充缓存 100 | if should_refill() then 101 | refill_cache() 102 | end 103 | 104 | -- 直接从原始接口获取剩余的ID 105 | local new_ids = M.raw_newids(remaining) 106 | for _, id in ipairs(new_ids) do 107 | table.insert(result, id) 108 | end 109 | end 110 | 111 | -- 检查是否需要补充缓存 112 | if should_refill() then 113 | refill_cache() 114 | end 115 | 116 | return result 117 | end 118 | 119 | -- 原始接口 120 | function M.raw_newid() 121 | return snowflake.newid() 122 | end 123 | 124 | function M.raw_newids(count) 125 | return snowflake.newids(count) 126 | end 127 | 128 | -- 公开接口 129 | function M.newid() 130 | return get_cached_id() 131 | end 132 | 133 | function M.newids(count) 134 | return get_cached_ids(count) 135 | end 136 | 137 | -- 获取缓存状态(用于监控) 138 | function M.get_cache_status() 139 | return { 140 | size = cache_size, 141 | capacity = CACHE_CAPACITY, 142 | is_refilling = is_refilling, 143 | should_refill = should_refill(), 144 | } 145 | end 146 | 147 | -- 初始化缓存 148 | skynet.init(function() 149 | refill_cache() 150 | end) 151 | 152 | return M 153 | -------------------------------------------------------------------------------- /etc/common.conf.lua: -------------------------------------------------------------------------------- 1 | -- path config 2 | root = "./" 3 | 4 | luaservice = root .. "app/".. start .. "/?.lua;" 5 | luaservice = luaservice .. root .. "app/?/main.lua;" 6 | luaservice = luaservice .. root .. "app/" .. start .. "/?/main.lua;" 7 | luaservice = luaservice .. root .. "service/?.lua;" 8 | luaservice = luaservice .. root .. "service/?/main.lua;" 9 | luaservice = luaservice .. root .. "skynet/service/?.lua;" 10 | 11 | lua_path = root .. "?.lua;" 12 | lua_path = lua_path .. root .. "lualib/?.lua;" 13 | lua_path = lua_path .. root .. "lualib/?/init.lua;" 14 | lua_path = lua_path .. root .. "skynet/lualib/?.lua;" 15 | 16 | lua_cpath = root .. "luaclib/?.so;" 17 | lua_cpath = lua_cpath .. root .. "skynet/luaclib/?.so;" 18 | 19 | cpath = root .. "skynet/cservice/?.so;" 20 | snax = root .. "service/?.lua;" 21 | 22 | lualoader = root .. "lualib/loader.lua" 23 | preload = root .. "lualib/preload.lua" 24 | 25 | -- core config 26 | thread = 8 27 | bootstrap = "snlua bootstrap" -- The service for bootstrap 28 | harbor = 0 -- disable master-slave mode 29 | 30 | -- log config 31 | logger = "logger" 32 | logservice = "snlua" 33 | bootfaillogpath = "logs/bootfail.log" -- 启动失败的日志文件 34 | log_overload_mqlen = 1000000 -- 日志过载队列长度 35 | log_src = true -- 日志是否打印代码位置 36 | log_print_table = true -- 日志是否打印table内容 37 | log_level = 4 -- 日志等级 DEBUG = 4, INFO = 3, WARN = 2, ERROR = 1, FATAL = 0 38 | log_config = log_config 39 | or [[ 40 | { 41 | { 42 | name = "file", 43 | filename = "logs/skyext.log", 44 | split = "size", -- size/line/day/hour 45 | maxline = 10000, -- 每个文件最大行数 line split 有效 46 | maxsize = "100M", -- 每个文件最大尺寸 size split 有效 47 | }, 48 | { 49 | name = "console", 50 | } 51 | } 52 | ]] 53 | 54 | etcd_config = [[ 55 | { 56 | http_host = { 57 | "http://127.0.0.1:2379", 58 | "http://127.0.0.1:2378", 59 | "http://127.0.0.1:2377", 60 | }, 61 | --user = "root", 62 | --password = "123456", 63 | } 64 | ]] 65 | 66 | mongo_config = [[ 67 | { 68 | center = { 69 | connections = 4, -- 连接数 70 | cfg = { 71 | host = "127.0.0.1", 72 | port = 27017, 73 | username = nil, 74 | password = nil, 75 | authdb = nil, 76 | }, 77 | collections = { 78 | gid = { 79 | indexes = { 80 | { "name", unique = true, background = true }, 81 | }, 82 | }, 83 | user = { 84 | indexes = { 85 | { "account", unique = true, background = true }, 86 | }, 87 | }, 88 | }, 89 | }, 90 | role = { 91 | connections = 4, -- 连接数 92 | cfg = { 93 | host = "127.0.0.1", 94 | port = 27017, 95 | username = nil, 96 | password = nil, 97 | authdb = nil, 98 | }, 99 | collections = { 100 | role = { 101 | indexes = { 102 | { "rid", unique = true, background = true }, 103 | { "account", "server", background = true }, 104 | }, 105 | }, 106 | }, 107 | }, 108 | } 109 | ]] 110 | 111 | -- other config 112 | sproto_index = 1 113 | sproto_schema_path = "build/proto/sproto.spb" 114 | proto_checksum_enable = true -- 是否检查 proto 协议 115 | 116 | -- 最大角色数量 117 | max_role_count = 5 118 | 119 | user_db_name = "center" 120 | user_db_coll = "user" 121 | 122 | role_db_name = "role" 123 | role_db_coll = "role" 124 | 125 | db_save_interval = 3 * 60 -- 数据入库间隔秒 126 | 127 | login_jwt_secret = "your_access_secret" -- 登录用的 jwt 密钥 128 | 129 | -- 客户端看到到服务器与游戏服的映射 130 | server2game = [[ 131 | { 132 | ["s1"]="game1", 133 | ["s2"]="game2", 134 | ["s3"]="game2", -- game3 合并到 game2 之后 135 | } 136 | ]] 137 | 138 | http_request_body_size = 1024 * 1024 139 | -------------------------------------------------------------------------------- /app/role/roleagent/main.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local cmd_api = require "cmd_api" 3 | local client = require "client" 4 | local modules = require "modules" 5 | local rolemgr = require "rolemgr" 6 | local global = require "global" 7 | local distributed_lock = require "distributed_lock" 8 | local rolenode_api = require "rolenode_api" 9 | local errcode = require "errcode" 10 | local roleagent_api = require "roleagent_api" 11 | local cluster_discovery = require "cluster_discovery" 12 | local cluster = require "skynet.cluster" 13 | local log = require "log" 14 | 15 | local g_roleagent_index = ... 16 | local g_agent_name = roleagent_api.format_agent_name(g_roleagent_index) 17 | local g_agent_service_name = roleagent_api.format_agent_service_name(g_roleagent_index) 18 | 19 | log.config { 20 | name = g_agent_name, 21 | } 22 | 23 | local CMD = {} 24 | 25 | local function lockkey(rid) 26 | return "roleagent/" .. rid 27 | end 28 | 29 | local function lock_expired_cb(lock_info) 30 | local value = lock_info.value 31 | local rid = value.rid 32 | rolemgr.unload_role(rid) 33 | log.info("lock expired", "rid", rid, "node", value.rolenode) 34 | end 35 | 36 | function CMD.load_bind_role(rid, fd) 37 | log.info("load_bind_role begin", "rid", rid, "fd", fd) 38 | 39 | -- 检查 rid 是否应该在本节点 40 | local rolenode = rolenode_api.calc_rolenode(rid) 41 | if rolenode ~= rolenode_api.self_rolenode() then 42 | log.warn("role not on this node", "rid", rid, "node", rolenode) 43 | return errcode.NOT_THIS_NODE, rolenode 44 | end 45 | -- 加锁 46 | local ok, lockvalue = distributed_lock.try_lock(lockkey(rid), { rolenode = rolenode, rid = rid }, lock_expired_cb) 47 | if not ok then 48 | if not lockvalue then 49 | log.warn("failed to lock lockvalue is nil", "rid", rid) 50 | return errcode.LOCAK_FAILED 51 | end 52 | 53 | log.info("role locked other node", "rid", rid, "node", lockvalue.rolenode) 54 | -- 通知对方卸载角色 55 | local agent_name = roleagent_api.calc_agent_name(rid) 56 | local ret = cluster.call(lockvalue.rolenode, agent_name, "unload_role", rid) 57 | if not ret then 58 | log.error("other node failed to unload role", "rid", rid, "node", lockvalue.rolenode) 59 | return errcode.IN_OTHER_NODE 60 | end 61 | -- 再次尝试获取锁 62 | ok, lockvalue = distributed_lock.try_lock(lockkey(rid), { rolenode = rolenode, rid = rid }, lock_expired_cb) 63 | if not ok then 64 | if not lockvalue then 65 | log.warn("failed to lock again", "rid", rid) 66 | return errcode.LOCAK_FAILED 67 | end 68 | 69 | log.error("role still in other role", "rid", rid, "role", lockvalue.rolenode) 70 | return errcode.IN_OTHER_NODE 71 | end 72 | end 73 | local role_obj = rolemgr.load_role(rid) 74 | if role_obj.fd then 75 | log.info("role already bound", "rid", rid, "fd", role_obj.fd) 76 | client.unbind_kick(role_obj.fd) 77 | end 78 | client.bind(fd, role_obj) 79 | skynet.call(global.watchdog_service, "lua", "forward", fd, skynet.self()) 80 | return 0 81 | end 82 | 83 | function CMD.unload_role(rid) 84 | rolemgr.unload_role(rid) 85 | return true 86 | end 87 | 88 | function CMD.disconnect(fd) 89 | log.info("closing client", "fd", fd) 90 | client.unbind(fd) 91 | end 92 | 93 | function CMD.start(conf) 94 | global.watchdog_service = conf.watchdog 95 | global.roleagentmgr_service = conf.roleagentmgr 96 | log.info("roleagent service started", "agent", g_agent_name) 97 | end 98 | 99 | skynet.start(function() 100 | modules.init(client) 101 | cmd_api.dispatch(CMD) 102 | cluster_discovery.register({ g_agent_service_name }) 103 | log.info("roleagent service init", "agent", g_agent_service_name) 104 | skynet.register(g_agent_service_name) 105 | end) 106 | -------------------------------------------------------------------------------- /app/role/login/login_request.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local global = require "global" 3 | local role_db_api = require "role_db_api" 4 | local user_db_api = require "user_db_api" 5 | local client = require "client" 6 | local roleagent_api = require "roleagent_api" 7 | local errcode = require "errcode" 8 | local config = require "config" 9 | local log = require "log" 10 | local jwt = require "jwt" 11 | local sproto_api = require "sproto_api" 12 | local id = require "id_generator" 13 | 14 | local M = {} 15 | 16 | local login_jwt_secret = config.get("login_jwt_secret") 17 | local proto_checksum_enable = config.get_boolean("proto_checksum_enable") 18 | local server2game = config.get_table("server2game") 19 | 20 | function M:report_remote_addr(fd, client_obj) 21 | log.info("report_remote_addr", "fd", fd, "remote_addr", self.remote_addr, "local_addr", self.local_addr) 22 | if not client_obj then 23 | log.error( 24 | "report_remote_addr client not found", 25 | "fd", 26 | fd, 27 | "remote_addr", 28 | self.remote_addr, 29 | "local_addr", 30 | self.local_addr 31 | ) 32 | return 33 | end 34 | cient_obj.addr = self.remote_addr 35 | end 36 | 37 | local function load_bind_rold(fd, rid) 38 | -- TODO: use timeout call 39 | local agent_addr = roleagent_api.calc_agent_addr(rid) 40 | local code, rolenode = skynet.call(agent_addr, "lua", "load_bind_role", rid, fd) 41 | if code ~= 0 then 42 | log.warn("load_bind_role failed", "rid", rid, "fd", fd, "ok", ok) 43 | return { 44 | code = code, 45 | rolenode = rolenode, 46 | } 47 | end 48 | 49 | -- 跟 login 解绑 50 | client.unbind(fd) 51 | 52 | log.info("load_bind_role success", "rid", rid, "fd", fd) 53 | 54 | -- 绑定成功后,后面的消息会转发到roleagent 55 | return { 56 | code = errcode.OK, 57 | rid = rid, 58 | } 59 | end 60 | 61 | function M:login(fd, client_obj) 62 | local data, err = jwt.verify(self.token, login_jwt_secret) 63 | if not data then 64 | log.warn("jwt failed", "err", err, "token", self.token) 65 | return { 66 | code = errcode.TOKEN_ERROR, 67 | } 68 | end 69 | 70 | local account = data.account 71 | if proto_checksum_enable then 72 | -- 协议不一致不允许登录 73 | local proto_checksum = sproto_api.get_sproto_info().checksum 74 | if self.proto_checksum ~= proto_checksum then 75 | log.warn( 76 | "proto checksum failed", 77 | "client", 78 | self.proto_checksum, 79 | "server", 80 | proto_checksum, 81 | "account", 82 | account 83 | ) 84 | return { 85 | code = errcode.PROTO_CHECKSUM, 86 | } 87 | end 88 | end 89 | 90 | log.info("login", "account", account) 91 | -- 如果是首次进来,会创建用户 user 92 | local user = user_db_api.ensure_get_user(account) 93 | if not user then 94 | log.warn("login ensure_get_user failed", "account", account) 95 | return { 96 | code = errcode.DB_ERROR, 97 | } 98 | end 99 | 100 | local rid = self.rid 101 | if (not rid) or (rid <= 0) then 102 | log.warn("login rid not exist", "account", account) 103 | return { 104 | code = errcode.ROLE_NOT_EXIST, 105 | } 106 | end 107 | 108 | -- 检查服务器是否存在 109 | local server = self.server 110 | if not server2game[server] then 111 | return { 112 | code = errcode.SERVER_NOT_EXIST, 113 | } 114 | end 115 | -- 加载 role 并 绑定 fd 116 | return load_bind_rold(fd, rid) 117 | end 118 | 119 | function M:logout(fd, client_obj) 120 | log.info("logout", "fd", fd) 121 | skynet.call(global.watchdog_service, "lua", "close_client", fd) 122 | end 123 | 124 | return M 125 | -------------------------------------------------------------------------------- /lualib/log/formatter.lua: -------------------------------------------------------------------------------- 1 | local time = require "time" 2 | local log_level = require "log.log_level" 3 | 4 | local pairs = pairs 5 | local tconcat = table.concat 6 | local sformat = string.format 7 | 8 | local colors = { 9 | Black = 30, 10 | Red = 31, 11 | Green = 32, 12 | Yellow = 33, 13 | Blue = 34, 14 | Magenta = 91, 15 | Cyan = 36, 16 | Default = 39, -- 默认颜色 17 | LightRed = 91, 18 | White = 97, 19 | } 20 | 21 | local function color_seq(color) 22 | return sformat("\x1b[%dm", color) 23 | end 24 | 25 | local color_reset = color_seq(0) 26 | 27 | local level_desc = { 28 | [log_level.FATAL] = { name = "FATAL", color = color_seq(colors.LightRed), simple_name = "F" }, 29 | [log_level.ERROR] = { name = "ERROR", color = color_seq(colors.Red), simple_name = "E" }, 30 | [log_level.WARN] = { name = "WARN", color = color_seq(colors.Yellow), simple_name = "W" }, 31 | [log_level.INFO] = { name = "INFO", color = "", simple_name = "I" }, 32 | [log_level.DEBUG] = { name = "DEBUG", color = color_seq(colors.Cyan), simple_name = "D" }, 33 | } 34 | 35 | local M = {} 36 | 37 | local last_time, last_time_str 38 | local function format_time(timestamp) 39 | local sec = timestamp // 1 40 | local ms = timestamp * 1000 % 1000 41 | 42 | local f 43 | if sec == last_time then 44 | f = last_time_str 45 | else 46 | f = time.format(sec) 47 | last_time_str = f 48 | last_time = sec 49 | end 50 | return sformat("%s.%03d", f, ms) 51 | end 52 | 53 | local function level_to_string(level) 54 | local desc = level_desc[level] 55 | return desc.name 56 | end 57 | 58 | local json_encoder 59 | local grecord = {} 60 | local function json_format_record(record) 61 | local rec = grecord 62 | rec.module = record.module 63 | rec.level = level_to_string(record.level) 64 | rec.line = record.line 65 | rec.msg = record.msg 66 | for _, event in pairs(record.events) do 67 | rec[event[1]] = event[2] 68 | end 69 | rec.time = format_time(record.timestamp) 70 | return json_encoder(rec) 71 | end 72 | 73 | local F = {} 74 | 75 | function F.json(record) 76 | json_encoder = json_encoder or require("cjson.safe").encode 77 | return json_format_record(record) 78 | end 79 | 80 | local function default_logmt(record) 81 | local desc = level_desc[record.level] 82 | local date = format_time(record.timestamp) 83 | local short_log_level = desc.simple_name 84 | local mod = record.module 85 | local line = record.line 86 | local msg = record.msg 87 | local events_tbl = {} 88 | for _, event in pairs(record.events) do 89 | events_tbl[#events_tbl+1] = event[1] .. ":" .. event[2] 90 | end 91 | local events_str = tconcat(events_tbl, " ") 92 | return sformat("[%s %s] %s%s: msg:%s %s", date, short_log_level, mod, line, msg, events_str) 93 | end 94 | 95 | function F.text(record, style) 96 | if not level_desc[record.level] then 97 | error("log level not exist, level: " .. record.level) 98 | end 99 | return style(record) 100 | end 101 | 102 | local function colorify(msg, record) 103 | local desc = level_desc[record.level] 104 | if not desc then 105 | error("log level not exist, level: " .. record.level) 106 | end 107 | local color_beg = desc.color 108 | local color_end = color_reset 109 | return sformat("%s%s%s", color_beg, msg, color_end) 110 | end 111 | 112 | -- text format 可配置 style 函数, 参考 default_logmt 113 | function M.get_formatter(format, color, style) 114 | ---返回值 115 | ---@return msg string format后的字符串 116 | format = format or "text" 117 | local logfmt = assert(F[format], format) 118 | style = style or default_logmt 119 | return function(record) 120 | local msg = logfmt(record, style) 121 | if color then 122 | return colorify(msg, record) 123 | else 124 | return msg 125 | end 126 | end 127 | end 128 | 129 | return M 130 | -------------------------------------------------------------------------------- /app/account/lualib/account_router.lua: -------------------------------------------------------------------------------- 1 | local log = require "log" 2 | local config = require "config" 3 | local jwt = require "jwt" 4 | local role_db_api = require "role_db_api" 5 | local id = require "id_generator" 6 | local errcode = require "errcode" 7 | local time = require "time" 8 | local rolenode_api = require "rolenode_api" 9 | 10 | local login_jwt_secret = config.get("login_jwt_secret") 11 | local server2game = config.get_table("server2game") 12 | local max_role_count = config.get_number("max_role_count") or 5 13 | 14 | local M = { 15 | GET = {}, 16 | POST = {}, 17 | } 18 | 19 | local function get_roles(account, query) 20 | return role_db_api.get_roles(account, query, { _id = false, rid = 1, server = 1, name = 1 }) 21 | end 22 | 23 | M.GET["/roles"] = function(req, res) 24 | log.debug("begin get roles", req, res) 25 | local q = req.parse_query() 26 | 27 | local data, err = jwt.verify(q.token, login_jwt_secret) 28 | if not data then 29 | log.warn("jwt failed", "err", err, "token", q.token) 30 | return res.write_json({ 31 | code = errcode.TOKEN_ERROR, 32 | }) 33 | end 34 | 35 | local account = data.account 36 | local query = {} 37 | local server = q.server 38 | if server and server ~= "" then 39 | -- 检查服务器是否存在 40 | if not server2game[server] then 41 | return res.write_json({ 42 | code = errcode.SERVER_NOT_EXIST, 43 | }) 44 | end 45 | query.server = server 46 | end 47 | local roles = get_roles(account, query) 48 | log.debug("get_roles", "token", token, "account", account, "roles", roles) 49 | for _, role in pairs(roles) do 50 | local rolenode = rolenode_api.calc_rolenode(role.rid) 51 | role.rolenode = rolenode 52 | end 53 | 54 | return res.write_json({ 55 | code = errcode.OK, 56 | roles = roles, 57 | }) 58 | end 59 | 60 | M.POST["/create_role"] = function(req, res) 61 | log.debug("begin get roles", req, res) 62 | local b = req.read_json() 63 | local data, err = jwt.verify(b.token, login_jwt_secret) 64 | if not data then 65 | log.warn("jwt failed", "err", err, "token", b.token) 66 | return res.write_json({ 67 | code = errcode.TOKEN_ERROR, 68 | }) 69 | end 70 | 71 | -- 检查 account 的角色数量 72 | local account = data.account 73 | local server = b.server 74 | local query = { 75 | server = server, 76 | } 77 | local rids = role_db_api.get_roles(account, query) 78 | if #rids > max_role_count then 79 | log.warn("create_role too many roles for account", "account", account, "max", max_role_count) 80 | return res.write_json({ 81 | code = errcode.ROLE_TOO_MANY, 82 | }) 83 | end 84 | local game = server2game[server] 85 | if not game then 86 | return res.write_json({ 87 | code = errcode.SERVER_NOT_EXIST, 88 | }) 89 | end 90 | 91 | -- TODO: 限制单服角色数量 92 | 93 | -- TODO: 同名检测(全服同名还是单服同名?) 94 | 95 | -- 创建新的角色 96 | local name = b.name 97 | local data = { 98 | server = server, 99 | game = game, 100 | name = name, 101 | create_time = time.now_ms(), 102 | } 103 | local rid = id.newid() -- 分配唯一id 104 | local ret = role_db_api.create(rid, account, data) 105 | if not ret then 106 | log.error("create_role role_db_api.create failed", "account", account, "name", name) 107 | return res.write_json({ 108 | code = errcode.DB_ERROR, 109 | }) 110 | end 111 | 112 | query.rid = rid 113 | local roles = get_roles(account, query) 114 | log.debug("create_role get_roles", "token", token, "account", account, "roles", roles) 115 | if #roles ~= 1 then 116 | log.error("create_role get_roles failed", "token", token, "account", account, "roles", roles) 117 | return res.write_json({ 118 | code = errcode.DB_ERROR, 119 | }) 120 | end 121 | local role = roles[1] 122 | local rolenode = rolenode_api.calc_rolenode(rid) 123 | role.rolenode = rolenode 124 | 125 | return res.write_json({ 126 | code = errcode.OK, 127 | role = role, 128 | }) 129 | end 130 | 131 | return M 132 | -------------------------------------------------------------------------------- /lualib/mongo_conn.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local queue = require "skynet.queue" 3 | local config = require "config" 4 | local bson = require "bson" 5 | local log = require "log" 6 | local orm = require "orm" 7 | 8 | local to_lightuserdata = bson.to_lightuserdata 9 | 10 | local M = {} 11 | 12 | local mongo_config = config.get_table("mongo_config") 13 | 14 | -- 处理 orm 的情况 15 | local _bson_encode = bson.encode 16 | local _with_bson_encode_context = orm.with_bson_encode_context 17 | local function bson_encode(doc) 18 | return _with_bson_encode_context(_bson_encode, doc) 19 | end 20 | 21 | local conn_mt = {} 22 | conn_mt.__index = conn_mt 23 | 24 | -- fuck tail call 25 | local function call_ret(...) 26 | return ... 27 | end 28 | 29 | function conn_mt:call(cmd, ...) 30 | return call_ret(skynet.call(self.addr, "lua", cmd, ...)) 31 | end 32 | 33 | local conn = {} 34 | function conn.new(name, index) 35 | local service_name = string.format("mongo_conn:%s:%d", name, index) 36 | local service_addr = skynet.uniqueservice(service_name, name, index) 37 | log.info("mongo_conn new", "service_name", service_name, "service_addr", service_addr) 38 | return setmetatable({ name = service_name, addr = service_addr }, conn_mt) 39 | end 40 | 41 | local coll_mt = {} 42 | coll_mt.__index = coll_mt 43 | 44 | function coll_mt:find_and_modify(doc) 45 | local conn_obj = self.db:_route() 46 | return conn_obj:call("find_and_modify", self.coll, doc) 47 | end 48 | 49 | function coll_mt:find(doc, projection) 50 | local conn_obj = self.db:_route() 51 | return conn_obj:call("find", self.coll, doc, projection) 52 | end 53 | 54 | function coll_mt:find_one(doc, projection) 55 | local conn_obj = self.db:_route() 56 | return conn_obj:call("find_one", self.coll, doc, projection) 57 | end 58 | 59 | -- 兼容 orm 数据打包,必须在调用者服务执行 bson_encode 60 | function coll_mt:safe_insert(doc) 61 | local conn_obj = self.db:_route() 62 | log.debug("safe_insert doc:", "doc", doc) 63 | local bson_obj = bson_encode(doc) 64 | return conn_obj:call("raw_safe_insert", self.coll, to_lightuserdata(bson_obj)) 65 | end 66 | 67 | function coll_mt:safe_update(query, update, upsert, multi) 68 | local conn_obj = self.db:_route() 69 | log.debug("safe_update", "query", query, "update", update, "upsert", upsert, "multi", multi) 70 | local bson_obj = bson_encode({ 71 | q = query, 72 | u = update, 73 | upsert = upsert, 74 | multi = multi, 75 | }) 76 | return conn_obj:call("raw_safe_update", self.coll, to_lightuserdata(bson_obj)) 77 | end 78 | 79 | local db_mt = {} 80 | db_mt.__index = db_mt 81 | 82 | function db_mt:get_collection(coll) 83 | local collection = self.collections[coll] 84 | if not collection then 85 | collection = setmetatable({ 86 | db = self, 87 | coll = coll, 88 | }, coll_mt) 89 | self.collections[coll] = collection 90 | end 91 | return collection 92 | end 93 | 94 | function db_mt:_route() 95 | local index = math.random(1, #self.conns) 96 | return self.conns[index] 97 | end 98 | 99 | function db_mt:init() 100 | local db_config = mongo_config[self.name] 101 | for i = 1, db_config.connections do 102 | local conn_obj = conn.new(self.name, i) 103 | table.insert(self.conns, conn_obj) 104 | end 105 | end 106 | 107 | local db = {} 108 | function db.new(name) 109 | return setmetatable({ 110 | name = name, 111 | collections = {}, 112 | conns = {}, 113 | }, db_mt) 114 | end 115 | 116 | local g_dbs = {} 117 | local g_db_locks = {} 118 | 119 | local function _get_db(name) 120 | local db_obj = g_dbs[name] 121 | if db_obj then 122 | return db_obj 123 | end 124 | 125 | db_obj = db.new(name) 126 | db_obj:init() 127 | g_dbs[name] = db_obj 128 | return db_obj 129 | end 130 | 131 | local function get_db(name) 132 | local db_obj = g_dbs[name] 133 | if db_obj then 134 | return db_obj 135 | end 136 | 137 | local lock = g_db_locks[name] 138 | if not lock then 139 | lock = queue() 140 | g_db_locks[name] = lock 141 | end 142 | 143 | return lock(_get_db, name) 144 | end 145 | 146 | function M.get_collection(name, coll) 147 | local db_obj = get_db(name) 148 | return db_obj:get_collection(coll) 149 | end 150 | 151 | return M 152 | -------------------------------------------------------------------------------- /lualib/util/table.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | -- Save copied tables in `copies`, indexed by original table. 4 | function M.deepcopy(orig, copies) 5 | copies = copies or {} 6 | local orig_type = type(orig) 7 | local copy 8 | if orig_type == "table" then 9 | if copies[orig] then 10 | copy = copies[orig] 11 | else 12 | copy = {} 13 | for orig_key, orig_value in next, orig, nil do 14 | copy[M.deepcopy(orig_key, copies)] = M.deepcopy(orig_value, copies) 15 | end 16 | copies[orig] = copy 17 | end 18 | else -- number, string, boolean, etc 19 | copy = orig 20 | end 21 | return copy 22 | end 23 | 24 | function M.keys(data) 25 | local copy = {} 26 | local idx = 1 27 | for k, _ in pairs(data) do 28 | copy[idx] = k 29 | idx = idx + 1 30 | --table.insert(copy, k) 31 | end 32 | return copy 33 | end 34 | 35 | function M.size(data) 36 | if type(data) ~= "table" then 37 | return 0 38 | end 39 | local cnt = 0 40 | for _ in pairs(data) do 41 | cnt = cnt + 1 42 | end 43 | return cnt 44 | end 45 | 46 | -- not deep copy 47 | function M.values(data) 48 | local copy = {} 49 | local idx = 1 50 | for _, v in pairs(data) do 51 | copy[idx] = v 52 | idx = idx + 1 53 | end 54 | return copy 55 | end 56 | 57 | function M.clear(t) 58 | for k, _ in pairs(t) do 59 | t[k] = nil 60 | end 61 | end 62 | 63 | function M.merge(t1, t2) 64 | for k, v in pairs(t2) do 65 | t1[k] = v 66 | end 67 | end 68 | 69 | --------------------------------- 70 | -- table.tostring(tbl) 71 | --------------------------------- 72 | -- fork from http://lua-users.org/wiki/TableUtils 73 | local gsub = string.gsub 74 | local match = string.match 75 | local function append_result(result, ...) 76 | local n = select("#", ...) 77 | for i = 1, n do 78 | result.i = result.i + 1 79 | result[result.i] = select(i, ...) 80 | end 81 | end 82 | 83 | local function val_to_str(v, result, seen) 84 | local tp = type(v) 85 | if "string" == tp then 86 | v = gsub(v, "\n", "\\n") 87 | if match(gsub(v, "[^'\"]", ""), '^"+$') then 88 | append_result(result, "'", v, "'") 89 | else 90 | v = gsub(v, '"', '\\"') 91 | append_result(result, '"', v, '"') 92 | end 93 | elseif "table" == tp then 94 | if seen[v] then 95 | append_result(result, '""') 96 | return 97 | end 98 | 99 | seen[v] = true 100 | if getmetatable(v) and getmetatable(v).__tostring then 101 | append_result(result, tostring(v)) 102 | else 103 | M.tostring_tbl(v, result, seen) 104 | end 105 | elseif "function" == tp then 106 | append_result(result, '"', tostring(v), '"') 107 | else 108 | append_result(result, tostring(v)) 109 | end 110 | end 111 | 112 | local function key_to_str(k, result, seen) 113 | if "string" == type(k) and match(k, "^[_%a][_%a%d]*$") then 114 | append_result(result, k) 115 | else 116 | append_result(result, "[") 117 | val_to_str(k, result, seen) 118 | append_result(result, "]") 119 | end 120 | end 121 | 122 | M.tostring_tbl = function(tbl, result, seen) 123 | if not result.i then 124 | result.i = 0 125 | end 126 | -- 这是一个硬限制,防止结果字符串过大,保持不变 127 | local MAX_STR_TBL_CNT = 1024 * 1024 -- result has 1M element 128 | 129 | append_result(result, "{") 130 | local first = true 131 | for k, v in pairs(tbl) do 132 | if result.i > MAX_STR_TBL_CNT then 133 | append_result(result, "...,") -- 添加一个截断标记 134 | break 135 | end 136 | if not first then 137 | append_result(result, ",") 138 | end 139 | key_to_str(k, result, seen) 140 | append_result(result, "=") 141 | val_to_str(v, result, seen) 142 | first = false 143 | end 144 | append_result(result, "}") 145 | end 146 | 147 | M.concat_tostring_tbl = function(result) 148 | result.i = nil 149 | return table.concat(result, "") 150 | end 151 | 152 | M.tostring = function(tbl) 153 | local result = {} 154 | result.i = 0 155 | local seen = {} -- 初始化访问记录表 156 | val_to_str(tbl, result, seen) 157 | return M.concat_tostring_tbl(result) 158 | end 159 | 160 | M.in_array = function(tbl, check_value) 161 | for k, v in pairs(tbl) do 162 | if v == check_value then 163 | return k 164 | end 165 | end 166 | end 167 | 168 | return M 169 | -------------------------------------------------------------------------------- /app/robot/robotagent/main.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local sproto_api = require "sproto_api" 3 | local cmd_api = require "cmd_api" 4 | local socket = require "skynet.socket" 5 | local config = require "config" 6 | local log = require "log" 7 | local jwt = require "jwt" 8 | local httpc = require "httpc" 9 | local cjson = require "cjson.safe" 10 | local errcode = require "errcode" 11 | 12 | local CMD = {} 13 | local g_fd = nil 14 | 15 | local g_session = 0 16 | 17 | local login_jwt_secret = config.get("login_jwt_secret") 18 | local gate_nodes = config.get_table("gate_nodes") 19 | 20 | local function new_session() 21 | g_session = g_session + 1 22 | return g_session 23 | end 24 | 25 | local function call(name, param) 26 | local ret = sproto_api.call(g_fd, name, param, new_session()) 27 | return ret 28 | end 29 | 30 | function CMD.start(conf) 31 | local data = { 32 | account = "robot3", 33 | } 34 | local token = jwt.sign(data, login_jwt_secret, "HS512", 60) 35 | log.info("generate token", "token", token) 36 | 37 | local account_host = config.get("account_host") 38 | local status, body = httpc.get(account_host, "/roles?token=" .. token) 39 | if status ~= 200 then 40 | log.error("failed to get roles", "status", status, "body", body) 41 | return 42 | end 43 | local res = cjson.decode(body) 44 | if res.code ~= errcode.OK then 45 | log.error("failed to get roles", "res", res) 46 | return 47 | end 48 | 49 | local roles = res.roles 50 | if #roles == 0 then 51 | local req = { 52 | token = token, 53 | server = "s1", 54 | name = "robot3", 55 | } 56 | local status, res = httpc.post_json(account_host, "/create_role", req) 57 | if status ~= 200 then 58 | log.error("failed to create role", "status", status, "res", res) 59 | return 60 | end 61 | if res.code ~= errcode.OK then 62 | log.error("failed to create role", "res", res) 63 | return 64 | end 65 | roles = { res.role } 66 | end 67 | 68 | if #roles == 0 then 69 | log.error("no roles available") 70 | return 71 | end 72 | 73 | local role = roles[1] 74 | local rolenode = role.rolenode 75 | local gate_ip = gate_nodes[rolenode].ip 76 | local gate_port = gate_nodes[rolenode].port 77 | local fd, err = socket.open(gate_ip, gate_port) 78 | if not fd then 79 | log.error("failed to connect to gate", "err", err) 80 | return 81 | end 82 | g_fd = fd 83 | log.info("connected to gate", "ip", gate_ip, "port", gate_port) 84 | 85 | local param = { 86 | token = token, 87 | rid = role.rid, 88 | server = "s1", 89 | proto_checksum = sproto_api.get_sproto_info().checksum, 90 | } 91 | local ret = call("login.login", param) 92 | log.info("login response", "ret", ret) 93 | local ret = call("role.login_info") 94 | log.info("login_info response", "ret", ret) 95 | end 96 | 97 | local function unpack_package(text) 98 | local size = #text 99 | if size < 2 then 100 | return nil, text 101 | end 102 | local s = text:byte(1) * 256 + text:byte(2) 103 | if size < s + 2 then 104 | return nil, text 105 | end 106 | 107 | return text:sub(3, 2 + s), text:sub(3 + s) 108 | end 109 | 110 | local function recv_package(last) 111 | local result 112 | result, last = unpack_package(last) 113 | if result then 114 | return result, last 115 | end 116 | local r = socket.read(g_fd) 117 | if not r then 118 | return nil, last 119 | end 120 | if r == "" then 121 | error "Server closed" 122 | end 123 | return recv_package(last .. r) 124 | end 125 | 126 | skynet.start(function() 127 | cmd_api.dispatch(CMD) 128 | skynet.fork(function() 129 | local host = sproto_api.get_sproto_host() 130 | local last = "" 131 | while true do 132 | if g_fd then 133 | v, last = recv_package(last) 134 | if not v then 135 | log.error("socket read error:", sz) 136 | break 137 | end 138 | local type, request_name, request, response_cb = host:dispatch(v) 139 | if type then 140 | local ret = sproto_api.raw_dispatch(g_fd, type, request_name, request, response_cb) 141 | if ret then 142 | log.debug("dispatched message response", "ret", ret) 143 | end 144 | else 145 | log.error("failed to dispatch message") 146 | end 147 | end 148 | skynet.sleep(10) -- Sleep to avoid busy loop 149 | end 150 | end) 151 | end) 152 | -------------------------------------------------------------------------------- /lualib/snowflake.lua: -------------------------------------------------------------------------------- 1 | local function snowflake_service() 2 | local log = require "log" 3 | local skynet = require "skynet" 4 | local cmd_api = require "cmd_api" 5 | local config = require "config" 6 | 7 | local function parse_date(str_date) 8 | local pattern = "(%d+)-(%d+)-(%d+)" 9 | local y, m, d = str_date:match(pattern) 10 | if not y or not m or not d then 11 | error("invalid date format") 12 | end 13 | return os.time({ year = y, month = m, day = d, hour = 0 }) 14 | end 15 | 16 | -- 时间起点 17 | local start_date = config.get("snowflake_start_date") or "2025-10-09" 18 | local BEGIN_TIMESTAMP = parse_date(start_date) 19 | 20 | -- 每一部分占用的位数 (总共 1 + 63 = 64 位,符号位为0) 21 | local TIME_BIT = 32 -- 136年 22 | local MACHINE_BIT = 14 -- 16384个进程 23 | local SEQUENCE_BIT = 17 -- 13万/秒 24 | -- 位数检查 25 | assert(TIME_BIT + MACHINE_BIT + SEQUENCE_BIT < 64, "total bits exceeds 64") 26 | 27 | -- 每一部分的最大值 28 | local MAX_SEQUENCE = (1 << SEQUENCE_BIT) - 1 29 | local MAX_MACHINE_ID = (1 << MACHINE_BIT) - 1 30 | local MAX_RELATIVE_TIME = (1 << TIME_BIT) - 1 31 | 32 | -- 每一部分向左的位移 33 | local MACHINE_LEFT = SEQUENCE_BIT 34 | local TIMESTAMP_LEFT = SEQUENCE_BIT + MACHINE_BIT 35 | 36 | local MACHINE_ID 37 | local sequence = 0 38 | local last_timestamp = -1 39 | local is_inited = false 40 | 41 | -- 获取当前时间,单位 1s 42 | local function get_cur_timestamp() 43 | return math.floor(skynet.time()) 44 | end 45 | 46 | local function generate_one() 47 | local cur_timestamp = get_cur_timestamp() 48 | -- 检查时钟回拨 49 | if cur_timestamp < last_timestamp then 50 | error("clock moved backwards") 51 | end 52 | 53 | if cur_timestamp == last_timestamp then 54 | sequence = sequence + 1 55 | if sequence > MAX_SEQUENCE then 56 | log.warn("sequence overflow", "sequence", sequence, "cur_timestamp", cur_timestamp) 57 | -- 等待下一个时间单位 58 | repeat 59 | skynet.sleep(50) -- sleep 0.5s 60 | cur_timestamp = get_cur_timestamp() 61 | until cur_timestamp > last_timestamp 62 | sequence = 0 63 | end 64 | else 65 | -- 新的时间单位,序列号重置 66 | sequence = 0 67 | end 68 | 69 | last_timestamp = cur_timestamp 70 | local relative_timestamp = cur_timestamp - BEGIN_TIMESTAMP 71 | -- 修复: 检查相对时间戳是否溢出 72 | assert(relative_timestamp <= MAX_RELATIVE_TIME, "timestamp is out of range, over 174 years") 73 | 74 | -- 组合 ID 75 | return (relative_timestamp << TIMESTAMP_LEFT) | (MACHINE_ID << MACHINE_LEFT) | sequence 76 | end 77 | 78 | local CMD = {} 79 | function CMD.newid() 80 | if not is_inited then 81 | error("snowflake service is not initialized yet") 82 | end 83 | return generate_one() 84 | end 85 | 86 | -- 返回一个顺序数组 { id1, id2, ... } 87 | function CMD.newids(count) 88 | if not is_inited then 89 | error("snowflake service is not initialized yet") 90 | end 91 | 92 | count = tonumber(count) or 1 93 | if count < 1 then 94 | count = 1 95 | end 96 | if count > 10000 then -- 可根据业务场景修改 97 | error("count is too large") 98 | end 99 | 100 | local list = {} 101 | for i = 1, count do 102 | list[i] = generate_one() 103 | end 104 | return list 105 | end 106 | 107 | skynet.start(function() 108 | MACHINE_ID = config.get_number("snowflake_machine_id") 109 | -- 检查机器ID 110 | assert( 111 | MACHINE_ID and MACHINE_ID >= 0 and MACHINE_ID <= MAX_MACHINE_ID, 112 | "invalid machine_id = " .. tostring(MACHINE_ID) 113 | ) 114 | 115 | -- 初始化 last_timestamp,避免首次启动时钟回拨误判 116 | last_timestamp = get_cur_timestamp() 117 | is_inited = true -- 标记初始化完成 118 | log.info( 119 | "snowflake service started", 120 | "machine_id", 121 | MACHINE_ID, 122 | "begin_timestamp", 123 | BEGIN_TIMESTAMP, 124 | "last_timestamp", 125 | last_timestamp 126 | ) 127 | 128 | cmd_api.dispatch(CMD) 129 | end) 130 | end 131 | 132 | local service = require "skynet.service" 133 | local skynet = require "skynet" 134 | 135 | local M = {} 136 | 137 | local snowflake_service_addr = nil 138 | skynet.init(function() 139 | snowflake_service_addr = service.new("snowflake", snowflake_service) 140 | end) 141 | 142 | function M.newid() 143 | return skynet.call(snowflake_service_addr, "lua", "newid") 144 | end 145 | 146 | -- 批量获取ID接口 147 | function M.newids(count) 148 | return skynet.call(snowflake_service_addr, "lua", "newids", count) 149 | end 150 | 151 | return M 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 游戏服务器 skyext 2 | 3 | 🎮 [WIP] [skyext](https://github.com/hanxi/skyext) 是一个基于 [skynet](https://github.com/cloudwu/skynet) 实现的分布式游戏服务器框架 4 | 5 | 🚀 欢迎 Star & Fork! 6 | 7 | 目前还处于早期开发中,仅用于学习参考! 8 | 9 | ## ✨ 特性 10 | 11 | - **分布式架构**: 基于 skynet 实现的高性能分布式游戏服务器 12 | - **微服务设计**: 支持 account 节点和 role 节点分离部署 13 | - **ORM 支持**: 内置 ORM 框架,支持 MongoDB 数据持久化 14 | - **协议管理**: 使用 sproto 协议,支持协议版本校验 15 | - **服务发现**: 基于 etcd 的服务注册与发现 16 | - **JWT 认证**: 支持 JWT token 登录验证 17 | - **日志系统**: 完善的日志记录和管理系统 18 | - [ ] **热更新**: 支持代码热更新和配置热加载 19 | - [ ] **监控系统**: 完善监控告警系统 20 | - [ ] **管理后台**: 完善管理后台,包括火焰图分析 21 | 22 | ## 🏗️ 架构设计 23 | 24 | ### 节点类型 25 | 26 | - **Account 节点**: 负责用户账号管理、登录验证、角色创建等 27 | - **Role 节点**: 负责角色数据管理、游戏逻辑处理 28 | - **Robot 节点**: 机器人客户端,用于压力测试 29 | 30 | ### 核心模块 31 | 32 | - **ORM 系统**: `lualib/orm/` - 对象关系映射,支持数据版本管理 33 | - **服务发现**: `lualib/cluster_discovery.lua` - 基于 etcd 的集群发现 34 | - **数据库管理**: `lualib/dbmgr.lua` - MongoDB 连接池管理 35 | - **协议处理**: `lualib/sproto_api.lua` - sproto 协议编解码 36 | - **HTTP 服务**: `lualib/http_server/` - HTTP 服务器实现 37 | - **分布式锁**: `lualib/distributed_lock.lua` - 基于 etcd 的分布式锁 38 | 39 | ## 📋 依赖环境 40 | 41 | - **skynet**: 游戏服务器框架 42 | - **etcd**: 服务发现和配置管理 43 | - **MongoDB**: 数据持久化存储 44 | - **libsodium**: 加密库 45 | - **lua-cjson**: JSON 处理 46 | - **sproto**: 协议序列化 47 | 48 | ## 🚀 快速开始 49 | 50 | ### 1. 初始化项目 51 | 52 | ```bash 53 | # 克隆项目 54 | git clone https://github.com/hanxi/skyext.git 55 | cd skyext 56 | 57 | # 初始化子模块 58 | make init 59 | 60 | # 编译项目 61 | make build 62 | ``` 63 | 64 | ### 2. 启动基础服务 65 | 66 | 启动 etcd 集群: 67 | 68 | ```bash 69 | cd tools/etcd 70 | docker compose up -d 71 | ``` 72 | 73 | 启动 MongoDB: 74 | 75 | ```bash 76 | cd tools/mongodb 77 | docker compose up -d 78 | ``` 79 | 80 | ### 3. 生成协议和模式文件 81 | 82 | ```bash 83 | # 生成协议文件 84 | make proto 85 | 86 | # 生成 ORM 模式文件 87 | make schema 88 | 89 | # 生成代码 90 | make autocode 91 | ``` 92 | 93 | ### 4. 启动游戏服务器 94 | 95 | ```bash 96 | # 启动 account 节点(支持多实例) 97 | ./bin/skynet etc/account1.conf.lua 98 | ./bin/skynet etc/account2.conf.lua 99 | 100 | # 启动 role 节点(支持多实例) 101 | ./bin/skynet etc/role1.conf.lua 102 | ./bin/skynet etc/role2.conf.lua 103 | ``` 104 | 105 | ### 5. 启动机器人测试 106 | 107 | ```bash 108 | # 启动机器人客户端进行测试 109 | ./bin/skynet etc/robot.conf.lua 110 | ``` 111 | 112 | ## 📁 目录结构 113 | 114 | ``` 115 | skyext/ 116 | ├── app/ # 应用程序入口 117 | │ ├── account/ # 账号服务 118 | │ ├── role/ # 角色服务 119 | │ └── robot/ # 机器人客户端 120 | ├── etc/ # 配置文件 121 | ├── lualib/ # Lua 库文件 122 | │ ├── orm/ # ORM 框架 123 | │ ├── http_server/ # HTTP 服务器 124 | │ └── util/ # 工具库 125 | ├── proto/ # 协议定义文件 126 | ├── schema/ # 数据模式定义 127 | ├── service/ # 服务模块 128 | ├── tools/ # 工具脚本 129 | └── test/ # 测试用例 130 | ``` 131 | 132 | ## ⚙️ 配置说明 133 | 134 | ### 主要配置文件 135 | 136 | - `etc/common.conf.lua`: 通用配置(日志、数据库、etcd 等) 137 | - `etc/account*.conf.lua`: Account 节点配置 138 | - `etc/role*.conf.lua`: Role 节点配置 139 | - `etc/robot.conf.lua`: 机器人客户端配置 140 | 141 | ### config 配置模块 142 | 143 | - 支持 string, number, boolean, table 类型的配置 144 | - table 类型的配置需要用 `[[` 和 `]]` 包裹起来 145 | - 配置语法见 [skynet 官方文档 Config](https://github.com/cloudwu/skynet/wiki/Config) 146 | - 详见 [config 模块](lualib/config.lua) 147 | 148 | ## 🔧 开发工具 149 | 150 | ```bash 151 | # 清理编译文件 152 | make clean 153 | 154 | # 完全清理 155 | make cleanall 156 | 157 | # 打包发布 158 | make dist 159 | 160 | # 查看所有可用命令 161 | make help 162 | ``` 163 | 164 | ## 🎮 游戏功能 165 | 166 | ### 已实现功能 167 | 168 | - **用户系统**: 账号注册、登录、JWT 认证 169 | - **角色系统**: 角色创建、数据持久化、模块化设计 170 | - **服务发现**: 动态服务注册与发现 171 | 172 | ### 扩展模块 173 | 174 | 项目采用模块化设计,可以轻松扩展新功能: 175 | 176 | - 在 `schema/` 目录定义数据结构 177 | - 在 `proto/roleagent/` 目录定义协议 178 | - 使用 `make autocode` 自动生成模块代码 179 | 180 | ### 计划实现功能 181 | 182 | - **策划配置**: 策划 excel 配置管理 183 | - **背包系统**: 物品管理 184 | - **邮件系统**: 邮件收发功能 185 | - **社交系统**: 好友系统、群聊系统 186 | - **游戏内商城**: 商品管理、交易功能 187 | - **交易系统**: 物品交易拍卖功能 188 | - **副本系统**: 副本创建、副本内玩法 189 | - **排行榜系统**: 玩家排行榜 190 | - **活动系统**: 活动管理、活动玩法 191 | 192 | ## 🔗 相关工程 193 | 194 | - **Demo 客户端**: [phaser-game](https://github.com/hanxi/phaser-game) - 基于 Phaser 的游戏客户端 195 | - **网关服务**: [goscon](https://github.com/hanxi/goscon) - WebSocket 转 TCP 网关 196 | - **登录鉴权**: [gamepass](https://github.com/hanxi/gamepass) - 第三方登录鉴权服务 197 | 198 | ## 📚 文档 199 | 200 | - [项目 Wiki](https://github.com/hanxi/skyext/wiki) - 详细开发文档 201 | - [Skynet 官方文档](https://github.com/cloudwu/skynet/wiki) - Skynet 框架文档 202 | 203 | ## 💬 交流讨论 204 | 205 | - **QQ 群**: `677839887` 206 | - **GitHub Issues**: 提交 Bug 报告和功能建议 207 | 208 | ## 📄 许可证 209 | 210 | 本项目基于 MIT 许可证开源,详见 [LICENSE](LICENSE) 文件。 211 | 212 | ## 🤝 贡献 213 | 214 | 欢迎提交 Pull Request 和 Issue! 215 | 216 | 1. Fork 本项目 217 | 2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) 218 | 3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) 219 | 4. 推送到分支 (`git push origin feature/AmazingFeature`) 220 | 5. 开启 Pull Request 221 | -------------------------------------------------------------------------------- /lualib-src/crypto.c: -------------------------------------------------------------------------------- 1 | #define LUA_LIB 2 | 3 | #include 4 | #include 5 | #include 6 | #include "sodium.h" 7 | 8 | // Base64URL编码表,不包含填充字符= 9 | static const char base64url_enc[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; 10 | 11 | // 解码用的查找表,将字符映射到对应的值 12 | static const unsigned char base64url_dec[] = { 13 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 14 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 15 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3E, 0xFF, 0xFF, 16 | 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 17 | 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 18 | 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 19 | 0xFF, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 20 | 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 21 | }; 22 | 23 | // Base64URL编码函数 24 | static int lb64urlencode(lua_State *L) { 25 | size_t input_len; 26 | const unsigned char *input = (const unsigned char *)luaL_checklstring(L, 1, &input_len); 27 | 28 | // 计算输出缓冲区大小 29 | size_t output_len = ((input_len + 2) / 3) * 4; 30 | char *output = (char *)lua_newuserdata(L, output_len + 1); // +1 用于终止符 31 | 32 | size_t i, j; 33 | for (i = 0, j = 0; i < input_len; i += 3) { 34 | // 读取三个字节 35 | unsigned long val = (input[i] << 16); 36 | if (i + 1 < input_len) val |= (input[i + 1] << 8); 37 | if (i + 2 < input_len) val |= input[i + 2]; 38 | 39 | // 拆分为四个6位值 40 | output[j++] = base64url_enc[(val >> 18) & 0x3F]; 41 | output[j++] = base64url_enc[(val >> 12) & 0x3F]; 42 | 43 | // 处理剩余字节 44 | if (i + 1 < input_len) { 45 | output[j++] = base64url_enc[(val >> 6) & 0x3F]; 46 | } 47 | if (i + 2 < input_len) { 48 | output[j++] = base64url_enc[val & 0x3F]; 49 | } 50 | } 51 | 52 | output[j] = '\0'; // 添加终止符 53 | lua_pushlstring(L, output, j); // 推送实际长度的字符串 54 | return 1; 55 | } 56 | 57 | // Base64URL解码函数 58 | static int lb64urldecode(lua_State *L) { 59 | size_t input_len; 60 | const char *input = luaL_checklstring(L, 1, &input_len); 61 | 62 | // 移除可能存在的填充字符(虽然Base64URL通常不使用) 63 | while (input_len > 0 && input[input_len - 1] == '=') { 64 | input_len--; 65 | } 66 | 67 | // 计算输出缓冲区大小 68 | size_t output_len = (input_len * 3) / 4; 69 | unsigned char *output = (unsigned char *)lua_newuserdata(L, output_len + 1); 70 | 71 | size_t i, j; 72 | unsigned long val = 0; 73 | int bits = 0; 74 | 75 | for (i = 0, j = 0; i < input_len; i++) { 76 | unsigned char c = (unsigned char)input[i]; 77 | if (c > 127) { 78 | return luaL_error(L, "invalid base64url character"); 79 | } 80 | 81 | unsigned char dec = base64url_dec[c]; 82 | if (dec == 0xFF) { 83 | return luaL_error(L, "invalid base64url character: %c", c); 84 | } 85 | 86 | // 累积位值 87 | val = (val << 6) | dec; 88 | bits += 6; 89 | 90 | // 当累积了8位或更多时,提取一个字节 91 | if (bits >= 8) { 92 | bits -= 8; 93 | output[j++] = (val >> bits) & 0xFF; 94 | } 95 | } 96 | 97 | output[j] = '\0'; // 添加终止符 98 | lua_pushlstring(L, (const char *)output, j); // 推送实际长度的字符串 99 | return 1; 100 | } 101 | 102 | static int 103 | lhmac_sha256(lua_State *L) { 104 | size_t key_len = 0; 105 | const char* key = luaL_checklstring(L, 1, &key_len); 106 | 107 | size_t msg_len = 0; 108 | const char* msg = luaL_checklstring(L, 2, &msg_len); 109 | 110 | unsigned char out[crypto_auth_hmacsha256_BYTES]; 111 | 112 | crypto_auth_hmacsha256_state state; 113 | 114 | crypto_auth_hmacsha256_init(&state, (const unsigned char*)key, key_len); 115 | crypto_auth_hmacsha256_update(&state, (const unsigned char*)msg, msg_len); 116 | crypto_auth_hmacsha256_final(&state, out); 117 | 118 | lua_pushlstring(L, (const char*)out, crypto_auth_hmacsha256_BYTES); 119 | return 1; 120 | } 121 | 122 | static int 123 | lhmac_sha512(lua_State *L) { 124 | size_t key_len = 0; 125 | const char* key = luaL_checklstring(L, 1, &key_len); 126 | 127 | size_t msg_len = 0; 128 | const char* msg = luaL_checklstring(L, 2, &msg_len); 129 | 130 | unsigned char out[crypto_auth_hmacsha512_BYTES]; 131 | 132 | crypto_auth_hmacsha512_state state; 133 | 134 | crypto_auth_hmacsha512_init(&state, (const unsigned char*)key, key_len); 135 | crypto_auth_hmacsha512_update(&state, (const unsigned char*)msg, msg_len); 136 | crypto_auth_hmacsha512_final(&state, out); 137 | 138 | lua_pushlstring(L, (const char*)out, crypto_auth_hmacsha512_BYTES); 139 | return 1; 140 | } 141 | 142 | LUAMOD_API int 143 | luaopen_crypto(lua_State* L) { 144 | luaL_checkversion(L); 145 | 146 | // 在模块加载时初始化 libsodium 147 | if (sodium_init() < 0) { 148 | return luaL_error(L, "libsodium init failed"); 149 | } 150 | 151 | luaL_Reg l[] = { 152 | {"hmac_sha256", lhmac_sha256}, 153 | {"hmac_sha512", lhmac_sha512}, 154 | {"base64urlencode", lb64urlencode}, 155 | {"base64urldecode", lb64urldecode}, 156 | {NULL, NULL} 157 | }; 158 | luaL_newlib(L, l); 159 | return 1; 160 | } 161 | -------------------------------------------------------------------------------- /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release Skyext 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | tags: [ 'v*' ] 7 | 8 | permissions: 9 | contents: write 10 | actions: read 11 | 12 | jobs: 13 | build: 14 | name: Build ${{ matrix.platform }} 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | include: 20 | - platform: windows 21 | os: ubuntu-latest 22 | container: debian:13 23 | target: mingw 24 | artifact: skyext-windows.zip 25 | - platform: linux 26 | os: ubuntu-latest 27 | container: debian:13 28 | target: linux 29 | artifact: skyext-linux.zip 30 | - platform: macosx 31 | os: macos-latest 32 | target: macosx 33 | artifact: skyext-macosx.zip 34 | - platform: freebsd 35 | os: ubuntu-latest 36 | container: debian:13 37 | target: freebsd 38 | artifact: skyext-freebsd.zip 39 | 40 | container: ${{ matrix.container }} 41 | 42 | steps: 43 | - name: Install dependencies (Debian - Windows/Linux) 44 | if: matrix.container == 'debian:13' 45 | run: | 46 | apt-get update 47 | apt-get install -y --no-install-recommends \ 48 | build-essential \ 49 | make \ 50 | pkg-config \ 51 | mingw-w64 \ 52 | mingw-w64-tools \ 53 | mingw-w64-i686-dev \ 54 | mingw-w64-x86-64-dev \ 55 | autoconf \ 56 | automake \ 57 | libtool \ 58 | git \ 59 | zip \ 60 | ca-certificates \ 61 | curl \ 62 | libreadline-dev \ 63 | libedit-dev \ 64 | rsync 65 | 66 | - name: Install dependencies (macOS) 67 | if: matrix.platform == 'macosx' 68 | run: | 69 | # macOS usually has most build tools pre-installed 70 | # Install any additional dependencies if needed 71 | brew install autoconf automake libtool || true 72 | 73 | - name: Install dependencies (FreeBSD) 74 | if: matrix.platform == 'freebsd' 75 | run: | 76 | # FreeBSD build using standard build tools (cross-compilation compatible) 77 | apt-get update 78 | apt-get install -y --no-install-recommends \ 79 | build-essential \ 80 | make \ 81 | pkg-config \ 82 | autoconf \ 83 | automake \ 84 | libtool \ 85 | git \ 86 | zip \ 87 | ca-certificates \ 88 | curl \ 89 | libreadline-dev \ 90 | libedit-dev \ 91 | rsync 92 | 93 | - name: Update CA certificates (Debian containers) 94 | if: matrix.container == 'debian:13' 95 | run: | 96 | update-ca-certificates 97 | 98 | - name: Configure Git SSL (Debian containers) 99 | if: matrix.container == 'debian:13' 100 | run: | 101 | git config --global http.sslverify true 102 | git config --global http.sslcainfo /etc/ssl/certs/ca-certificates.crt 103 | 104 | - name: Checkout code 105 | uses: actions/checkout@v6 106 | with: 107 | submodules: recursive 108 | fetch-depth: 1 109 | 110 | - name: Build ${{ matrix.platform }} 111 | run: | 112 | make cleanall 113 | make ${{ matrix.target }} 114 | 115 | - name: Prepare build files 116 | run: | 117 | mkdir -p build-output 118 | # Copy all files except .git and .github with ignore-errors flag 119 | - name: Clean and recreate directory 120 | run: | 121 | rm -rf build-output/ 122 | mkdir -p build-output/ 123 | 124 | - name: Sync files to build-output excluding specific directories 125 | run: | 126 | rsync -av --ignore-errors --exclude='.git*' --exclude='.github' --exclude='build-output' . build-output/ 127 | 128 | - name: Upload artifact 129 | uses: actions/upload-artifact@v4 130 | with: 131 | name: ${{ matrix.artifact }} 132 | path: build-output/ 133 | retention-days: 30 134 | 135 | release: 136 | name: Create Release 137 | needs: build 138 | runs-on: ubuntu-latest 139 | if: startsWith(github.ref, 'refs/tags/v') 140 | 141 | steps: 142 | - name: Download all artifacts 143 | uses: actions/download-artifact@v4 144 | with: 145 | path: artifacts 146 | 147 | - name: Prepare release assets 148 | run: | 149 | mkdir -p release-assets 150 | # Copy zip files from artifact directories to release assets 151 | for artifact in skyext-windows.zip skyext-linux.zip skyext-macosx.zip skyext-freebsd.zip; do 152 | if [ -d "artifacts/${artifact}" ]; then 153 | # The artifacts are already organized, just copy them 154 | cp -r "artifacts/${artifact}/"* "release-assets/" 2>/dev/null || true 155 | # Or if we want to create new zip files with consistent naming 156 | platform=$(echo ${artifact} | sed 's/skyext-\(.*\)\.zip/\1/') 157 | cd "artifacts/${artifact}" 158 | zip -r "../../release-assets/skyext-${platform}.zip" . 159 | cd ../.. 160 | fi 161 | done 162 | ls -la release-assets/ 163 | 164 | - name: Create Release 165 | uses: softprops/action-gh-release@v1 166 | with: 167 | files: release-assets/*.zip 168 | generate_release_notes: true 169 | draft: false 170 | prerelease: false 171 | env: 172 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 173 | -------------------------------------------------------------------------------- /lualib/http_server/agent.lua: -------------------------------------------------------------------------------- 1 | return function(agent_id) 2 | local skynet = require "skynet" 3 | local socket = require "skynet.socket" 4 | local sockethelper = require "http.sockethelper" 5 | local cmd_api = require "cmd_api" 6 | local log = require "log" 7 | local httpd = require "http.httpd" 8 | local config = require "config" 9 | local cjson = require "cjson.safe" 10 | local urllib = require "http.url" 11 | 12 | local xpcall_msgh = log.xpcall_msgh 13 | local decode_json = cjson.decode 14 | local encode_json = cjson.encode 15 | 16 | local CMD = {} 17 | local SOCKET = {} 18 | 19 | local request_body_size = config.get_number("http_request_body_size") or (1024 * 1024) 20 | local routers = {} -- method -> path -> handler 21 | 22 | local function response(id, write, ...) 23 | local ok, err = httpd.write_response(write, ...) 24 | if not ok then 25 | log.debug("response failed", "id", id, "err", err) 26 | end 27 | log.debug("response ok", "id", id) 28 | end 29 | 30 | local function handle_request(id, interface, addr) 31 | log.debug("handle_request", "addr", addr, "id", id) 32 | local code, url, method, header, body = httpd.read_request(interface.read, request_body_size) 33 | if not code then 34 | if url == sockethelper.socket_error then 35 | log.info("handle_request socket closed", "id", id, "addr", addr, "err", url) 36 | else 37 | log.warn("handle_request read_request failed", "id", id, "addr", addr, "err", url) 38 | end 39 | return 40 | end 41 | 42 | if code ~= 200 then 43 | response(id, interface.write, code) 44 | return 45 | end 46 | 47 | local res = { 48 | header = { 49 | ["X-Powered-By"] = "skyext framework", 50 | ["Access-Control-Allow-Origin"] = "*", 51 | ["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS", 52 | ["Access-Control-Allow-Headers"] = "Content-Type,x-token", 53 | ["Access-Control-Allow-Credentials"] = "true", 54 | }, 55 | } 56 | 57 | if method == "OPTIONS" then 58 | response(id, interface.write, 204, "", res.header) 59 | return 60 | end 61 | 62 | local method_routers = routers[method] 63 | if not method_routers then 64 | response(id, interface.write, 405) 65 | return 66 | end 67 | 68 | local path, query = urllib.parse(url) 69 | local handler = method_routers[path] 70 | if not handler then 71 | log.warn("handle_request not found", "id", id, "addr", addr, "method", method, "path", path) 72 | response(id, interface.write, 404) 73 | return 74 | end 75 | 76 | local req = { 77 | id = id, 78 | addr = addr, 79 | url = url, 80 | path = path, 81 | query = query, 82 | header = header, 83 | body = body, 84 | parse_query = function() 85 | return urllib.parse_query(query) 86 | end, 87 | read_json = function() 88 | return decode_json(body) 89 | end, 90 | } 91 | 92 | res.write = function(statuscode, bodyfunc, header) 93 | header = header or {} 94 | for k, v in pairs(res.header) do 95 | header[k] = v 96 | end 97 | response(id, interface.write, statuscode, bodyfunc, header) 98 | end 99 | res.write_json = function(data, header) 100 | header = header or {} 101 | for k, v in pairs(res.header) do 102 | header[k] = v 103 | end 104 | response(id, interface.write, 200, encode_json(data), header) 105 | end 106 | handler(req, res) 107 | end 108 | 109 | local function gen_interface(id) 110 | return { 111 | init = nil, 112 | close = nil, 113 | read = sockethelper.readfunc(id), 114 | write = sockethelper.writefunc(id), 115 | } 116 | end 117 | 118 | function SOCKET.request(id, addr) 119 | log.info("request", "id", id, "addr", addr) 120 | 121 | socket.start(id) 122 | 123 | local interface = gen_interface(id) 124 | if interface.init then 125 | interface.init() 126 | end 127 | 128 | local ok, err = xpcall(handle_request, xpcall_msgh, id, interface, addr) 129 | if not ok then 130 | log.warn("request failed", "id", id, "addr", addr, "err", err) 131 | end 132 | 133 | socket.close(id) 134 | if interface and interface.close then 135 | interface.close() 136 | end 137 | log.debug("request end", "id", id, "addr", addr) 138 | end 139 | 140 | skynet.start(function() 141 | cmd_api.dispatch_socket(CMD, SOCKET) 142 | log.info("http agent start", "agent_id", agent_id) 143 | end) 144 | 145 | function CMD.register_router(router_name) 146 | local router = require(router_name) 147 | for method, handlers in pairs(router) do 148 | for path, handler in pairs(handlers) do 149 | local method_routers = routers[method] 150 | if not method_routers then 151 | method_routers = {} 152 | routers[method] = method_routers 153 | end 154 | method_routers[path] = handler 155 | end 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /tools/dist.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import subprocess 3 | import shutil 4 | import time 5 | import os 6 | 7 | 8 | def copy_files(files, source, target): 9 | for file_path in files: 10 | # 计算相对于源目录的路径 11 | relative_path = file_path.relative_to(source) 12 | # 构建目标路径 13 | dest_path = target / relative_path 14 | 15 | # 创建目标文件的父目录(如果不存在) 16 | dest_path.parent.mkdir(parents=True, exist_ok=True) 17 | 18 | # 复制文件 19 | shutil.copy(file_path, dest_path) 20 | print(f" 🟢 Copied: {file_path} -> {dest_path}") 21 | 22 | 23 | def is_luac_file(file_path): 24 | try: 25 | with open(file_path, "rb") as f: 26 | header = f.read(4) 27 | return header == b"\x1bLua" 28 | except Exception: 29 | return False 30 | 31 | 32 | def luac_files(files, source, target): 33 | for file_path in files: 34 | try: 35 | # 计算相对于源目录的路径 36 | relative_path = file_path.relative_to(source) 37 | # 构建目标路径 38 | dest_path = target / relative_path 39 | 40 | # 创建目标文件的父目录(如果不存在) 41 | dest_path.parent.mkdir(parents=True, exist_ok=True) 42 | 43 | # 检查是否已经是编译后的文件 44 | if is_luac_file(file_path): 45 | shutil.copy(file_path, dest_path) 46 | print(f" 🟢 copy: {file_path} -> {dest_path}") 47 | else: 48 | cmd = f'./bin/luac -o "{dest_path}" "{file_path}"' 49 | result = subprocess.run(cmd, shell=True, capture_output=True, text=True) 50 | 51 | if result.returncode == 0: 52 | print(f" 🟢 luac: {file_path} -> {dest_path}") 53 | else: 54 | print(f" ❌ Error processing {file_path}: {result.stderr}") 55 | 56 | except Exception as e: 57 | print(f" ❌ Error processing {file_path}: {str(e)}") 58 | 59 | 60 | def copy_dirs(dir_names, dist_root="./dist", exclude_paths=None): 61 | """ 62 | 复制多个目录,并可选地忽略指定路径中的内容。 63 | """ 64 | dist_root = Path(dist_root) 65 | dist_root.mkdir(parents=True, exist_ok=True) 66 | 67 | # 将排除路径标准化为 Path 并转为绝对路径 68 | excluded_full_paths = [] 69 | if exclude_paths: 70 | for p in exclude_paths: 71 | try: 72 | excluded_full_paths.append((Path(".") / p).resolve()) 73 | except Exception: 74 | pass 75 | 76 | def ignore_function(directory, files): 77 | """shutil.copytree 的 ignore 回调""" 78 | dir_path = Path(directory).resolve() 79 | ignored = [] 80 | for f in files: 81 | file_path = (dir_path / f).resolve() 82 | if any( 83 | ( 84 | file_path.is_relative_to(excluded) 85 | if hasattr(file_path, "is_relative_to") 86 | else str(file_path).startswith(str(excluded) + "/") 87 | or file_path == excluded 88 | ) 89 | for excluded in excluded_full_paths 90 | ): 91 | ignored.append(f) 92 | return ignored 93 | 94 | for dir_name in dir_names: 95 | source_path = Path(f"./{dir_name}").resolve() 96 | target_path = dist_root / dir_name 97 | 98 | if not source_path.exists(): 99 | print(f" ⚠️ 源目录不存在: {source_path}") 100 | continue 101 | 102 | if not source_path.is_dir(): 103 | print(f" ⚠️ 不是目录: {source_path}") 104 | continue 105 | 106 | print(f" 📂 复制 {dir_name} 目录 ...") 107 | try: 108 | shutil.copytree( 109 | source_path, target_path, ignore=ignore_function, dirs_exist_ok=True 110 | ) 111 | print(f" 🟢 成功复制: {source_path} -> {target_path}") 112 | except Exception as e: 113 | print(f" ❌ 复制失败 {dir_name}: {e}") 114 | 115 | 116 | def filter_paths(source, pattern, excluded_root_dirs, excluded_current_dirs): 117 | return [ 118 | path 119 | for path in source.rglob(pattern) 120 | if not ( 121 | excluded_root_dirs.intersection(path.parts) # 完全排除的目录 122 | or ( 123 | len(path.parts) > 1 and path.parts[0] in excluded_current_dirs 124 | ) # 当前目录下的 125 | ) 126 | ] 127 | 128 | 129 | if __name__ == "__main__": 130 | start_time = time.time() 131 | 132 | source = Path(".").resolve() # 源根目录 133 | target = Path("./dist") # 目标根目录 134 | if target.exists(): 135 | shutil.rmtree(target) # 清空目标目录 136 | print("🗑️ 已清空 dist 目录") 137 | 138 | excluded_root_dirs = {"3rd"} # 完全排除的目录 139 | excluded_current_dirs = {"etc", "skyext"} # 只排除当前目录下的 140 | 141 | so_files = filter_paths(source, "*.so", excluded_root_dirs, excluded_current_dirs) 142 | lua_files = filter_paths(source, "*.lua", excluded_root_dirs, excluded_current_dirs) 143 | 144 | print("📂 复制 so 文件 ...") 145 | copy_files(so_files, source, target) 146 | 147 | print("⚙️ 编译/复制 lua 文件 ...") 148 | luac_files(lua_files, source, target) 149 | 150 | print("📂 创建 logs 目录 ...") 151 | logs_dir = Path(target / "logs").resolve() 152 | logs_dir.mkdir(parents=True, exist_ok=True) 153 | print(f" 🟢 成功创建: {logs_dir}") 154 | 155 | print("📂 复制项目目录 ...") 156 | copy_dirs( 157 | dir_names=[ 158 | "etc", 159 | "tools", 160 | "bin", 161 | "proto", 162 | "schema", 163 | "build", 164 | ], 165 | exclude_paths=[ 166 | "./tools/mongodb/db", 167 | "./tools/etcd/etcd1_data", 168 | "./tools/etcd/etcd2_data", 169 | "./tools/etcd/etcd3_data", 170 | ], 171 | ) 172 | 173 | print("📄 复制文档文件 ...") 174 | copy_files( 175 | [ 176 | source / "README.md", 177 | source / "LICENSE", 178 | ], 179 | source, 180 | target, 181 | ) 182 | 183 | # 打包成 zip 文件 184 | print("📦 正在打包 dist -> skyext.zip ...") 185 | shutil.make_archive("skyext", "zip", root_dir=target) 186 | print("🟢 打包完成: skyext.zip") 187 | 188 | shutil.rmtree(target) # 清空目标目录 189 | 190 | elapsed = time.time() - start_time 191 | print(f"⏱️ 总耗时: {elapsed:.2f}s") 192 | -------------------------------------------------------------------------------- /lualib/sproto_api.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local socket = require "skynet.socket" 3 | local sproto = require "sproto" 4 | local sprotoloader = require "sprotoloader" 5 | local netpack = require "skynet.netpack" 6 | local event_channel_api = require "event_channel_api" 7 | local config = require "config" 8 | local log = require "log" 9 | 10 | local M = {} 11 | local g_sproto_loader_service 12 | local g_schema 13 | local g_sproto_info 14 | local g_host 15 | local g_send_request 16 | local g_sessions = {} 17 | local REQUEST_CMD = {} 18 | local GET_OBJ_CB = {} 19 | local MIDDLEWARE_CBS = {} 20 | 21 | local function register_request_func(request_name, func) 22 | if REQUEST_CMD[request_name] then 23 | error("Request command " .. request_name .. " already registered") 24 | end 25 | REQUEST_CMD[request_name] = func 26 | end 27 | 28 | -- mod_name 是协议的文件名 29 | -- module 是连接管理模块 30 | -- 一个连接管理模块需要提供 get_obj 接口用于获取连接对象 31 | -- 同时也可以提供 middleware_cbs 表,用于注册中间件,主要用于做协议拦截 32 | -- obj 就是一个连接对象,由连接管理模块的 get_obj 接口提供 33 | function M.register_module(mod_name, module, cmds) 34 | for k, v in pairs(cmds or {}) do 35 | if type(v) == "function" then 36 | local request_name = mod_name .. "." .. k 37 | register_request_func(request_name, v) 38 | 39 | GET_OBJ_CB[request_name] = module.get_obj 40 | MIDDLEWARE_CBS[request_name] = module.middleware_cbs 41 | end 42 | end 43 | end 44 | 45 | local function new_context(fd, request, obj) 46 | return { 47 | fd = fd, 48 | request = request, 49 | obj = obj, 50 | } 51 | end 52 | 53 | -- request 消息处理接口: 54 | -- 第 1 个参数是协议数据里的 request ,协议处理接口用 : 实现的时候,刚好 self 就是 request 55 | -- 第 2 个参数是 fd 56 | -- 第 3 个参数是 obj 对象 57 | local function dispatch_request(fd, request_name, request, response_cb) 58 | local f = assert(REQUEST_CMD[request_name]) 59 | request = request or {} 60 | local get_obj = GET_OBJ_CB[request_name] 61 | log.debug("dispatch_request", "request_name", request_name, "fd", fd, "get_obj", get_obj, "request", request) 62 | local obj = nil 63 | if get_obj then 64 | obj = get_obj(fd) 65 | end 66 | 67 | local middleware_cbs = MIDDLEWARE_CBS[request_name] 68 | for _, cb in ipairs(middleware_cbs or {}) do 69 | local ok, ret, err = pcall(cb, request_name, request, fd, obj) 70 | if not ok then 71 | log.warn("middleware callback error", "request_name", request_name, "fd", fd, "err", ret) 72 | return nil 73 | else 74 | if ret == nil then 75 | log.warn("middleware callback reject the request", "request_name", request_name, "fd", fd, "err", err) 76 | return nil 77 | end 78 | end 79 | end 80 | 81 | local r = f(request, fd, obj) 82 | if response_cb == nil then 83 | if r ~= nil then 84 | log.error("request function should not return a value", "request_name", request_name, "fd", fd) 85 | end 86 | return 87 | end 88 | return response_cb(r) 89 | end 90 | 91 | local function send_package(fd, pack) 92 | local data, sz = netpack.pack(pack, sz) 93 | socket.write(fd, data, sz) 94 | end 95 | 96 | local function wakeup_call(pending, response, sz) 97 | local co = pending.co 98 | if not co then 99 | return false 100 | end 101 | pending.co = nil 102 | pending.response = response 103 | pending.sz = sz 104 | skynet.wakeup(pending) 105 | return true 106 | end 107 | 108 | function M.raw_dispatch(fd, type, request_name, request, response_cb) 109 | if type == "REQUEST" then 110 | local ok, result = xpcall(dispatch_request, debug.traceback, fd, request_name, request, response_cb) 111 | if ok then 112 | if result then 113 | send_package(fd, result) 114 | log.debug("request dispatched successfully", "request_name", request_name, "fd", fd) 115 | end 116 | else 117 | log.error("dispatch request error", "request_name", request_name, "result", result) 118 | end 119 | else 120 | local sess = request_name 121 | local pending = g_sessions[sess] 122 | if pending then 123 | return wakeup_call(pending, request, sz) 124 | end 125 | end 126 | end 127 | 128 | local function register_protocol() 129 | skynet.register_protocol({ 130 | name = "client", 131 | id = skynet.PTYPE_CLIENT, 132 | unpack = function(msg, sz) 133 | return g_host:dispatch(msg, sz) 134 | end, 135 | dispatch = function(fd, _, type, request_name, request, response_cb) 136 | skynet.ignoreret() -- session is fd, don't call skynet.ret 137 | return M.raw_dispatch(fd, type, request_name, request, response_cb) 138 | end, 139 | }) 140 | end 141 | 142 | function M.notify(fd, name, param) 143 | send_package(fd, g_send_request(name, param)) 144 | end 145 | 146 | function M.call(fd, name, param, session, timeout) 147 | send_package(fd, g_send_request(name, param, session)) 148 | 149 | local pending = { 150 | name = name, 151 | co = coroutine.running(), 152 | response = nil, 153 | } 154 | g_sessions[session] = pending 155 | 156 | -- 默认超时时间 157 | if not timeout then 158 | timeout = config.get_number("sproto_timeout") or 10 159 | end 160 | skynet.sleep(timeout * 100, pending) 161 | g_sessions[session] = nil 162 | 163 | if pending.co then 164 | log.error("call timeout", "name", name, "fd", fd, "session", session) 165 | return nil 166 | end 167 | 168 | return pending.response 169 | end 170 | 171 | function M.get_sproto_info() 172 | return g_sproto_info 173 | end 174 | 175 | function M.get_sproto_host() 176 | return g_host 177 | end 178 | 179 | local function get_sproto_schema() 180 | local sproto_schema_path = config.get("sproto_schema_path") 181 | local sproto_index = config.get_number("sproto_index") or 1 182 | local ok, info = skynet.call(g_sproto_loader_service, "lua", "load_proto", sproto_schema_path, sproto_index) 183 | if not ok then 184 | error(info) 185 | end 186 | 187 | local sprotoloader = require("sprotoloader") 188 | local ret = sprotoloader.load(sproto_index) 189 | return ret, info or {} 190 | end 191 | 192 | local function reload_sproto() 193 | g_schema, g_sproto_info = get_sproto_schema() 194 | g_host = g_schema:host("base.package") 195 | g_send_request = g_host:attach(g_schema) 196 | end 197 | 198 | skynet.init(function() 199 | g_sproto_loader_service = skynet.uniqueservice("sproto_loader") 200 | 201 | reload_sproto() 202 | local sproto_schema_path = config.get("sproto_schema_path") 203 | event_channel_api.subscribe(g_sproto_loader_service, sproto_schema_path, reload_sproto) 204 | 205 | register_protocol() 206 | end) 207 | 208 | return M 209 | -------------------------------------------------------------------------------- /lualib/dbmgr.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local config = require "config" 3 | local log = require "log" 4 | local mongo_conn = require "mongo_conn" 5 | local orm = require "orm" 6 | local schema = require "orm.schema" 7 | local timer = require "timer" 8 | 9 | local M = {} 10 | 11 | local g_collection_obj = {} -- 数据库链接 12 | local g_cache_collection = {} -- 缓存数据对象 13 | local g_default_projection = { _id = false } 14 | local mongo_config = config.get_table("mongo_config") 15 | local db_save_interval = config.get_number("db_save_interval") or 3*60 16 | 17 | local function check_collection(dbname, dbcoll) 18 | local db_config = mongo_config[dbname] 19 | if db_config == nil then 20 | error("database not configured" .. dbname) 21 | end 22 | 23 | local coll_config = db_config.collections[dbcoll] 24 | if coll_config == nil then 25 | error("collection not configured" .. dbname .. "." .. dbcoll) 26 | end 27 | end 28 | 29 | local function get_cache_collection(dbname, dbcoll) 30 | if not g_cache_collection[dbname] then 31 | g_cache_collection[dbname] = {} 32 | end 33 | local colls = g_cache_collection[dbname] 34 | if not colls[dbcoll] then 35 | colls[dbcoll] = {} 36 | end 37 | return colls[dbcoll] 38 | end 39 | 40 | local function get_collection_obj(dbname, dbcoll) 41 | if not g_collection_obj[dbname] then 42 | g_collection_obj[dbname] = {} 43 | end 44 | local colls = g_collection_obj[dbname] 45 | if not colls[dbcoll] then 46 | local coll_obj = mongo_conn.get_collection(dbname, dbcoll) 47 | colls[dbcoll] = coll_obj 48 | log.info("new mongo collection", "dbname", dbname, "dbcoll", dbcoll) 49 | end 50 | return colls[dbcoll] 51 | end 52 | 53 | local function save_dirty(coll_obj, query, dirty_doc) 54 | local ok, err, ret = coll_obj:safe_update(query, dirty_doc, true) 55 | if not ok then 56 | log.error("save failed", "query", query, "dirty_doc", dirty_doc, "ret", ret, "err", err) 57 | return false 58 | end 59 | 60 | if ret.nModified ~= 1 then 61 | log.error("save failed not modified", "query", query, "dirty_doc", dirty_doc, "ret", ret) 62 | return false 63 | end 64 | return true 65 | end 66 | 67 | local function save_doc(coll_obj, key, unique_id, doc) 68 | if not orm.is_dirty(doc) then 69 | log.debug("doc not dirty", "dbname", dbname, "dbcoll", dbcoll, "key", key, "unique_id", unique_id) 70 | return true 71 | end 72 | 73 | local old_version = doc._version 74 | doc._version = old_version + 1 75 | local query = { 76 | [key] = unique_id, 77 | _version = old_version, 78 | } 79 | local is_dirty, dirty_doc = orm.commit_mongo(doc) 80 | if not is_dirty then 81 | log.info("save not dirty", "query", query, "doc", doc) 82 | return true 83 | end 84 | 85 | local ok = save_dirty(coll_obj, query, dirty_doc) 86 | if not ok then 87 | doc._version = doc._version - 1 88 | end 89 | 90 | log.info("save success", "query", query, "doc", doc, "ret", ret) 91 | return true 92 | end 93 | 94 | function M.load(dbname, dbcoll, key, unique_id, default) 95 | check_collection(dbname, dbcoll) 96 | 97 | local cache_collection = get_cache_collection(dbname, dbcoll) 98 | if cache_collection[unique_id] then 99 | return cache_collection[unique_id].doc 100 | end 101 | 102 | -- 防重入,提前占位 103 | cache_collection[unique_id] = { 104 | loading = true, 105 | } 106 | 107 | local t = default or {} 108 | t[key] = unique_id 109 | t._version = 0 110 | 111 | -- 从数据库加载数据 112 | local coll_obj = get_collection_obj(dbname, dbcoll) 113 | local ret = coll_obj:find_and_modify({ 114 | query = { [key] = unique_id }, 115 | update = { ["$setOnInsert"] = t }, 116 | fields = g_default_projection, 117 | upsert = true, 118 | new = true, 119 | }) 120 | 121 | log.debug("load data", "dbname", dbname, "dbcoll", dbcoll, "key", key, "unique_id", unique_id, "ret", ret) 122 | if ret.ok ~= 1 then 123 | log.error("load failed", "dbname", dbname, "dbcoll", dbcoll, "key", key, "unique_id", unique_id, "ret", ret) 124 | return 125 | end 126 | 127 | -- 防止重入 128 | if not cache_collection[unique_id].loading then 129 | log.warn("load again", "dbname", dbname, "dbcoll", dbcoll, "key", key, "unique_id", unique_id) 130 | return cache_collection[unique_id].doc 131 | end 132 | 133 | -- 用 orm 包裹: dbcoll 为 schema name 134 | local doc = schema[dbcoll].new(ret.value) 135 | 136 | -- 定时器入库脏数据(随机分布) 137 | local timer_obj = timer.repeat_random_delayed("dbmgr", db_save_interval, function() 138 | save_doc(coll_obj, key, unique_id, doc) 139 | log.debug("timer save", "dbname", dbname, "dbcoll", dbcoll, "key", key, "unique_id", unique_id) 140 | end) 141 | 142 | cache_collection[unique_id] = { 143 | doc = doc, 144 | timer_obj = timer_obj, 145 | } 146 | 147 | return doc 148 | end 149 | 150 | function M.unload(dbname, dbcoll, key, unique_id) 151 | check_collection(dbname, dbcoll) 152 | 153 | local cache_collection = get_cache_collection(dbname, dbcoll) 154 | if not cache_collection then 155 | log.error("unload failed no cache", "dbname", dbname, "dbcoll", dbcoll, "key", key, "unique_id", unique_id) 156 | return 157 | end 158 | local cache = cache_collection[unique_id] 159 | if not cache then 160 | log.error("unload failed no such row", "dbname", dbname, "dbcoll", dbcoll, "key", key, "unique_id", unique_id) 161 | return 162 | end 163 | 164 | if cache.loading then 165 | log.warn("unload failed by loading", "dbname", dbname, "dbcoll", dbcoll, "key", key, "unique_id", unique_id) 166 | return 167 | end 168 | 169 | if cache.unloading then 170 | log.warn("unload again", "dbname", dbname, "dbcoll", dbcoll, "key", key, "unique_id", unique_id) 171 | return 172 | end 173 | 174 | cache.unloading = true 175 | 176 | -- 先取消定时存盘 177 | cache.timer_obj:cancel() 178 | log.info("cancel timer save by unload", "dbname", dbname, "dbcoll", dbcoll, "key", key, "unique_id", unique_id) 179 | 180 | local doc = cache.doc 181 | if not doc then 182 | log.error("unload failed no such doc", "dbname", dbname, "dbcoll", dbcoll, "key", key, "unique_id", unique_id) 183 | return 184 | end 185 | 186 | local coll_obj = get_collection_obj(dbname, dbcoll) 187 | local ok = save_doc(coll_obj, key, unique_id, doc) 188 | if not ok then 189 | -- TODO: 把数据完整写入到其他地方 190 | end 191 | 192 | cache.unloading = nil 193 | -- 移除 cache 194 | cache_collection[unique_id] = nil 195 | 196 | log.info("unload success", "dbname", dbname, "dbcoll", dbcoll, "key", key, "unique_id", unique_id) 197 | end 198 | 199 | -- TODO: 关进程时 unload 所有数据:可以考虑用 bulkWrite 接口。 200 | 201 | return M 202 | -------------------------------------------------------------------------------- /lualib/distributed_lock.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local etcd = require "etcd" 3 | local timer = require "timer" 4 | local util_string = require "util.string" 5 | local config = require "config" 6 | local log = require "log" 7 | local crypt = require "skynet.crypt" 8 | 9 | local sformat = string.format 10 | local encode_base64 = crypt.base64encode 11 | 12 | local M = {} 13 | 14 | local LOCK_PREFIX = "/skynet/lock/" 15 | local LEASE_TTL = 30 -- 秒 16 | local HEARTBEAT_INTERVAL = LEASE_TTL // 3 17 | 18 | local g_etcd_client 19 | local g_leaseid 20 | local g_locks = {} 21 | 22 | local function make_lock_info(key, value) 23 | local pfx = sformat("%s%s/", LOCK_PREFIX, key) 24 | return { 25 | pfx = pfx, 26 | my_key = pfx .. g_leaseid, 27 | value = value, 28 | } 29 | end 30 | 31 | local function make_my_key(key) 32 | return sformat("%s%s/%s", LOCK_PREFIX, key, g_leaseid) 33 | end 34 | 35 | -- 在锁失效时通知业务 36 | local function notify_lock_expired(key, lock_info) 37 | if lock_info.expired_cb then 38 | local ok, err = xpcall(lock_info.expired_cb, debug.traceback, lock_info) 39 | if not ok then 40 | log.error("notify_lock_expired failed", "err", err, "key", key) 41 | end 42 | end 43 | end 44 | 45 | local function try_acquire(key, value) 46 | if not g_leaseid then 47 | log.error("etcd client not initialized", "key", key) 48 | return 49 | end 50 | 51 | local lock_info = make_lock_info(key, value) 52 | local pfx = lock_info.pfx 53 | local my_key = lock_info.my_key 54 | local compare = { 55 | { 56 | target = "CREATE", 57 | key = my_key, 58 | createRevision = 0, -- key不存在 59 | }, 60 | } 61 | local success = { 62 | { 63 | requestPut = { 64 | key = my_key, 65 | lease = g_leaseid, 66 | value = value, 67 | }, 68 | }, 69 | { 70 | requestRange = { 71 | key = pfx, 72 | sortOrder = "ASCEND", 73 | sortTarget = "CREATE", 74 | limit = 1, 75 | }, 76 | }, 77 | } 78 | local failure = { 79 | { 80 | requestRange = { 81 | key = my_key, 82 | }, 83 | }, 84 | { 85 | requestRange = { 86 | key = pfx, 87 | sortOrder = "ASCEND", 88 | sortTarget = "CREATE", 89 | limit = 1, 90 | }, 91 | }, 92 | } 93 | 94 | local ret, err = g_etcd_client:txn(compare, success, failure) 95 | if err then 96 | log.error("etcd txn failed", "err", err) 97 | return 98 | end 99 | 100 | lock_info.my_rev = ret.body.header.revision 101 | if not ret.body.succeeded then 102 | -- 事务失败,说明key已存在,获取已存在key的revision 103 | lock_info.my_rev = ret.body.responses[1].response_range.kvs[1].create_revision 104 | end 105 | return ret.body, lock_info 106 | end 107 | 108 | -- TODO: 使用 watch 实现 lock 接口 109 | 110 | -- 不等待锁定成功,失败返回谁占用 111 | function M.try_lock(key, value, expired_cb) 112 | if g_locks[key] then 113 | return true 114 | end 115 | 116 | local resp, lock_info = try_acquire(key, value) 117 | if not resp then 118 | return false 119 | end 120 | 121 | -- 检查是否获得锁 122 | local owner_key = resp.responses[2].response_range.kvs 123 | if (not owner_key) or (#owner_key == 0) or (owner_key[1].create_revision == lock_info.my_rev) then 124 | lock_info.expired_cb = expired_cb 125 | g_locks[key] = lock_info 126 | log.info("lock success", "key", lock_info.my_key, "value", value) 127 | return true 128 | end 129 | 130 | -- 被谁锁的 131 | local lockvalue = owner_key[1].value 132 | 133 | -- 未获得锁,清理key 134 | local ret, err = g_etcd_client:delete(lock_info.my_key) 135 | if not ret then 136 | log.error("distributed_lock etcd delete fail", "key", lock_info.my_key, "err", err) 137 | end 138 | 139 | return false, lockvalue 140 | end 141 | 142 | -- 解锁 143 | function M.unlock(key) 144 | if not g_locks[key] then 145 | return false, "lock not held" 146 | end 147 | 148 | local my_key = g_locks[key].my_key 149 | 150 | local ret, err = g_etcd_client:delete(my_key) 151 | if not ret then 152 | log.error("etcd delete fail", "key", my_key, "err", err) 153 | return false 154 | end 155 | 156 | g_locks[key] = nil 157 | log.info("unlock success", "key", key, "my_key", my_key) 158 | return true 159 | end 160 | 161 | local function etcd_grant() 162 | local ret, err = g_etcd_client:grant(LEASE_TTL) 163 | if not ret then 164 | log.error("distributed_lock grant leaseid failed", "err", err) 165 | return 166 | elseif (not ret.body) or not ret.body.ID then 167 | log.error("distributed_lock grant leaseid failed", "ret", ret) 168 | return 169 | end 170 | 171 | g_leaseid = ret.body.ID 172 | log.info("distributed_lock grant", "leaseid", g_leaseid) 173 | return true 174 | end 175 | 176 | local function etcd_keepalive() 177 | if not g_leaseid then 178 | log.error("distributed_lock keepalive no leaseid") 179 | return false 180 | end 181 | 182 | local ret, err = g_etcd_client:keepalive(g_leaseid) 183 | if not ret then 184 | log.error("distributed_lock keepalive failed socket", "err", err) 185 | return true 186 | elseif (not ret.body) or not ret.body.result or not ret.body.result.TTL then 187 | log.error("distributed_lock keepalive overdue", "leaseid", g_leaseid) 188 | return false 189 | end 190 | 191 | log.debug("distributed_lock etcd keepalive ok", "leaseid", g_leaseid, "ttl", ret.body.result.TTL) 192 | return true 193 | end 194 | 195 | -- 尝试使用新lease重建所有锁 196 | local function on_recreate_lease() 197 | local old_locks = g_locks 198 | g_locks = {} -- 立刻清空,表示所有旧锁都已失效 199 | 200 | log.info("on_recreate_lease leaseid recreated", "leaseid", g_leaseid) 201 | for key, lock_info in pairs(old_locks) do 202 | local ok, lockvalue = M.try_lock(key, lock_info.value, lock_info.expired_cb) 203 | if not ok then 204 | log.error("on_recreate_lease recreate lock failed", "key", key, "lockvalue", lockvalue) 205 | -- 锁重建失败,通知业务 206 | notify_lock_expired(key, lock_info) 207 | else 208 | log.info("Successfully re-acquired lock", "key", key) 209 | end 210 | end 211 | end 212 | 213 | skynet.init(function() 214 | local etcd_config = config.get_table("etcd_config") 215 | g_etcd_client = etcd.new(etcd_config) 216 | g_keepalive_timer = timer.repeat_immediately("distributed_lock_heartbeat", HEARTBEAT_INTERVAL, function() 217 | if g_leaseid and etcd_keepalive() then 218 | -- 续约成功,一切正常 219 | return 220 | end 221 | 222 | -- 续约失败或 leaseid 不存在,需要重新获取 223 | g_leaseid = nil 224 | if etcd_grant() then 225 | -- 成功获取新租约,现在处理锁的重建 226 | -- 使用 skynet.fork 来异步执行,避免阻塞 timer 227 | skynet.fork(on_recreate_lease) 228 | else 229 | log.error("Failed to grant a new lease. Will retry in next interval.") 230 | end 231 | end) 232 | end) 233 | 234 | return M 235 | -------------------------------------------------------------------------------- /lualib/log/bucket/file.lua: -------------------------------------------------------------------------------- 1 | -- 输出到文件 2 | local time = require "time" 3 | local log_util = require "log.util" 4 | local log_formatter = require "log.formatter" 5 | 6 | local parse_level = log_util.parse_level 7 | local should_log = log_util.should_log 8 | 9 | local assert = assert 10 | local tonumber = tonumber 11 | local sfind = string.find 12 | local smatch = string.match 13 | local os_date = os.date 14 | 15 | local LOG_ONELINE_SZ = 255 16 | 17 | local file_mt = {} 18 | file_mt.__index = file_mt 19 | 20 | local line_mt = {} 21 | line_mt.__index = line_mt 22 | 23 | local time_mt = {} 24 | time_mt.__index = time_mt 25 | 26 | local size_mt = {} 27 | size_mt.__index = size_mt 28 | 29 | local M = {} 30 | 31 | function line_mt:need_split() 32 | self.linecount = self.linecount + 1 33 | if self.linecount > self.maxline then 34 | self.linecount = 0 35 | return true 36 | end 37 | return false 38 | end 39 | 40 | function time_mt:need_split() 41 | local t = os_date("*t", time.now()) 42 | if self.split == "hour" then 43 | if t.year ~= self.last.year or t.month ~= self.last.month or t.day ~= self.last.day or t.hour ~= self.last.hour then 44 | self.last.year = t.year 45 | self.last.month = t.month 46 | self.last.day = t.day 47 | self.last.hour = t.hour 48 | return true 49 | end 50 | else -- day 51 | if t.year ~= self.last.year or t.month ~= self.last.month or t.day ~= self.last.day then 52 | self.last.year = t.year 53 | self.last.month = t.month 54 | self.last.day = t.day 55 | return true 56 | end 57 | end 58 | return false 59 | end 60 | 61 | function size_mt:need_split(str) 62 | local delta = #str + 1 63 | local size = self.size + delta 64 | if size > delta and size > self.maxsize then 65 | self.size = delta 66 | return true 67 | end 68 | self.size = size 69 | return false 70 | end 71 | 72 | function file_mt:reload() 73 | self:close() 74 | local handle = assert(io.open(self.path, "a+")) 75 | if self.flush_n > 0 then 76 | handle:setvbuf("full", self.flush_n * LOG_ONELINE_SZ) 77 | else 78 | handle:setvbuf("line") 79 | end 80 | self.handle = handle 81 | self.size = handle:seek("end") 82 | end 83 | 84 | function file_mt:split() 85 | local split_mgr = self.split_mgr 86 | local path = self.path 87 | local filename = path 88 | local new_path 89 | repeat 90 | if filename == split_mgr.lastprefix then 91 | split_mgr.count = split_mgr.count + 1 92 | new_path = filename .. "." .. split_mgr.count 93 | else 94 | split_mgr.count = 0 95 | split_mgr.lastprefix = filename 96 | new_path = filename 97 | end 98 | local handle = io.open(new_path) 99 | until not handle 100 | os.rename(path, new_path) 101 | self:reload() 102 | if split_mgr.size then 103 | split_mgr.size = split_mgr.size + self.size 104 | end 105 | end 106 | 107 | function file_mt:init() 108 | self:reload() 109 | if self.split_mgr and self.size > 0 then 110 | self:split() 111 | end 112 | end 113 | 114 | function file_mt:put(record) 115 | if not self.handle then 116 | return nil 117 | end 118 | 119 | if not should_log(self.level, record.level) then 120 | return nil 121 | end 122 | local str = self.formatter(record) 123 | local split_mgr = self.split_mgr 124 | if split_mgr and split_mgr:need_split(str) then 125 | self:split() 126 | end 127 | local flush_n = self.flush_n 128 | if flush_n > 0 then 129 | local flush_i = self.flush_i + 1 130 | self.handle:write(str, "\n") 131 | if flush_i == flush_n then 132 | self:flush() 133 | end 134 | else 135 | self.handle:write(str, "\n") 136 | end 137 | return true 138 | end 139 | 140 | function file_mt:flush() 141 | if self.flush_n > 0 then 142 | self.flush_i = 0 143 | end 144 | self.flush_ok = self.handle:flush() 145 | end 146 | 147 | function file_mt:check_error() 148 | return self.flush_ok ~= true 149 | end 150 | 151 | function file_mt:close() 152 | local handle = self.handle 153 | if handle then 154 | self:flush() 155 | handle:close() 156 | self.handle = nil 157 | end 158 | end 159 | 160 | local function parse_bytes(u) 161 | local num, unit = smatch(u:upper(), "^([%d.]+)([KMGTP]?)B?$") 162 | num = assert(unit and tonumber(num), "invalid format") 163 | local index = (sfind("KMGTP", unit) or 0) * 10 164 | return num * (1 << index) 165 | end 166 | 167 | local function new_split_mgr(params) 168 | if not params.split then 169 | return 170 | end 171 | 172 | local mgr = { 173 | count = 0, 174 | dateext = params.dateext, 175 | } 176 | 177 | if params.split == "line" then 178 | mgr.maxline = params.maxline 179 | mgr.linecount = 0 180 | return setmetatable(mgr, line_mt) 181 | elseif params.split == "size" then 182 | mgr.maxsize = parse_bytes(params.maxsize) 183 | mgr.size = 0 184 | return setmetatable(mgr, size_mt) 185 | elseif params.split == "hour" then 186 | local t = os_date("*t", time.now()) 187 | mgr.last = { 188 | year = t.year, 189 | month = t.month, 190 | day = t.day, 191 | hour = t.hour, 192 | } 193 | mgr.split = params.split 194 | return setmetatable(mgr, time_mt) 195 | elseif params.split == "day" then 196 | local t = os_date("*t", time.now()) 197 | mgr.last = { 198 | year = t.year, 199 | month = t.month, 200 | day = t.day, 201 | } 202 | mgr.split = params.split 203 | return setmetatable(mgr, time_mt) 204 | end 205 | end 206 | 207 | 208 | -- 日志轮转生成名字格式: 209 | -- filename.count 210 | -- 例如: 配置文件名 app.log 211 | -- 轮转文件名: app.log.1 212 | -- 必须提供文件名,不支持配空 213 | -- params: 支持三种分割方式(默认不分割); 支持 json 风格输出(默认关闭) 214 | -- filename 215 | -- split=size&maxsize=100M 216 | -- split=line&maxline=100000 217 | -- split=hour/day 218 | -- format=json (json/text) 219 | -- color=true (使用颜色) 220 | -- style (函数或日志格式字符串或vtext格式) 221 | -- level=INFO-DEBUG (日志级别范围) 222 | -- flush_period=0 (多少条日志刷一次盘,默认为 0,每次都刷) 223 | function M.new(params) 224 | assert(params.filename, "please specify a file pattern") 225 | local split_mgr = new_split_mgr(params) 226 | local file_obj = { 227 | path = params.filename, -- 当前文件路径 228 | split_mgr = split_mgr, 229 | flush_i = 0, 230 | flush_n = params.flush_period or 0, 231 | flush_ok = true, 232 | } 233 | file_obj.params = { 234 | format = "text", 235 | color = false, 236 | split = false, 237 | maxline = 1e6, 238 | maxsize = "100M", 239 | } 240 | for k, v in pairs(params) do 241 | file_obj.params[k] = v 242 | end 243 | params = file_obj.params 244 | 245 | file_obj.formatter = log_formatter.get_formatter(params.format, params.color, params.style) 246 | file_obj.level = parse_level(params.level) 247 | file_obj = setmetatable(file_obj, file_mt) 248 | 249 | file_obj:init() 250 | return file_obj 251 | end 252 | 253 | return M 254 | -------------------------------------------------------------------------------- /lualib/timer.lua: -------------------------------------------------------------------------------- 1 | -- 秒级定时器 2 | local skynet = require "skynet" 3 | local binaryheap = require "binaryheap" 4 | local time = require "time" 5 | local log = require "log" 6 | 7 | local mrandom = math.random 8 | local millisecond = time.now_ms 9 | local xpcall_msgh = log.xpcall_msgh 10 | 11 | local MAX_IDLE_MS = 5000 -- 最大等待5秒 12 | local OVERLOAD_TIME_MS = 1000 -- 超过1秒的执行时间认为是过载 13 | 14 | local M = {} 15 | local g_timers = {} 16 | local g_minheap = nil 17 | local g_task_co = nil 18 | local g_task_running = false 19 | local g_timer_idx = 0 20 | local g_sleeping = false 21 | local g_skip_sleep = false 22 | 23 | local function new_timer_id() 24 | g_timer_idx = g_timer_idx + 1 25 | return g_timer_idx 26 | end 27 | 28 | local function do_wakeup() 29 | if g_sleeping then 30 | skynet.wakeup(g_task_co) 31 | g_sleeping = false 32 | else 33 | g_skip_sleep = true 34 | end 35 | end 36 | 37 | ---@class TimerObj 38 | local timer_mt = {} 39 | timer_mt.__index = timer_mt 40 | 41 | -- 取消定时器 42 | function timer_mt:cancel() 43 | g_minheap:remove(self._id) 44 | g_timers[self._id] = nil 45 | end 46 | 47 | -- 时间提前到下一帧执行 48 | function timer_mt:wakeup() 49 | if g_minheap:valueByPayload(self._id) then 50 | g_minheap:update(self._id, millisecond()) 51 | else 52 | if g_timers[self.id] then 53 | -- 如果定时器还存在,则重新插入到最小堆中 54 | g_minheap:insert(millisecond(), self._id) 55 | end 56 | end 57 | log.debug("timer wakeup", "timer_obj", self) 58 | 59 | do_wakeup() 60 | end 61 | 62 | function timer_mt:__tostring() 63 | return (""):format(self._name, self._id, self) 64 | end 65 | 66 | --- 创建定时器对象 67 | -- @param name 定时器名称 68 | -- @param sec 执行间隔,单位是秒 69 | -- @param func 定时器回调函数 70 | -- @param first: 首次执行时间,单位是秒(0立即执行) 71 | -- @param times 执行次数,如果不提供,则始终周期性执行 72 | local function new_timer_obj(name, sec, func, first, times) 73 | local id = new_timer_id() 74 | local timer_obj = { 75 | _name = name, 76 | _id = id, 77 | _interval = sec * 1000, 78 | _func = func, 79 | _first = first * 1000, 80 | _times = times or 0, 81 | } 82 | log.info("new timer", "timer_obj", timer_obj) 83 | return setmetatable(timer_obj, timer_mt) 84 | end 85 | 86 | local function insert_timer_exec_time(timer_obj, next_exec_time) 87 | local id = timer_obj._id 88 | g_timers[id] = timer_obj 89 | local old_ms = g_minheap:valueByPayload(id) 90 | if old_ms then 91 | -- 如果定时器已经存在,更新其执行时间,使用最小的时间,可能 wakeup 时已经插入了 92 | if old_ms > next_exec_time then 93 | g_minheap:update(id, next_exec_time) 94 | end 95 | else 96 | g_minheap:insert(next_exec_time, id) 97 | end 98 | end 99 | 100 | --- 插入定时器 101 | local function insert_timer(timer_obj) 102 | local next_exec_time = timer_obj._first + millisecond() 103 | insert_timer_exec_time(timer_obj, next_exec_time) 104 | do_wakeup() 105 | end 106 | 107 | local function update_timer(id, ms_now) 108 | local timer_obj = g_timers[id] 109 | if not timer_obj then 110 | -- 需要更新的定时器可能已经被取消 111 | return 112 | end 113 | 114 | local times = timer_obj._times 115 | if times and times ~= 1 then 116 | -- 周期性任务继续插入 117 | if times > 0 then 118 | timer_obj._times = times - 1 119 | end 120 | -- 不补帧: 每次执行间隔>=interval 121 | local next_exec_time = ms_now + timer_obj._interval 122 | insert_timer_exec_time(timer_obj, next_exec_time) 123 | else 124 | -- 一次性任务执行后删除 125 | g_timers[timer_obj._id] = nil 126 | end 127 | end 128 | 129 | --- 执行定时器 130 | local function exec_timers(timer_ids, ms_now) 131 | for _, id in pairs(timer_ids) do 132 | local timer_obj = g_timers[id] 133 | if timer_obj then 134 | xpcall(timer_obj._func, xpcall_msgh) 135 | update_timer(id, ms_now) 136 | end 137 | 138 | if not g_task_running then 139 | break 140 | end 141 | end 142 | end 143 | 144 | --- 弹出时间节点前触发的定时器并执行 145 | local function minheap_pop_exec() 146 | local id, next_exec_time 147 | local timer_ids = {} 148 | local ms_now = millisecond() 149 | while true do 150 | local id, exec_time = g_minheap:peek() 151 | if not exec_time or exec_time > ms_now then 152 | break 153 | end 154 | timer_ids[#timer_ids + 1] = id 155 | g_minheap:pop() 156 | end 157 | exec_timers(timer_ids, ms_now) 158 | end 159 | 160 | local function do_sleep(duration) 161 | if g_skip_sleep then 162 | g_skip_sleep = false 163 | else 164 | g_sleeping = true 165 | skynet.sleep(duration) 166 | g_sleeping = false 167 | end 168 | end 169 | 170 | --- 启动定时器 171 | local function ensure_init() 172 | if g_task_running then 173 | return 174 | end 175 | 176 | g_task_running = true 177 | g_minheap = binaryheap.minUnique() 178 | g_task_co = skynet.fork(function() 179 | local overload_duration = 0 180 | local duration 181 | local ms_now 182 | while g_task_running do 183 | minheap_pop_exec() 184 | if g_task_running then 185 | ms_now = millisecond() 186 | local _, next_exec_time = g_minheap:peek() 187 | if not next_exec_time then 188 | duration = MAX_IDLE_MS 189 | else 190 | duration = next_exec_time - ms_now 191 | end 192 | 193 | if duration > 0 then 194 | overload_duration = 0 195 | duration = math.min(duration, MAX_IDLE_MS) 196 | do_sleep(duration // 10) 197 | else 198 | local compensate = -duration 199 | overload_duration = overload_duration + compensate 200 | if overload_duration > OVERLOAD_TIME_MS then 201 | overload_duration = 0 202 | log.error("timer overload duration exceed threshold", "overload_duration", overload_duration) 203 | do_sleep(0) 204 | end 205 | end 206 | end 207 | end 208 | end) 209 | end 210 | 211 | -- 创建一个单次定时器 212 | function M.timeout(name, sec, func) 213 | ensure_init() 214 | local timer_obj = new_timer_obj(name, sec, func, sec, 1) 215 | insert_timer(timer_obj) 216 | return timer_obj 217 | end 218 | 219 | --- 创建一个立即执行的周期性定时器 220 | -- @param name 定时器名称 221 | -- @param sec 执行间隔,单位是秒 222 | -- @param func 定时器回调函数 223 | -- @param times 执行次数,如果不提供,则始终周期性执行 224 | function M.repeat_immediately(name, sec, func, times) 225 | ensure_init() 226 | local timer_obj = new_timer_obj(name, sec, func, 0, times) 227 | insert_timer(timer_obj) 228 | return timer_obj 229 | end 230 | 231 | --- 创建一个延迟执行的周期性定时器 232 | -- @param name 定时器名称 233 | -- @param sec 执行间隔,单位是秒 234 | -- @param func 定时器回调函数 235 | -- @param times 执行次数,如果不提供,则始终周期性执行 236 | function M.repeat_delayed(name, sec, func, times) 237 | ensure_init() 238 | local timer_obj = new_timer_obj(name, sec, func, sec, times) 239 | insert_timer(timer_obj) 240 | return timer_obj 241 | end 242 | 243 | --- 创建一个首次随机的延迟执行的周期性定时器 244 | -- @param name 定时器名称 245 | -- @param sec 执行间隔,单位是秒 246 | -- @param func 定时器回调函数 247 | -- @param times 执行次数,如果不提供,则始终周期性执行 248 | function M.repeat_random_delayed(name, sec, func, times) 249 | ensure_init() 250 | local first = mrandom(1, sec) 251 | local timer_obj = new_timer_obj(name, sec, func, first, times) 252 | insert_timer(timer_obj) 253 | return timer_obj 254 | end 255 | 256 | --- 关闭定时器 257 | function M.shutdown() 258 | if g_task_running then 259 | return 260 | end 261 | g_task_running = false 262 | for id, timer_obj in pairs(g_timers) do 263 | timer_obj:cancel() 264 | end 265 | end 266 | 267 | return M 268 | -------------------------------------------------------------------------------- /lualib/log/logger.lua: -------------------------------------------------------------------------------- 1 | local time = require "time" 2 | local util_table = require "util.table" 3 | local bucket = require "log.bucket" 4 | local log_level = require "log.log_level" 5 | local traceback_c = require "traceback.c" 6 | 7 | local type = type 8 | local error = error 9 | local pairs = pairs 10 | local assert = assert 11 | local select = select 12 | local tostring = tostring 13 | local setmetatable = setmetatable 14 | 15 | local sgsub = string.gsub 16 | local smatch = string.match 17 | local sformat = string.format 18 | local tconcat = table.concat 19 | local tunpack = table.unpack 20 | 21 | local DEBUG = log_level.DEBUG 22 | local INFO = log_level.INFO 23 | local WARN = log_level.WARN 24 | local ERROR = log_level.ERROR 25 | local FATAL = log_level.FATAL 26 | 27 | local M = {} 28 | 29 | local service_bucket, default_bucket 30 | local function get_bucket() 31 | -- logger service 会返回 service/logger/bucket.lua 中的 service_bucket 32 | service_bucket = service_bucket or bucket.new({ name = "service" }) 33 | if service_bucket then 34 | return service_bucket 35 | end 36 | 37 | -- 没有 service_bucket 时,使用默认的 bucket 38 | default_bucket = default_bucket or bucket.get_default() 39 | return default_bucket 40 | end 41 | 42 | local g_record = {} 43 | local function save_to_bucket(modname, level, timestamp, src, msg, events) 44 | local bucket = get_bucket() 45 | g_record.module = modname 46 | g_record.level = level 47 | g_record.timestamp = timestamp 48 | g_record.line = src 49 | g_record.msg = msg 50 | g_record.events = events 51 | bucket:put(g_record) 52 | return g_record 53 | end 54 | 55 | local function get_log_src(level) 56 | -- 防止函数尾调用抓到C函数堆栈导致行号异常 57 | while true do 58 | level = level + 1 59 | local info = debug.getinfo(level, "Sl") 60 | if not info then 61 | return "UNKNOWN" 62 | end 63 | local currentline = info.currentline 64 | if currentline > 0 then 65 | return sformat("%s:%s", info.source, info.currentline) 66 | end 67 | end 68 | end 69 | 70 | -- logger object 71 | local logger = {} 72 | logger.__index = logger 73 | 74 | function logger:_serialize(level, s) 75 | if type(s) == "table" and (self.log_table or level < INFO) then 76 | return util_table.tostring(s) 77 | end 78 | return s 79 | end 80 | 81 | -- 结构化日志 82 | function logger:_pack_events(level, ...) 83 | local n = select("#", ...) 84 | assert(n % 2 == 0, "log args not even, must be key-value pairs") 85 | local values = { ... } 86 | for i = 1, n do 87 | values[i] = tostring(self:_serialize(level, values[i])) 88 | end 89 | 90 | local events = {} 91 | local events_key_cnt = { 92 | module = 1, 93 | level = 1, 94 | timestamp = 1, 95 | line = 1, 96 | msg = 1, 97 | } 98 | for i = 1, n, 2 do 99 | local k = values[i] 100 | local v = values[i + 1] 101 | local key = k 102 | local cnt = events_key_cnt[k] or 0 103 | if cnt > 0 then 104 | key = k .. "_" .. events_key_cnt[k] 105 | end 106 | events[#events + 1] = { key, v } 107 | events_key_cnt[k] = cnt + 1 108 | end 109 | return events 110 | end 111 | 112 | function logger:_raw_log(level, stack_depth, msg, ...) 113 | local events = self:_pack_events(level, ...) 114 | local timestamp = time.time() 115 | local src = "" 116 | if self.log_src then 117 | src = get_log_src(self.stack_level + stack_depth) or "" 118 | end 119 | local modname = self.name 120 | save_to_bucket(modname, level, timestamp, src, msg, events) 121 | end 122 | 123 | function logger:_log(level, stack_depth, msg, ...) 124 | -- 过滤掉信息的条件:level高于log_level 125 | if level > self.level then 126 | return 127 | end 128 | local ok, err = pcall(self._raw_log, self, level, stack_depth, msg, ...) 129 | if not ok then 130 | -- 兜底 131 | print(err, msg, ...) 132 | end 133 | end 134 | 135 | -- public interface 136 | local ext_stack_depth = 2 137 | function logger:debug(...) 138 | return self:_log(DEBUG, ext_stack_depth, ...) 139 | end 140 | 141 | function logger:info(...) 142 | return self:_log(INFO, ext_stack_depth, ...) 143 | end 144 | 145 | function logger:warn(...) 146 | return self:_log(WARN, ext_stack_depth, ...) 147 | end 148 | 149 | function logger:error(...) 150 | local tcb = self.traceback(nil, 2) 151 | return self:_log(ERROR, ext_stack_depth, "traceback", tcb, ...) 152 | end 153 | 154 | local function log_traceback(self, log_lv, stack_depth, err_type, err_msg) 155 | local tcb = self.traceback(nil, stack_depth + 1) 156 | return self:_log(log_lv, stack_depth, "stack traceback", "err_type", err_type, "err_msg", err_msg, "traceback", tcb) 157 | end 158 | 159 | local xpcall_counter = 0 160 | local pcall_counter = 0 161 | 162 | -- 创建恢复对象 163 | local function create_restore_guard() 164 | local original_xpcall_count = xpcall_counter 165 | local original_pcall_count = pcall_counter 166 | local guard = {} 167 | 168 | -- 设置元表使对象可关闭 169 | local mt = {} 170 | mt.__close = function(self) 171 | xpcall_counter = original_xpcall_count 172 | pcall_counter = original_pcall_count 173 | end 174 | 175 | setmetatable(guard, mt) 176 | return guard 177 | end 178 | 179 | -- 检查是否在错误处理上下文中 180 | local function in_error_handling_context() 181 | return xpcall_counter > 0 or pcall_counter > 0 182 | end 183 | 184 | -- 封装的 pcall 185 | local origin_pcall = pcall 186 | function M.safe_pcall(func, ...) 187 | pcall_counter = pcall_counter + 1 188 | local guard = create_restore_guard() 189 | return origin_pcall(func, ...) 190 | end 191 | 192 | -- 封装的 xpcall 193 | local origin_xpcall = xpcall 194 | function M.safe_xpcall(func, msgh, ...) 195 | xpcall_counter = xpcall_counter + 1 196 | local guard = create_restore_guard() 197 | return origin_xpcall(func, msgh, ...) 198 | end 199 | 200 | local do_error_stack_depth = 1 201 | local is_first_get_traceback = true 202 | local reset_is_first_get_traceback = setmetatable({}, { 203 | __close = function() 204 | is_first_get_traceback = true 205 | end, 206 | }) 207 | local function do_error(self, err_type, err_msg, err_lv) 208 | if is_first_get_traceback and (not in_error_handling_context()) then 209 | is_first_get_traceback = false 210 | local _ = reset_is_first_get_traceback 211 | log_traceback(self, FATAL, do_error_stack_depth, err_type, err_msg) 212 | end 213 | return error(err_msg, err_lv) 214 | end 215 | 216 | local def_assert_msg = "assertion failed!" 217 | local assert_err_lv = 2 218 | function logger:sys_assert(v, ...) 219 | if v then 220 | return v, ... 221 | end 222 | local message = select("#", ...) > 0 and ... or def_assert_msg 223 | 224 | return do_error(self, "assert", message, assert_err_lv) 225 | end 226 | 227 | function logger:sys_error(message, level) 228 | level = level or 1 229 | if level > 0 then 230 | level = level + 1 231 | end 232 | 233 | return do_error(self, "error", message, level) 234 | end 235 | 236 | local xpcall_msgh_stack_depth = 2 237 | function logger:xpcall_msgh(msg) 238 | log_traceback(self, FATAL, xpcall_msgh_stack_depth, "error", msg) 239 | return msg 240 | end 241 | 242 | -- 配置字段类型约束,如果 type 为 table,则为 table 映射值。 243 | -- field 代表映射到config中代表的字段 244 | local config_constraint = { 245 | name = { type = "string" }, 246 | level = { type = log_level }, 247 | log_src = { type = "boolean" }, 248 | log_table = { type = "boolean" }, 249 | stack_level = { type = "number" }, 250 | traceback = { type = "function" }, 251 | } 252 | 253 | function logger:config(t) 254 | -- 检查配置字段约束 255 | for f, v in pairs(t) do 256 | local c = assert(config_constraint[f], "invalid config type:" .. f) 257 | local ct = c.type 258 | if type(ct) == "table" then 259 | v = assert(ct[v], "invalid value: " .. v .. " for config: " .. f) 260 | else -- lua types 261 | if type(v) ~= ct then 262 | error("type mismatch for field: " .. f) 263 | end 264 | end 265 | self[f] = v 266 | end 267 | end 268 | 269 | function M.new() 270 | local obj = {} 271 | -- 单 vm 内 SERVICE_ARGS 作为全局变量读取。 272 | obj.name = SERVICE_ARGS or "skyext" 273 | 274 | obj.level = log_level.DEBUG 275 | obj.log_src = true 276 | 277 | -- 回溯栈的深度 278 | obj.stack_level = 3 279 | 280 | -- 序列化相关 281 | obj.log_table = true 282 | 283 | local max_depth = 3 284 | local max_ele = 5 285 | local max_string = 80 286 | local max_len = 1000 287 | local levels1 = 3 288 | local levels2 = 3 289 | obj.traceback = traceback_c.get_traceback(max_depth, max_ele, max_string, max_len, levels1, levels2) 290 | 291 | return setmetatable(obj, logger) 292 | end 293 | 294 | return M 295 | -------------------------------------------------------------------------------- /lualib/orm/schema.lua: -------------------------------------------------------------------------------- 1 | -- Code generated from lualib/orm/schema_define.lua 2 | -- DO NOT EDIT! 3 | 4 | local orm = require "orm" 5 | local tointeger = math.tointeger 6 | local sformat = string.format 7 | 8 | local number = setmetatable({ 9 | type = "number", 10 | }, { 11 | __tostring = function() 12 | return "schema_number" 13 | end, 14 | }) 15 | 16 | local integer = setmetatable({ 17 | type = "integer", 18 | }, { 19 | __tostring = function() 20 | return "schema_integer" 21 | end, 22 | }) 23 | 24 | local string = setmetatable({ 25 | type = "string", 26 | }, { 27 | __tostring = function() 28 | return "schema_string" 29 | end, 30 | }) 31 | 32 | local boolean = setmetatable({ 33 | type = "boolean", 34 | }, { 35 | __tostring = function() 36 | return "schema_boolean" 37 | end, 38 | }) 39 | 40 | local function _parse_k_tp(k, need_tp) 41 | if need_tp == integer then 42 | nk = tointeger(k) 43 | if tointeger(k) == nil then 44 | error(sformat("not equal k type. need integer, real: %s, k: %s, need_tp: %s", type(k), tostring(k), tostring(need_tp))) 45 | end 46 | return nk 47 | elseif need_tp == string then 48 | return tostring(k) 49 | end 50 | error(sformat("not support need_tp type: %s, k: %s", tostring(need_tp), tostring(k))) 51 | end 52 | 53 | local function _check_k_tp(k, need_tp) 54 | if need_tp == integer then 55 | if (type(k) ~= "number") or (tointeger(k) == nil) then 56 | error(sformat("not equal k type. need integer, real: %s, k: %s, need_tp: %s", type(k), tostring(k), tostring(need_tp))) 57 | end 58 | return 59 | elseif need_tp == string then 60 | if type(k) ~= "string" then 61 | error(sformat("not equal k type. need string, real: %s, k: %s, need_tp: %s", type(k), tostring(k), tostring(need_tp))) 62 | end 63 | return 64 | end 65 | error(sformat("not support need_tp type: %s, k: %s", tostring(need_tp), tostring(k))) 66 | end 67 | 68 | local function _check_v_tp(v, need_tp) 69 | if need_tp == integer then 70 | if (type(v) ~= "number") or (tointeger(v) == nil) then 71 | error(sformat("not equal v type. need integer, real: %s, v: %s, need_tp: %s", type(v), tostring(v), tostring(need_tp))) 72 | end 73 | return 74 | elseif need_tp == number then 75 | if type(v) ~= "number" then 76 | error(sformat("not equal v type. need number, real: %s, v: %s, need_tp: %s", type(v), tostring(v), tostring(need_tp))) 77 | end 78 | return 79 | elseif need_tp == string then 80 | if type(v) ~= "string" then 81 | error(sformat("not equal v type. need string, real: %s, v: %s, need_tp: %s", type(v), tostring(v), tostring(need_tp))) 82 | end 83 | return 84 | elseif need_tp == boolean then 85 | if type(v) ~= "boolean" then 86 | error(sformat("not equal v type. need boolean, real: %s, v: %s, need_tp: %s", type(v), tostring(v), tostring(need_tp))) 87 | end 88 | return 89 | end 90 | if v ~= need_tp then 91 | error(sformat("not equal v type. need_tp: %s, v: %s", tostring(need_tp), tostring(v))) 92 | end 93 | end 94 | 95 | local function parse_k_func(need_tp) 96 | return function(self, k) 97 | return _parse_k_tp(k, need_tp) 98 | end 99 | end 100 | 101 | local function check_k_func(need_tp) 102 | return function(self, k) 103 | _check_k_tp(k, need_tp) 104 | end 105 | end 106 | 107 | local function check_kv_func(k_need_tp, v_need_tp) 108 | return function(self, k, v) 109 | _check_k_tp(k, k_need_tp) 110 | _check_v_tp(v, v_need_tp) 111 | end 112 | end 113 | 114 | local function parse_k(self, k) 115 | local schema = self[k] 116 | if not schema then 117 | error(sformat("not exist key: %s", k)) 118 | end 119 | return k 120 | end 121 | 122 | local function check_k(self, k) 123 | local schema = self[k] 124 | if not schema then 125 | error(sformat("not exist key: %s", k)) 126 | end 127 | end 128 | 129 | local function check_kv(self, k, v) 130 | local schema = self[k] 131 | if not schema then 132 | error(sformat("not exist key: %s", k)) 133 | end 134 | 135 | _check_v_tp(v, schema) 136 | end 137 | 138 | local bag = { type = "struct" } 139 | local map_integer_resource = { type = "map"} 140 | local mail = { type = "struct" } 141 | local map_string_string = { type = "map"} 142 | local mail_attach = { type = "struct" } 143 | local mail_role = { type = "struct" } 144 | local resource = { type = "struct" } 145 | local role = { type = "struct" } 146 | local role_bag = { type = "struct" } 147 | local map_integer_bag = { type = "map"} 148 | local role_mail = { type = "struct" } 149 | local map_integer_mail = { type = "map"} 150 | local role_modules = { type = "struct" } 151 | local str2str = { type = "struct" } 152 | 153 | setmetatable(map_integer_resource, { 154 | __tostring = function() 155 | return "schema_map_integer_resource" 156 | end, 157 | __index = function(t, k) 158 | return resource 159 | end, 160 | }) 161 | map_integer_resource._parse_k = parse_k_func(integer) 162 | map_integer_resource._check_k = check_k_func(integer) 163 | map_integer_resource._check_kv = check_kv_func(integer, resource) 164 | map_integer_resource.new = function(init) 165 | return orm.new(map_integer_resource, init) 166 | end 167 | 168 | setmetatable(bag, { 169 | __tostring = function() 170 | return "schema_bag" 171 | end, 172 | }) 173 | bag.res = map_integer_resource 174 | bag.res_type = integer 175 | bag._parse_k = parse_k 176 | bag._check_k = check_k 177 | bag._check_kv = check_kv 178 | bag.new = function(init) 179 | return orm.new(bag, init) 180 | end 181 | local bag_fields = {"res","res_type"} 182 | bag.fields = function() 183 | return bag_fields 184 | end 185 | 186 | setmetatable(map_string_string, { 187 | __tostring = function() 188 | return "schema_map_string_string" 189 | end, 190 | __index = function(t, k) 191 | return string 192 | end, 193 | }) 194 | map_string_string._parse_k = parse_k_func(string) 195 | map_string_string._check_k = check_k_func(string) 196 | map_string_string._check_kv = check_kv_func(string, string) 197 | map_string_string.new = function(init) 198 | return orm.new(map_string_string, init) 199 | end 200 | 201 | setmetatable(mail, { 202 | __tostring = function() 203 | return "schema_mail" 204 | end, 205 | }) 206 | mail.attach = mail_attach 207 | mail.cfg_id = integer 208 | mail.detail = map_string_string 209 | mail.mail_id = integer 210 | mail.send_role = mail_role 211 | mail.send_time = integer 212 | mail.title = map_string_string 213 | mail._parse_k = parse_k 214 | mail._check_k = check_k 215 | mail._check_kv = check_kv 216 | mail.new = function(init) 217 | return orm.new(mail, init) 218 | end 219 | local mail_fields = {"attach","cfg_id","detail","mail_id","send_role","send_time","title"} 220 | mail.fields = function() 221 | return mail_fields 222 | end 223 | 224 | setmetatable(mail_attach, { 225 | __tostring = function() 226 | return "schema_mail_attach" 227 | end, 228 | }) 229 | mail_attach.res_id = integer 230 | mail_attach.res_size = integer 231 | mail_attach.res_type = integer 232 | mail_attach._parse_k = parse_k 233 | mail_attach._check_k = check_k 234 | mail_attach._check_kv = check_kv 235 | mail_attach.new = function(init) 236 | return orm.new(mail_attach, init) 237 | end 238 | local mail_attach_fields = {"res_id","res_size","res_type"} 239 | mail_attach.fields = function() 240 | return mail_attach_fields 241 | end 242 | 243 | setmetatable(mail_role, { 244 | __tostring = function() 245 | return "schema_mail_role" 246 | end, 247 | }) 248 | mail_role.name = string 249 | mail_role.rid = integer 250 | mail_role._parse_k = parse_k 251 | mail_role._check_k = check_k 252 | mail_role._check_kv = check_kv 253 | mail_role.new = function(init) 254 | return orm.new(mail_role, init) 255 | end 256 | local mail_role_fields = {"name","rid"} 257 | mail_role.fields = function() 258 | return mail_role_fields 259 | end 260 | 261 | setmetatable(resource, { 262 | __tostring = function() 263 | return "schema_resource" 264 | end, 265 | }) 266 | resource.res_id = integer 267 | resource.res_size = integer 268 | resource._parse_k = parse_k 269 | resource._check_k = check_k 270 | resource._check_kv = check_kv 271 | resource.new = function(init) 272 | return orm.new(resource, init) 273 | end 274 | local resource_fields = {"res_id","res_size"} 275 | resource.fields = function() 276 | return resource_fields 277 | end 278 | 279 | setmetatable(role, { 280 | __tostring = function() 281 | return "schema_role" 282 | end, 283 | }) 284 | role._version = integer 285 | role.account = string 286 | role.create_time = integer 287 | role.game = string 288 | role.last_login_time = integer 289 | role.modules = role_modules 290 | role.name = string 291 | role.rid = integer 292 | role.server = string 293 | role._parse_k = parse_k 294 | role._check_k = check_k 295 | role._check_kv = check_kv 296 | role.new = function(init) 297 | return orm.new(role, init) 298 | end 299 | local role_fields = {"_version","account","create_time","game","last_login_time","modules","name","rid","server"} 300 | role.fields = function() 301 | return role_fields 302 | end 303 | 304 | setmetatable(map_integer_bag, { 305 | __tostring = function() 306 | return "schema_map_integer_bag" 307 | end, 308 | __index = function(t, k) 309 | return bag 310 | end, 311 | }) 312 | map_integer_bag._parse_k = parse_k_func(integer) 313 | map_integer_bag._check_k = check_k_func(integer) 314 | map_integer_bag._check_kv = check_kv_func(integer, bag) 315 | map_integer_bag.new = function(init) 316 | return orm.new(map_integer_bag, init) 317 | end 318 | 319 | setmetatable(role_bag, { 320 | __tostring = function() 321 | return "schema_role_bag" 322 | end, 323 | }) 324 | role_bag.bags = map_integer_bag 325 | role_bag._parse_k = parse_k 326 | role_bag._check_k = check_k 327 | role_bag._check_kv = check_kv 328 | role_bag.new = function(init) 329 | return orm.new(role_bag, init) 330 | end 331 | local role_bag_fields = {"bags"} 332 | role_bag.fields = function() 333 | return role_bag_fields 334 | end 335 | 336 | setmetatable(map_integer_mail, { 337 | __tostring = function() 338 | return "schema_map_integer_mail" 339 | end, 340 | __index = function(t, k) 341 | return mail 342 | end, 343 | }) 344 | map_integer_mail._parse_k = parse_k_func(integer) 345 | map_integer_mail._check_k = check_k_func(integer) 346 | map_integer_mail._check_kv = check_kv_func(integer, mail) 347 | map_integer_mail.new = function(init) 348 | return orm.new(map_integer_mail, init) 349 | end 350 | 351 | setmetatable(role_mail, { 352 | __tostring = function() 353 | return "schema_role_mail" 354 | end, 355 | }) 356 | role_mail._version = integer 357 | role_mail.mails = map_integer_mail 358 | role_mail._parse_k = parse_k 359 | role_mail._check_k = check_k 360 | role_mail._check_kv = check_kv 361 | role_mail.new = function(init) 362 | return orm.new(role_mail, init) 363 | end 364 | local role_mail_fields = {"_version","mails"} 365 | role_mail.fields = function() 366 | return role_mail_fields 367 | end 368 | 369 | setmetatable(role_modules, { 370 | __tostring = function() 371 | return "schema_role_modules" 372 | end, 373 | }) 374 | role_modules.bag = role_bag 375 | role_modules.mail = role_mail 376 | role_modules._parse_k = parse_k 377 | role_modules._check_k = check_k 378 | role_modules._check_kv = check_kv 379 | role_modules.new = function(init) 380 | return orm.new(role_modules, init) 381 | end 382 | local role_modules_fields = {"bag","mail"} 383 | role_modules.fields = function() 384 | return role_modules_fields 385 | end 386 | 387 | setmetatable(str2str, { 388 | __tostring = function() 389 | return "schema_str2str" 390 | end, 391 | }) 392 | str2str.key = string 393 | str2str.value = string 394 | str2str._parse_k = parse_k 395 | str2str._check_k = check_k 396 | str2str._check_kv = check_kv 397 | str2str.new = function(init) 398 | return orm.new(str2str, init) 399 | end 400 | local str2str_fields = {"key","value"} 401 | str2str.fields = function() 402 | return str2str_fields 403 | end 404 | 405 | return { 406 | bag = bag, 407 | map_integer_resource = map_integer_resource, 408 | mail = mail, 409 | map_string_string = map_string_string, 410 | mail_attach = mail_attach, 411 | mail_role = mail_role, 412 | resource = resource, 413 | role = role, 414 | role_bag = role_bag, 415 | map_integer_bag = map_integer_bag, 416 | role_mail = role_mail, 417 | map_integer_mail = map_integer_mail, 418 | role_modules = role_modules, 419 | str2str = str2str, 420 | } 421 | --------------------------------------------------------------------------------