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萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章