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