一、概述
Skynet 提供了靈活的日志系統(tǒng),主要通過兩種方式實(shí)現(xiàn):
| 方式 | 示例 | 特點(diǎn) |
|---|---|---|
| 服務(wù)型日志 | globallog.lua |
作為獨(dú)立服務(wù),通過消息調(diào)用記錄日志 |
| 協(xié)議型日志 | userlog.lua |
通過注冊協(xié)議,攔截特定類型消息 |
核心概念:
-
服務(wù)注冊:
skynet.register()將服務(wù)注冊為可調(diào)用的實(shí)體 -
協(xié)議注冊:
skynet.register_protocol()擴(kuò)展消息處理能力 -
本地 vs 全局:服務(wù)名以
.開頭為本地注冊,否則為全局注冊
二、運(yùn)行說明
2.1 globallog 使用方式
配置方式:無需特殊配置,直接啟動(dòng)服務(wù)即可
-- 在 main.lua 中啟動(dòng)
skynet.newservice("globallog")
使用方式:
-- 方式1:使用本地注冊名
skynet.send(".log", "lua", "Hello World")
-- 方式2:使用全局注冊名
skynet.send("LOG", "lua", "Hello World")
2.2 userlog 使用方式
配置方式:在 config 文件中配置 logger
logger = "userlog"
logservice = "snlua"
logpath = "."
使用方式:
-- 通過 skynet.error 輸出日志
skynet.error("This is an error message")
-- 通過 TEXT 協(xié)議發(fā)送消息
skynet.send(service_addr, "text", "This is a text message")
三、架構(gòu)結(jié)構(gòu)圖
3.1 globallog 架構(gòu)
┌─────────────────────────────────────────────────────────────────┐
│ 服務(wù)型日志架構(gòu) │
└─────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Service A │ │ Service B │ │ Service C │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ skynet.send(".log") │ │
│ │ skynet.send("LOG") │
│ │ │ skynet.send(".log")
▼ ▼ ▼
┌───────────────────────────────────────────────────────────┐
│ GlobalLog Service │
│ ┌───────────────────────────────────────────────────┐ │
│ │ skynet.dispatch("lua", handler) │ │
│ │ └─ skynet.register(".log") │ │
│ │ └─ skynet.register("LOG") │ │
│ │ └─ print("[GLOBALLOG]", address, ...) │ │
│ └───────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
│
▼
┌──────────────┐
│ Console │
└──────────────┘
3.2 userlog 架構(gòu)
┌─────────────────────────────────────────────────────────────────┐
│ 協(xié)議型日志架構(gòu) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 消息分發(fā)層 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ PTYPE_TEXT → userlog.dispatch() │ │
│ │ PTYPE_SYSTEM → userlog.dispatch() │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────┐
│ userlog.lua │
│ - TEXT 協(xié)議處理 │
│ - SYSTEM 協(xié)議處理│
└──────────────────┘
│
▼
┌──────────────────┐
│ Console │
└──────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Service A │ │ Service B │ │ Service C │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ skynet.error(msg) │ skynet.send(addr, │
│ │ "text", msg) │
└─────────────────────┴─────────────────────┘
四、源碼解析
4.1 globallog.lua 解析
local skynet = require "skynet"
require "skynet.manager" -- import skynet.register
skynet.start(function()
-- 注冊消息分發(fā)函數(shù)
skynet.dispatch("lua", function(session, address, ...)
-- 打印日志:包含發(fā)送者地址和消息內(nèi)容
print("[GLOBALLOG]", skynet.address(address), ...)
end)
-- 注冊本地服務(wù)名(僅當(dāng)前節(jié)點(diǎn)可見)
skynet.register ".log"
-- 注冊全局服務(wù)名(整個(gè)集群可見)
skynet.register "LOG"
end)
關(guān)鍵技術(shù)點(diǎn):
| 代碼行 | 說明 |
|---|---|
require "skynet.manager" |
引入 skynet.register 函數(shù) |
skynet.dispatch("lua", ...) |
注冊 LUA 協(xié)議消息處理器 |
skynet.address(address) |
將服務(wù)地址轉(zhuǎn)換為可讀格式 |
skynet.register(".log") |
本地注冊,僅當(dāng)前節(jié)點(diǎn)可用 |
skynet.register("LOG") |
全局注冊,整個(gè)集群可用 |
4.2 userlog.lua 解析
local skynet = require "skynet"
require "skynet.manager"
-- 注冊 TEXT 協(xié)議(必須在 skynet.start 之前注冊)
skynet.register_protocol {
name = "text", -- 協(xié)議名稱
id = skynet.PTYPE_TEXT, -- 協(xié)議類型 ID(預(yù)定義常量)
unpack = skynet.tostring, -- 解包函數(shù):將二進(jìn)制轉(zhuǎn)為字符串
dispatch = function(_, address, msg)
-- 格式化輸出:地址 + 時(shí)間 + 消息
print(string.format(":%08x(%.2f): %s", address, skynet.time(), msg))
end
}
-- 注冊 SYSTEM 協(xié)議
skynet.register_protocol {
name = "SYSTEM",
id = skynet.PTYPE_SYSTEM,
unpack = function(...) return ... end, -- 不解包,原樣傳遞
dispatch = function()
-- 處理系統(tǒng)信號(hào)(如 SIGHUP)
print("SIGHUP")
end
}
skynet.start(function()
-- 用戶日志服務(wù)不需要做任何初始化
-- 協(xié)議已在啟動(dòng)前注冊完成
end)
關(guān)鍵技術(shù)點(diǎn):
| 代碼行 | 說明 |
|---|---|
skynet.register_protocol {...} |
注冊自定義協(xié)議 |
name = "text" |
協(xié)議名稱,用于調(diào)試和標(biāo)識(shí) |
id = skynet.PTYPE_TEXT |
協(xié)議 ID,必須使用預(yù)定義值 |
unpack = skynet.tostring |
將二進(jìn)制消息轉(zhuǎn)為字符串 |
skynet.time() |
獲取當(dāng)前時(shí)間(單位:秒) |
PTYPE_SYSTEM |
系統(tǒng)信號(hào)協(xié)議,處理進(jìn)程信號(hào) |
4.3 協(xié)議注冊時(shí)機(jī)
重要注意:協(xié)議注冊必須在 skynet.start() 之前完成:
-- ? 正確:在 start 之前注冊協(xié)議
skynet.register_protocol {
name = "text",
id = skynet.PTYPE_TEXT,
...
}
skynet.start(function()
-- 服務(wù)啟動(dòng)邏輯
end)
-- ? 錯(cuò)誤:在 start 之后注冊無效
skynet.start(function()
skynet.register_protocol { ... } -- 不會(huì)生效
end)
五、實(shí)用舉例
5.1 基礎(chǔ)日志使用
示例1:使用 globallog
-- main.lua
local skynet = require "skynet"
skynet.start(function()
-- 啟動(dòng)全局日志服務(wù)
local log = skynet.newservice("globallog")
-- 使用本地名稱發(fā)送日志
skynet.send(".log", "lua", "Server started")
-- 使用全局名稱發(fā)送日志
skynet.send("LOG", "lua", "User login", "user123")
skynet.exit()
end)
示例2:使用 userlog
-- main.lua
local skynet = require "skynet"
skynet.start(function()
-- userlog 會(huì)自動(dòng)處理 TEXT 協(xié)議消息
skynet.error("This is a standard error message")
-- 直接發(fā)送 TEXT 協(xié)議消息
local addr = skynet.self()
skynet.send(addr, "text", "This is a text protocol message")
skynet.exit()
end)
5.2 自定義日志服務(wù)
場景:創(chuàng)建一個(gè)帶日志級(jí)別和時(shí)間戳的日志服務(wù)
-- custom_logger.lua
local skynet = require "skynet"
require "skynet.manager"
local LOG_LEVEL = {
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4
}
local current_level = LOG_LEVEL.DEBUG
local function get_time()
return os.date("%Y-%m-%d %H:%M:%S")
end
local function log(level, level_name, ...)
if level >= current_level then
print(string.format("[%s][%s] %s", get_time(), level_name, table.concat({...}, " ")))
end
end
skynet.start(function()
skynet.dispatch("lua", function(session, address, cmd, ...)
cmd = cmd:upper()
if cmd == "DEBUG" then
log(LOG_LEVEL.DEBUG, "DEBUG", ...)
elseif cmd == "INFO" then
log(LOG_LEVEL.INFO, "INFO", ...)
elseif cmd == "WARN" then
log(LOG_LEVEL.WARN, "WARN", ...)
elseif cmd == "ERROR" then
log(LOG_LEVEL.ERROR, "ERROR", ...)
elseif cmd == "SETLEVEL" then
local level = ...
current_level = LOG_LEVEL[level:upper()] or LOG_LEVEL.DEBUG
end
end)
skynet.register ".logger"
end)
使用方式:
local logger = skynet.newservice("custom_logger")
skynet.send(".logger", "lua", "INFO", "Server started")
skynet.send(".logger", "lua", "DEBUG", "Debug info")
skynet.send(".logger", "lua", "SETLEVEL", "WARN") -- 設(shè)置日志級(jí)別
5.3 文件日志輸出
場景:將日志寫入文件
-- file_logger.lua
local skynet = require "skynet"
require "skynet.manager"
local log_file
skynet.start(function()
local logpath = skynet.getenv("logpath") or "."
local filename = logpath .. "/server.log"
log_file = io.open(filename, "a")
skynet.dispatch("lua", function(session, address, ...)
local msg = table.concat({...}, " ")
local time = os.date("%Y-%m-%d %H:%M:%S")
local line = string.format("[%s][%s] %s\n", time, skynet.address(address), msg)
if log_file then
log_file:write(line)
log_file:flush() -- 立即寫入
end
-- 同時(shí)輸出到控制臺(tái)
print(line:sub(1, -2)) -- 去掉換行符
end)
skynet.register ".filelogger"
end)
function exit()
if log_file then
log_file:close()
end
end
六、關(guān)鍵技術(shù)對比
6.1 globallog vs userlog
| 特性 | globallog | userlog |
|---|---|---|
| 類型 | 服務(wù)(Service) | 協(xié)議處理器(Protocol Handler) |
| 注冊方式 | skynet.register() |
skynet.register_protocol() |
| 觸發(fā)方式 | 主動(dòng)調(diào)用 skynet.send()
|
發(fā)送特定協(xié)議消息自動(dòng)觸發(fā) |
| 適用場景 | 應(yīng)用層日志記錄 | 系統(tǒng)級(jí)消息處理 |
| 靈活性 | 高(可自定義命令) | 中(協(xié)議固定) |
| 性能 | 需消息傳遞 | 直接處理,更快 |
6.2 本地注冊 vs 全局注冊
| 特性 | 本地注冊(.name) | 全局注冊(NAME) |
|---|---|---|
| 作用域 | 當(dāng)前節(jié)點(diǎn) | 整個(gè)集群 |
| 注冊函數(shù) | skynet.register(".name") |
skynet.register("NAME") |
| 查找方式 | skynet.localname() |
skynet.queryservice() |
| 名稱限制 | 無特殊限制 | 長度 < 16,不能是數(shù)字 |
| 性能 | 本地查找,更快 | 需經(jīng)過集群路由 |
6.3 預(yù)定義協(xié)議類型
| 協(xié)議 ID | 名稱 | 用途 |
|---|---|---|
PTYPE_TEXT |
text | 文本消息 |
PTYPE_SYSTEM |
SYSTEM | 系統(tǒng)信號(hào) |
PTYPE_LUA |
lua | Lua 消息(默認(rèn)) |
PTYPE_RESPONSE |
response | 響應(yīng)消息 |
PTYPE_ERROR |
error | 錯(cuò)誤消息 |
PTYPE_CLIENT |
client | 客戶端協(xié)議 |
七、日志系統(tǒng)工作原理
7.1 消息分發(fā)流程
消息發(fā)送 消息路由 消息處理
│ │ │
│ skynet.send(addr, type) │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 消息封裝 │ │ 協(xié)議路由 │ │ 協(xié)議處理 │
│ pack(msg) │ ───────→ │ 查找協(xié)議ID │ ───────→ │ unpack(msg) │
└──────────────┘ │ 定位目標(biāo)服務(wù) │ │ dispatch() │
└──────────────┘ └──────────────┘
7.2 logger 配置機(jī)制
當(dāng)配置 logger = "userlog" 時(shí):
-- config 文件
logger = "userlog" -- 指定日志服務(wù)名
logservice = "snlua" -- 指定服務(wù)類型
logpath = "." -- 日志路徑
-- 底層處理流程
skynet.error(msg)
│
├─→ 如果配置了 logger
│ └─→ 發(fā)送到 logger 服務(wù)
│ └─→ logger 服務(wù)處理并輸出
│
└─→ 否則直接輸出到控制臺(tái)
八、最佳實(shí)踐建議
8.1 選擇合適的日志方式
-- 場景1:簡單日志記錄 → 使用 globallog
skynet.send(".log", "lua", "Message")
-- 場景2:系統(tǒng)級(jí)消息 → 使用 userlog(TEXT 協(xié)議)
skynet.send(addr, "text", "Message")
-- 場景3:結(jié)構(gòu)化日志 → 自定義日志服務(wù)
skynet.send(".logger", "lua", "INFO", "Message")
8.2 日志服務(wù)部署策略
生產(chǎn)環(huán)境建議:
┌──────────────────────────────────────────────────────────┐
│ 日志服務(wù)集群 │
├──────────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ logger1 │ │ logger2 │ │ logger3 │ │
│ │ (文件) │ │ (文件) │ │ (控制臺(tái)) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
│ └──────────┼──────────┘ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 日志聚合服務(wù) │ │
│ │ (定時(shí)輪轉(zhuǎn)) │ │
│ └──────────────┘ │
└──────────────────────────────────────────────────────────┘
8.3 性能優(yōu)化建議
-
異步日志:使用
skynet.send()而非skynet.call() - 批量寫入:累積多條日志后一次性寫入文件
- 日志級(jí)別控制:生產(chǎn)環(huán)境關(guān)閉 DEBUG 級(jí)別
- 避免阻塞:日志處理不要做耗時(shí)操作
九、常見問題與解決方案
9.1 日志服務(wù)找不到
問題現(xiàn)象:skynet.send(".log", ...) 報(bào)錯(cuò) "service not found"
解決方案:
-- 確保先啟動(dòng)日志服務(wù)
local log = skynet.newservice("globallog")
-- 或者使用 pcall 保護(hù)
local ok = pcall(skynet.send, ".log", "lua", "msg")
if not ok then
print("Log service not available")
end
9.2 協(xié)議注冊不生效
問題現(xiàn)象:注冊的協(xié)議沒有處理消息
解決方案:
-- 確保在 skynet.start 之前注冊協(xié)議
skynet.register_protocol {
name = "text",
id = skynet.PTYPE_TEXT,
...
}
skynet.start(function()
-- 協(xié)議已生效
end)
9.3 日志文件過大
解決方案:
-- 實(shí)現(xiàn)日志輪轉(zhuǎn)
local function rotate_log()
local current = os.date("%Y%m%d")
if current ~= last_date then
-- 關(guān)閉舊文件,打開新文件
log_file:close()
log_file = io.open("log_" .. current .. ".txt", "a")
last_date = current
end
end
十、總結(jié)
10.1 核心要點(diǎn)
- globallog:服務(wù)型日志,通過消息調(diào)用記錄,靈活可控
- userlog:協(xié)議型日志,自動(dòng)攔截 TEXT 協(xié)議消息
-
協(xié)議注冊:必須在
skynet.start()之前完成 -
服務(wù)注冊:本地注冊(
.前綴)vs 全局注冊
10.2 選擇建議
| 場景 | 推薦方案 |
|---|---|
| 應(yīng)用層業(yè)務(wù)日志 | globallog 或自定義日志服務(wù) |
| 系統(tǒng)級(jí)消息處理 | userlog(TEXT/SYSTEM 協(xié)議) |
| 集群環(huán)境 | 全局注冊的日志服務(wù) |
| 高性能要求 | 自定義異步日志服務(wù) |