skynet框架应用 (七) 本地服务间消息通信

7 服务间消息通信

​ skynet中的每一个服务都有一个独立的lua虚拟机,逻辑上服务之间是相互隔离的,那么你就不能使用传统意义上的LUA全局变量来进行服务间通信了。

​ 在skynet中服务之间可以通过skynet消息调度机制来完成通信。skynet中的服务是基于actor模型设计出来的,每个服务都可以接收消息,处理消息,发送应答消息。

​ 每条 skynet 消息由 6 部分构成:消息类型、session 、发起服务地址 、接收服务地址 、消息 C 指针、消息长度。

7.1消息类型

​ 在 skynet 中消息分为多种类别,对应的也有不同的编码方式(即协议),消息类型的宏定义可以查看 skynet.h 中:


#define PTYPE_TEXT 0   
#define PTYPE_RESPONSE 1    //表示一个回应包
#define PTYPE_MULTICAST 2   //广播消息
#define PTYPE_CLIENT 3      //用来处理网络客户端的请求消息
#define PTYPE_SYSTEM 4      //系统消息
#define PTYPE_HARBOR 5      //跨节点消息
#define PTYPE_SOCKET 6    //套接字消息
#define PTYPE_ERROR 7     //错误消息,一般服务退出的时候会发送error消息给关联的服务
#define PTYPE_QUEUE 8
#define PTYPE_DEBUG 9
#define PTYPE_LUA 10   //lua类型的消息,最常用
#define PTYPE_SNAX 11  //snax服务消息

#define PTYPE_TAG_DONTCOPY 0x10000
#define PTYPE_TAG_ALLOCSESSION 0x20000

​ 上面的消息类型有多种,但是最常用的是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

发布了101 篇原创文章 · 获赞 117 · 访问量 21万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章