skynet中的每一个服务都有一个独立的lua虚拟机,逻辑上服务之间是相互隔离的,那么你就不能使用传统意义上的LUA全局变量来进行服务间通信了。
在skynet中服务之间可以通过skynet消息调度机制来完成通信。skynet中的服务是基于actor模型设计出来的,每个服务都可以接收消息,处理消息,发送应答消息。
每条 skynet 消息由 6 部分构成:消息类型、session 、发起服务地址 、接收服务地址 、消息 C 指针、消息长度。
7.1消息类型
在 skynet 中消息分为多种类别,对应的也有不同的编码方式(即协议),消息类型的宏定义可以查看 skynet.h 中:
//表示一个回应包 //广播消息 //用来处理网络客户端的请求消息 //系统消息 //跨节点消息 //套接字消息 //错误消息,一般服务退出的时候会发送error消息给关联的服务 //lua类型的消息,最常用 //snax服务消息
上面的消息类型有多种,但是最常用的是PTYPE_LUA,对应到lua层,叫做lua消息
,大部分服务一般使用这种消息,默认情况下,PTYPE_REPSONSE、PTYPE_ERROR、PTYPE_LUA三种消息类型已经注册(查看源码了解情况),如果想使用其他的消息类型,需要自己显示注册消息 类型。
7.2 注册消息处理函数
当我们需要在一个服务中监听指定类型的消息,就需要在服务启动的时候先注册该类型的消息的监听,通常是在服务的入口函数 skynet.start
处通过调用 skynet.dispatch
来注册绑定:
--服务启动入口
skynet.start(function()
--注册"lua"类型消息的回调函数
skynet.dispatch("lua", function(session, address, ...)
dosomething(...)
end)
end)
一旦注册成功,那么只要是发送给这个服务的消息是lua类型消息,那么都会调用我们注册的function进行处理。
例如testluamsg.lua:
skynet = require "skynet"
require "skynet.manager"
local function dosomething(session, address, ...)
skynet.error("session", session)
skynet.error("address", skynet.address(address))
local args = {...}
for i,v in pairs(args) do
skynet.error("arg"..i..":", v)
end
end
skynet.start(function()
--注册"lua"类型消息的回调函数
skynet.dispatch("lua", function(session, address, ...)
dosomething(session, address, ...)
end)
skynet.register(".testluamsg")
end)
7.3 打包与解包消息
skynet中的消息在发送之前必须要把参数进行打包,然后才发送,接受方收到消息后会自动根据指定的解包函数进行解包,最常用的打包解包函数为skynet.pack与skynet.unpack.
skynet.pack(...)
打包后,会返回两个参数,一个是C指针msg指向数据包的起始地址,sz一个是数据包的长度。msg指针的内存区域是动态申请的。
skynet.unpack(msg, sz)
解包后,会返回一个参数列表。需要注意这个时候C指针msg指向的内存不会释放掉。如果msg有实际的用途,skynet框架会帮你在合适的地方释放掉,如果没有实际的用途,自己想释放掉可以使用skynet.trash(msg, sz)
释放掉。
例如:
local msg, sz = skynet.pack("nengzhong", 8.8, false)
local arg1, arg2, arg3 = skynet.unpack(msg, sz)
skynet.error(arg1, arg2, arg3)
local arglist = {skynet.unpack(msg, sz)}
for i,v in pairs(arglist) do
skynet.error("arg"..i..":", v)
end
skynet.trash(msg, sz) --没有用到skynet框架中,所以用完了需要自己释放一下
注意:skynet.pack返回的msg与sz只用在skynet.unpack中使用才有意义。不要这么使用table.unpack(msg, sz).
7.4 发送消息的方法
7.4.1 发送无需响应的消息
--用 type 类型向 addr 发送未打包的消息。该函数会自动把...参数列表进行打包,默认情况下lua消息使用skynet.pack打包。addr可以是服务句柄也可以是别名。
skynet.send(addr, type, ...)
--用 type 类型向 addr 发送一个打包好的消息。addr可以是服务句柄也可以是别名。
skynet.rawsend(addr, type, msg, sz)
例如testsendmsg.lua:
skynet = require "skynet"
skynet.start(function()
skynet.register(".testsendmsg")
local testluamsg = skynet.localname(".testluamsg")
--发送lua类型的消息给testluamsg,发送成功后立即返回,r的值为0
local r = skynet.send(testluamsg, "lua", 1, "nengzhong", true) --申请了C内存(msg,sz)已经用与发送,所以不用自己再释放内存了。
skynet.error("skynet.send return value:", r)
--通过skynet.pack来打包发送
r = skynet.rawsend(testluamsg, "lua", skynet.pack(2, "nengzhong", false)) --申请了C内存(msg,sz)已经用与发送,所以不用自己再释放内存了。
skynet.error("skynet.rawsend return value:", r)
end)
先运行testluamsg.lua再运行testsendmsg.lua, 结果如下:
$ ./skynet examples/config testluamsg [:0100000a] LAUNCH snlua testluamsg testsendmsg [:0100000b] LAUNCH snlua testsendmsg [:0100000b] skynet.send return value: 0 #发送完消息马上返回 [:0100000b] skynet.rawsend return value: 0 #发送完消息马上返回 [:0100000a] session 0 #接收端接到消息 [:0100000a] address :0100000b [:0100000a] arg1: 1 [:0100000a] arg2: nengzhong [:0100000a] arg3: true [:0100000a] session 0 [:0100000a] address :0100000b [:0100000a] arg1: 2 [:0100000a] arg2: nengzhong [:0100000a] arg3: false
上面的代码我们隐式或显示调用了skynet.pack,一共申请了两段C内存,但是并不需要我们释放C内存。因为已经把这段内存用于发送了,skynet会等到该消息处理完后,自动释放掉它的内存。
7.4.2 发送必须响应的消息
--用默认函数打包消息,向addr发送type类型的消息并等待返回响应,并对回应信息进行解包。(自动打包与解包。)
skynet.call(addr, type, ...)
--直接向addr发送type类型的msg,sz并等待返回响应,不对回应信息解包。(需要自己打包与解包)
skynet.rawcall(addr, type, msg, sz)
例如testcallmsg.lua:
skynet = require "skynet"
skynet.start(function()
skynet.register(".testcallmsg")
--发送lua类型的消息给service,发送成功,该函数将阻塞等待响应返回,r的值为响应的返回值
local r = skynet.call(".testluamsg", "lua", 1, "nengzhong", true)
skynet.error("skynet.call return value:", r)
--通过skynet.pack来打包发送,返回的值也需要自己解包
r = skynet.unpack(skynet.rawcall(".testluamsg", "lua", skynet.pack(2, "nengzhong", false)))
skynet.error("skynet.rawcall return value:", r)
end)
先运行testluamsg.lua再运行testcallmsg.lua, 结果如下:
$ ./skynet examples/config testluamsg [:0100000a] LAUNCH snlua testluamsg testcallmsg [:0100000b] LAUNCH snlua testcallmsg [:0100000a] session 2 #只发送出第一个消息,现在已经阻塞住,由于testluamsg并没有返回应答。 [:0100000a] address :0100000b [:0100000a] arg1: 1 [:0100000a] arg2: nengzhong [:0100000a] arg3: true
7.5 响应消息的方法
对lua消息响应的时候,使用的是PTYPE_REPSONSE这种消息,也是需要打包,打包的时候必须与接收到的消息打包方法一致。
skynet.ret --目标服务消息处理后需要通过该函数将结果返回
skynet.retpack(...) --将消息用skynet.pack 打包,并调用 ret 回应。
修改testluamsg.lua
skynet = require "skynet"
require "skynet.manager"
local function dosomething(session, address, ...)
skynet.error("session", session)
skynet.error("address", skynet.address(address))
local args = {...}
for i,v in pairs(args) do
skynet.error("arg"..i..":", v)
end
return 100, false
end
skynet.start(function()
--注册"lua"类型消息的回调函数
skynet.dispatch("lua", function(session, address, ...)
--skynet.sleep(500)
skynet.retpack(dosomething(session, address, ...)) --申请响应消息C内存
--或者skynet.ret(skynet.pack(dosomething(session, address, ...)))
end)--skynet.dispatch完成后,释放调用接收消息C内存
skynet.register(".testluamsg")
end)
先运行testluamsg.lua再运行testcallmsg.lua, 结果如下:
$ ./skynet examples/config testluamsg [:0100000a] LAUNCH snlua testluamsg testcallmsg [:0100000b] LAUNCH snlua testcallmsg [:0100000a] session 2 [:0100000a] address :0100000b [:0100000a] arg1: 1 [:0100000a] arg2: nengzhong [:0100000a] arg3: true [:0100000b] skynet.call return value: 100 #第一个call返回,返回值为100 [:0100000a] session 3 #通过skynet.call方式发送消息session才起作用了 [:0100000a] address :0100000b [:0100000a] arg1: 2 [:0100000a] arg2: nengzhong [:0100000a] arg3: false [:0100000b] skynet.rawcall return value: 100 false #第二个call也返回
注意:
1、应答消息打包的时候,打包方法必须与接收消息的打包方式一致。
2、skynet.ret不需要指定应答消息是给哪个服务的。
因为每次接收到消息时,服务都会启动一个协程来处理,并且把这个协程与源服务地址绑定在一起了(其实就是把协程句柄作为key,源服务地址为value,记录在一张表中)。需要响应的时候可以根据协程句柄找到源服务地址。
7.6 lua消息收发综合应用
mydb.lua
local skynet = require "skynet"
require "skynet.manager" -- import skynet.register
local db = {}
local command = {}
function command.GET(key)
return db[key]
end
function command.SET(key, value)
db[key] = value
end
skynet.start(function()
--注册该服务的lua消息回调函数
skynet.dispatch("lua", function(session, address, cmd, ...)
--接受到的第一参数作为命令使用
cmd = cmd:upper()
local f = command[cmd] --查询cmd命令的具体处理方法
if f then
--执行查询到的方法,并且通过skynet.ret将执行结果返回
skynet.retpack(f(...))
else
skynet.error(string.format("Unknown command %s", tostring(cmd)))
end
end)
skynet.register ".mydb" --给当前服务注册一个名字,便于其他服务给当前服务发送消息
end)
testmydb.lua
local skynet = require "skynet"
local key,value = ...
function task()
--给.mydb服务发送一个lua消息,命令为set
--发送成功后直接返回,不管接收消息端的是否调用skynet.ret,skynet.send的返回值都为0
local r = skynet.send(".mydb", "lua", "set", key, value)
skynet.error("mydb set Test", r)
--给.mydb服务发送一个lua消息,命令为get
--如果接收端没有调用skynet.ret,则skynet.call将一直阻塞
r = skynet.call(".mydb", "lua", "get", key)
skynet.error("mydb get Test", r)
skynet.exit()
end
skynet.start(function()
skynet.fork(task)
end)
7.7 session的意义
session只有在使用skynet.call或者skynet.rawcall发送消息的时候才有意义。
因为有可能一个服务开了多个协程去call消息,然后多个协程都在等待应答消息,回来了一个应答,那么到底是唤醒哪个协程,就可以通过session来判断了,skynet中的session能保证本服务中发出的消息是唯一的。消息与响应一一对应起来。
例如
echoluamsg.lua
skynet = require "skynet"
skynet.start(function()
--注册"lua"类型消息的回调函数
skynet.dispatch("lua", function(session, address, msg)
skynet.sleep(math.random(100, 500))
skynet.retpack(msg:upper())
end)
skynet.register(".echoluamsg")
end)
testforkcall.lua:
skynet = require "skynet"
local function task(id)
for i = 1,5 do
skynet.error("task"..id .." call")
skynet.error("task"..id .." return:", skynet.call(".echoluamsg", "lua", "task"..id))
end
end
skynet.start(function()
skynet.fork(task, 1) --开两个线程去执行
skynet.fork(task, 2)
end)
先运行echoluamsg,再运行testforkcall,结果如下:
testforkcall [:0100000b] LAUNCH snlua testforkcall [:0100000b] task1 call [:0100000b] task2 call [:0100000b] task2 return: TASK2 #消息能够一一对应在一起 [:0100000b] task2 call [:0100000b] task1 return: TASK1 [:0100000b] task1 call [:0100000b] task2 return: TASK2 [:0100000b] task2 call [:0100000b] task1 return: TASK1 [:0100000b] task1 call [:0100000b] task1 return: TASK1 [:0100000b] task1 call [:0100000b] task2 return: TASK2 [:0100000b] task2 call [:0100000b] task1 return: TASK1 [:0100000b] task1 call [:0100000b] task1 return: TASK1 [:0100000b] task2 return: TASK2 [:0100000b] task2 call [:0100000b] task2 return: TASK2
7.8 使用skynet.response响应消息
在使用skynet.ret或者skynet.retpack进行应答时,必须要保证与接受请求时在同一个协程中(源服务地址与协程句柄已经一一对应),也就是说在哪个协程接受的请求也必须在这个协程去做响应。看下面的一个例子:
errormydb.lua
local skynet = require "skynet"
require "skynet.manager" -- import skynet.register
local db = {}
local command = {}
function command.GET(key)
return db[key]
end
function command.SET(key, value)
db[key] = value
end
skynet.start(function()
skynet.dispatch("lua", function(session, address, cmd, ...)
skynet.error("lua dispatch ", coroutine.running()) --这个协程接收消息的
skynet.fork(function(cmd, ...) --开启一个新的协程来处理响应
skynet.error("fork ", coroutine.running())
cmd = cmd:upper()
local f = command[cmd]
if f then
skynet.retpack(f(...))
else
skynet.error(string.format("Unknown command %s", tostring(cmd)))
end
end, cmd, ...)
end)
skynet.register ".mydb"
end)
运行效果:
$ ./skynet examples/config errormydb #先运行errormydb [:0100000a] LAUNCH snlua errormydb testmydb name nengzhong #再运行testmydb [:0100000b] LAUNCH snlua testmydb name nengzhong [:0100000b] mydb set Test 0 [:0100000a] lua dispatch thread: 0x7ff359169088 false [:0100000a] fork thread: 0x7ff35928e0c8 false [:0100000a] lua call [100000b to :100000a : 0 msgsz = 19] error : ./lualib/skynet.lua:534: ./lualib/skynet.lua:178: dest address type (nil) must be a string or number. #报错误 stack traceback: [C]: in function 'assert' ./lualib/skynet.lua:534: in function 'skynet.manager.dispatch_message' [:0100000a] lua dispatch thread: 0x7ff359169088 false [:0100000a] fork thread: 0x7ff35928e1a8 false [:0100000a] lua call [100000b to :100000a : 2 msgsz = 9] error : ./lualib/skynet.lua:534: ./lualib/skynet.lua:178: dest address type (nil) must be a string or number. stack traceback: [C]: in function 'assert' ./lualib/skynet.lua:534: in function 'skynet.manager.dispatch_message'
在skynet中,当一个服务收到一个消息的时候,会启动一个协程来处理,并且把协程句柄与发送消息的服务地址进行一一对应记录在table中,当需要响应时,就使用当前调用skynet.ret
的协程句柄去table中查询对应的服务地址,然后把消息发给这个服务地址。
如果开了一个新的协程去调用skynet.ret,那么这个时候使用新启动的协程句柄去查询服务地址,肯定是查不到的。
当不想接受请求与响应请求在同一个协程中完成时,我可以使用response替代ret。
local skynet = require "skynet"
--参数pack指定应答打包函数,不填默认使用skynet.pack, 必须根据接收到消息的打包函数一致
--返回值是一个闭包函数
local response = skynet.response(pack)
--闭包函数使用方式
--参数ok的值可以是 "test"、true、false,为"test"时表示检查接收响应的服务是否存在,为true时表示发送应答PTYPE_RESPONSE,为false时表示发送PTYPE_ERROR错误消息。
response(ok, ...)
代码如下:
local skynet = require "skynet"
require "skynet.manager" -- import skynet.register
local db = {}
local command = {}
function command.GET(key)
return db[key]
end
function command.SET(key, value)
db[key] = value
end
skynet.start(function()
skynet.dispatch("lua", function(session, address, cmd, ...)
--先把发送服务地址以及session打包到闭包中,可以在任意地方调用
local response = skynet.response(skynet.pack) --指定打包函数,必须根据接收到的消息打包函数一致
skynet.fork(function(cmd, ...) --开启一个新的协程来处理响应
skynet.sleep(500)
cmd = cmd:upper()
local f = command[cmd]
if f then
response(true, f(...)) --第一个参数true表示应答成功,false则应答个错误消息
else
skynet.error(string.format("Unknown command %s", tostring(cmd)))
end
end, cmd, ...)
end)
skynet.register ".mydb"
end)
把发送服务地址以及session打包到闭包中,就可以在任意地方调用了。尽量多用skynet.response进行应答。
7.9 skynet.call失败的情况
当一个服务发起请求skynet.call
后等待应答,但是响应服务却退出了(调用skynet.exit
) ,响应服务退出的时候,会自动给未答复的请求发送一个error
消息,告诉它可以从skynet.call阻塞返回了,请求的服务会直接报一个错误。
noresponse.lua如下:
local skynet = require "skynet"
require "skynet.manager" -- import skynet.register
skynet.start(function()
skynet.dispatch("lua", function(session, address, cmd, ...)
skynet.exit() --在这里退出了服务
end)
skynet.register ".noresponse"
end)
testnoresponse.lua如下:
local skynet = require "skynet"
function task()
r = skynet.call(".noresponse", "lua", "get")
skynet.error("get Test", r)
skynet.exit()
end
skynet.start(function()
skynet.fork(task)
end)
运行结果:
$ ./skynet examples/config noresponse [:01000010] LAUNCH snlua noresponse testnoresponse [:01000012] LAUNCH snlua testnoresponse [:01000010] KILL self [:01000012] lua call [1000010 to :1000012 : 2 msgsz = 0] error : ./lualib/skynet.lua:534: ./lualib/skynet.lua:156: ./lualib/skynet.lua:391: call failed #call报错 stack traceback: [C]: in function 'error' ./lualib/skynet.lua:391: in upvalue 'yield_call' ./lualib/skynet.lua:402: in function 'skynet.call' ./my_workspace/testnoresponse.lua:4: in function 'task' ./lualib/skynet.lua:468: in upvalue 'f' ./lualib/skynet.lua:106: in function <./lualib/skynet.lua:105> stack traceback: [C]: in function 'assert' ./lualib/skynet.lua:534: in function 'skynet.dispatch_message'
7.10 服务重入问题
同一个 skynet 服务中的一条消息处理中,如果调用了一个阻塞 API ,那么它会被挂起。挂起过程中,这个服务可以响应其它消息。这很可能造成时序问题,要非常小心处理。
换句话说,一旦你的消息处理过程有外部请求,那么先到的消息未必比后到的消息先处理完。且每个阻塞调用之后,服务的内部状态都未必和调用前的一致(因为别的消息处理过程可能改变状态)。
来看一个服务重入的情况:
改造一下mydb.lua
local skynet = require "skynet"
require "skynet.manager" -- import skynet.register
local db = {}
local command = {}
function command.GET(key)
skynet.sleep(1000) --这里加了一个阻塞函数
return db[key]
end
function command.SET(key, value)
db[key] = value
end
skynet.start(function()
skynet.dispatch("lua", function(session, address, cmd, ...)
cmd = cmd:upper()
local f = command[cmd]
if f then
skynet.retpack(f(...))
else
skynet.error(string.format("Unknown command %s", tostring(cmd)))
end
end)
skynet.register ".mydb"
end)
testmydb.lua还是一样
local skynet = require "skynet"
local key,value = ...
function task()
local r = skynet.send(".mydb", "lua", "set", key, value)
skynet.error("mydb set Test", r)
r = skynet.call(".mydb", "lua", "get", key)
skynet.error("mydb get Test", r)
skynet.exit()
end
skynet.start(function()
skynet.fork(task)
end)
运行情况:
$ ./skynet examples/config mydb #先启动mydb [:01000010] LAUNCH snlua mydb testmydb name xm #再启动一个testmydb设置设置键值对name=xm,并获取name的值 [:01000012] LAUNCH snlua testmydb name xm [:01000012] mydb set Test 0 testmydb name xh #再启动一个testmydb设置设置键值对name=xh,并获取name值 [:01000019] LAUNCH snlua testmydb name xh [:01000019] mydb set Test 0 [:01000012] mydb get Test xh #获取到的值都是xh,我们希望获取的是xm [:01000012] KILL self [:01000019] mydb get Test xh #获取到的值都是xh [:01000019] KILL self
上面出现的情况,就是因为,mydb服务处理上一个请求还没结束时,又来了一个新的请求,并且新的请求改变的mydb中name的值,所以等到第一个请求从阻塞状态恢复时,获取的到值也变了。
上面的例子即使把skynet.sleep去掉,只要请求发送足够频繁,依然存在重入的问题。
7.11 服务临界区
skynet.queue 模块可以帮助你回避这些服务重入或者伪并发引起的复杂性。
local queue = require "skynet.queue"
local cs = queue() --获取一个执行队列
cs(f, ...) --将f丢到队列中执行
改进mydb.lua服务如下:
local skynet = require "skynet"
require "skynet.manager" -- import skynet.register
local queue = require "skynet.queue"
local cs = queue() --获取一个执行队列
local db = {}
local command = {}
function command.GET(key)
skynet.sleep(1000) --这里加了一个阻塞函数
return db[key]
end
function command.SET(key, value)
db[key] = value
end
skynet.start(function()
skynet.dispatch("lua", function(session, address, cmd, ...)
cmd = cmd:upper()
local f = command[cmd]
if f then
--将f丢到队列中去执行,队列中的函数严格按照先后顺序进行执行
skynet.retpack(cs(f, ...))
else
skynet.error(string.format("Unknown command %s", tostring(cmd)))
end
end)
skynet.register ".mydb"
end)
同样的执行步骤:
$ ./skynet examples/config mydb #先启动mydb [:01000012] LAUNCH snlua mydb testmydb name xm #再启动一个testmydb设置设置键值对name=xm,并获取name的值 [:01000019] LAUNCH snlua testmydb name xm [:01000019] mydb set Test 0 testmydb name xh #再启动一个testmydb设置设置键值对name=xh,并获取name值 [:01000020] LAUNCH snlua testmydb name xh [:01000020] mydb set Test 0 [:01000019] mydb get Test xm #获取到的值都是xm [:01000019] KILL self [:01000020] mydb get Test xh #获取到的值都是xh [:01000020] KILL self
上面的输出结果就是我们想要的了,把所有不希望重入的函数丢到cs队列中去执行,队列将依次执行每一个函数,前一个函数还没执行完的时候,后面的函数永远不会被执行。
执行队列虽然解决了重入的问题,但是明显降低了服务的并发处理能力,所以使用执行队列的时候尽量缩小临界区的颗粒度大小。
7.12 注册其他消息
如果想要使用其他的消息,那么需要显示注册一下:
othermsg.lua代码
local skynet = require "skynet"
require "skynet.manager" -- inject skynet.forward_type
skynet.register_protocol { --注册system消息
name = "system",
id = skynet.PTYPE_SYSTEM,
--pack = skynet.pack,
unpack = skynet.unpack, --unpack必须指定一下,接收到消息后会自动使用unpack解析
}
skynet.start(function()
skynet.dispatch("system", function(session, address, ...) --使用unpack解包
skynet.ret(skynet.pack("nengzhong"))
--使用skynet.retpack的时候,必须要在skynet.register_protocol指定pack
--skynet.retpack("nengzhong")
end)
skynet.register ".othermsg"
end)
testothermsg.lua代码
skynet = require "skynet"
skynet.register_protocol { --注册system消息
name = "system",
id = skynet.PTYPE_SYSTEM,
--pack = skynet.pack,
--unpack = skynet.unpack
}
skynet.start(function()
local othermsg = skynet.localname(".othermsg")
local r = skynet.unpack(skynet.rawcall(othermsg, "system", skynet.pack(1, "nengzhong", true)))
--使用skynet.call的时候必须要在skynet.register_protocol指定pack与unpack
--local r = skynet.call(othermsg, "system", 1, "nengzhong", true)
skynet.error("skynet.call return value:", r)
end)
需要注意,error、lua、response都是已经默认注册的消息类型,不要尝试修改他们的协议定义。
7.13 代理服务
在 skynet 中,有时候为一个服务实现一个前置的代理服务是很有必要的。所谓代理服务,就是向真正的功能服务发起请求时,请求消息发到另一个代理服务中,由这个代理服务转发这个请求给真正的功能服务。同样,回应消息也会被代理服务转发回去。
7.13.1 简单服务代理
示例代码:proxy.lua
local skynet = require "skynet"
require "skynet.manager"
local realsvr = ...
skynet.start( function() --注册消息处理函数
skynet.dispatch("lua", function (session, source, ...) --接收到消息msg,sz
skynet.ret(skynet.rawcall(realsvr, "lua", skynet.pack(...))) --根据参数列表重新打包消息转发
end)--释放消息msg,sz
skynet.register(".proxy")
end)
示例代码:testmydb.lua
local skynet = require "skynet"
local key,value = ...
function task()
local r = skynet.send(".proxy", "lua", "set", key, value) --修改为.proxy,发给代理
skynet.error("mydb set Test", r)
r = skynet.call(".proxy", "lua", "get", key) --修改为.proxy,发给代理
skynet.error("mydb get Test", r)
skynet.exit()
end
skynet.start(function()
skynet.fork(task)
end)
运行结果:
$ ./skynet examples/config mydb [:0100000a] LAUNCH snlua mydb proxy .mydb [:0100000b] LAUNCH snlua proxy .mydb testmydb name nengzhong [:0100000c] LAUNCH snlua testmydb name nengzhong [:0100000c] mydb set Test 0 [:0100000c] mydb get Test nengzhong [:0100000c] KILL self
这份代理重复的打包解包效率上有很大问题。
7.13.2 转换协议实现代理
上一节效率的问题是因为服务默认接收到lua消息后,会解包消息,但是lua消息已经注册了无法更改,那么我么可以使用skynet.forward_type进行协议转换。
使用skynet.forward_type也是启动服务的一种方法,跟skynet.start类似,只不过skynet.forward_type还需要提供一张消息转换映射表forward_map, 其他的方法与skynet.start一样。
local skynet = require "skynet"
require "skynet.manager" -- inject skynet.forward_type
skynet.register_protocol { --注册system消息
name = "system",
id = skynet.PTYPE_SYSTEM,
unpack = function (...) return ... end, --unpack直接返回不解包了
}
local forward_map = {
--发送到代理服务的lua消息全部转成system消息,不改变原先LUA的消息协议处理方式
[skynet.PTYPE_LUA] = skynet.PTYPE_SYSTEM,
--如果接收到应答消息,默认情况下会释放掉消息msg,sz,forward的方式处理消息不会释放掉消息msg,sz
[skynet.PTYPE_RESPONSE] = skynet.PTYPE_RESPONSE,
}
local realsvr = ...
skynet.forward_type( forward_map ,function() --注册消息处理函数
skynet.dispatch("system", function (session, source, msg, sz)
--直接转发给realsvr,接收到realsvr响应后也不释放内存,直接转发
skynet.ret(skynet.rawcall(realsvr, "lua", msg, sz))
end)--处理完不释放内存msg
skynet.register(".proxy")
end)
mydb.lua代码不变,运行结果:
$ ./skynet examples/config mydb #先运行mydb [:01000010] LAUNCH snlua mydb proxy .mydb #启动代理服务,并且告诉它真正的处理服务是.mydb [:01000012] LAUNCH snlua proxy .mydb testmydb name xx #启动testmydb服务 [:01000019] LAUNCH snlua testmydb name xx [:01000019] mydb set Test 0 #send方法成功 [:01000019] mydb get Test xx #get方法成功 [:01000019] KILL self
7.14 伪造消息
伪造其他服务地址来发送一个消息,可以使用到skynet.redirect
local skynet = require "skynet"
--使用source服务地址,发送typename类型的消息给dest服务,不需要接收响应,(source,dest只能是服务ID)
--msg sz一般使用skynet.pack打包生成
skynet.redirect(dest,source,typename, session, msg, sz)
例如:testredirect.lua
local skynet = require "skynet"
local source, dest = ...
skynet.start(function()
source = skynet.localname(source)
dest = skynet.localname(dest)
skynet.redirect(source, dest, "lua", 0, skynet.pack("nengzhong", 8.8, false))
end)
运行结果:
testluamsg #先启动testluamsg.lua [:0100000a] LAUNCH snlua testluamsg testsendmsg #再启动testsendmsg.lua [:0100000b] LAUNCH snlua testsendmsg [:0100000b] skynet.send return value: 0 [:0100000b] skynet.rawsend return value: 0 [:0100000a] session 0 [:0100000a] address :0100000b [:0100000a] arg1: 1 [:0100000a] arg2: nengzhong [:0100000a] arg3: true [:0100000a] session 0 [:0100000a] address :0100000b [:0100000a] arg1: 2 [:0100000a] arg2: nengzhong [:0100000a] arg3: false testredirect .testluamsg .testsendmsg #伪造testsendmsg给testluamsg发送一个消息 [:0100000c] LAUNCH snlua testredirect .testluamsg .testsendmsg [:0100000a] session 0 [:0100000a] address :0100000b [:0100000a] arg1: nengzhong [:0100000a] arg2: 8.8 [:0100000a] arg3: false
7.15 节点间消息通信
globalluamsg.lua
skynet = require "skynet"
require "skynet.manager"
skynet.start(function()
--注册"lua"类型消息的回调函数
skynet.dispatch("lua", function(session, address, msg)
skynet.retpack(msg:upper())
end)
skynet.register("globalluamsg")
end)
globalcall.lua
skynet = require "skynet"
harbor = require "skynet.harbor"
skynet.start(function()
local globalluamsg = harbor.queryname("globalluamsg")
local r = skynet.call(globalluamsg, "lua", "nengzhong")
skynet.error("skynet.call return value:", r)
end)
先在节点1运行globalluamsg,然后在节点2运行globalcall.
节点1运行情况:
globalluamsg [:0100000a] LAUNCH snlua globalluamsg
节点2运行情况:
globalcall [:02000008] LAUNCH snlua globalcall [:02000008] skynet.call return value: NENGZHONG