下面给你整理一份 《skynet.call 使用详细解析(超全版)》,从底层原理到使用示例,让你彻底弄懂 Skynet 微服务框架中最核心的服务间通信 API。
一、skynet.call 是什么?
在 Skynet 框架中(Lua 版分布式游戏服务器框架),所有逻辑都是服务(service)之间通过消息通信完成的。
消息分两类:
- 异步消息:
skynet.send - 同步消息:
skynet.call(重点)
skynet.call 是同步 RPC 调用,会阻塞等待返回结果:
local ret = skynet.call(address, "lua", "methodName", arg1, arg2)
调用后当前 coroutine 会挂起,直到目标服务处理完并返回结果。
二、skynet.call 的参数解析
skynet.call 的原型:
skynet.call(addr, typename, ...)
参数解释:
| 参数 | 说明 |
|---|---|
addr | 服务地址,如 :100004 或 serviceName |
typename | 消息协议类型(常用 lua) |
... | 实际发送的参数 |
例如:
local result = skynet.call("userCenter", "lua", "getUserInfo", uid)
三、skynet.call 和 skynet.send 的区别
| 功能 | skynet.send | skynet.call |
|---|---|---|
| 是否阻塞 | ❌ 不阻塞 | ✔ 阻塞等待返回 |
| 是否有返回值 | ❌ 无 | ✔ 有 |
| 使用场景 | 命令、事件推送 | 需要返回数据的 RPC |
| 性能 | 更轻量 | 略有开销 |
简单理解:
send:发通知call:发请求并等回复
四、skynet.call 的底层原理(工作流程)
- 生成 sessionid
- 类似于请求唯一 ID
- 挂起当前 coroutine
- 执行 call 的协程被挂起,进入等待队列
- 将请求包装成消息发送给目标服务
- 消息结构:
{source, session, data}
- 消息结构:
- 目标服务处理消息并返回应答(response)
- 从 session 唤醒协程,并把返回值恢复到协程中
- 继续执行 call 后面的代码
过程是完全异步非阻塞的,但对调用者表现为同步。
五、服务端如何响应 skynet.call?
你必须在服务端使用:
方式 1:skynet.dispatch 处理 RPC
local skynet = require "skynet"
skynet.start(function()
skynet.dispatch("lua", function(session, address, cmd, ...)
if cmd == "add" then
local a, b = ...
skynet.ret(skynet.pack(a + b)) -- 返回结果
end
end)
end)
方式 2:使用 skynet.response
skynet.dispatch("lua", function(session, address, cmd, ...)
local reply = skynet.response()
if cmd == "hello" then
reply(true, skynet.pack("Hello from server"))
end
end)
两种方法都可以正常响应调用者的 call。
六、客户端调用示例
local ret = skynet.call(serviceAddr, "lua", "add", 10, 20)
print(ret) -- 30
非常常用。
七、skynet.call 的返回值详解
- 多返回值:
skynet.pack()支持多值 - 结构体:通过
skynet.pack/table.pack完整封装 - 错误时:
- 如果服务地址不存在 → 报错
- 如果服务未返回 session → 死锁(call hang)
- 如果服务崩溃 → 抛异常
建议统一使用 pcall 捕获:
local ok, ret = pcall(skynet.call, addr, "lua", "getInfo")
if not ok then
skynet.error("RPC call failed:", ret)
end
八、skynet.call 超时机制
Skynet 默认没有超时机制,如果目标服务不返回,调用会永久等待。
常见防止死锁的方法:
- 控制子服务逻辑时间
- 使用 skynet.timeout 自己模拟超时
- 用专门的 timer / watchdog 服务监控
九、call 死锁场景分析(重点)
下面这个情况很危险:
服务 A → call 服务 B
服务 B 又 call 回 A
典型死锁:
A.call(B)
B.dispatch → call(A)
A.wait session → 等 B 返回
A 等 B → B 等 A → 死锁
解决方式:
- 调整成 send + call
- 或拆分服务逻辑
- 或使用 queue(如 skynet.queue)保证同步锁
十、skynet.call 的使用最佳实践
✔ 定义 API 层(模块化 RPC 入口)
例如 userService.lua:
local CMD = {}
function CMD.get(uid)
return db.query(uid)
end
function CMD.add(uid, delta)
return db.update(uid, delta)
end
return CMD
✔ 使用 skynet.dispatch 来分发
skynet.dispatch("lua", function(session, addr, cmd, ...)
local f = CMD[cmd]
skynet.ret(skynet.pack(f(...)))
end)
✔ 调用端统一封装
local user = {}
function user.get(uid)
return skynet.call(".user", "lua", "get", uid)
end
return user
十一、完整示例(从启动到调用)
1. 服务端 user.lua
local skynet = require "skynet"
local CMD = {}
function CMD.getName(uid)
return "User_" .. uid
end
skynet.start(function()
skynet.dispatch("lua", function(session, source, cmd, ...)
local f = CMD[cmd]
skynet.ret(skynet.pack(f(...)))
end)
end)
2. 注册服务
skynet.newservice("user")
3. 客户端 call 调用
local name = skynet.call(".user", "lua", "getName", 1001)
print(name)
输出:
User_1001
十二、总结
| 功能 | 说明 |
|---|---|
| 作用 | 同步 RPC 调用 |
| 阻塞 | ✓ 阻塞等待结果 |
| 使用场景 | 需要服务器返回值的情况 |
| 实现原理 | session + coroutine 挂起与恢复 |
| 注意点 | 死锁、无返回的永久等待 |
skynet.call 是 Skynet 的基础通信方式,也是构建分布式游戏服务器逻辑的关键。
发表回复