skynet是一個輕量級的遊戲伺服器框架。
簡介
在skynet
的體系中,服務是一個基礎概念。通常,我們使用skynet.newservice
來啟動一個snlua
服務。
那麼,當我們寫下local addr = skynet.newservice("test")
這行程式碼時,系統是怎麼運作的呢?
思考一下這些問題:
- 呼叫
skynet.newservice
會不會發生阻塞? - 如果
test
服務在skynet.start
時呼叫了skynet.exit
,addr
會是什麼值? - 如果
test
服務在skynet.start
時出現錯誤,addr
又會是什麼值? test
服務是不是一定要呼叫skynet.start
?- 如果要傳一些複雜的引數,又要怎麼做?
skynet.newservice的程式碼實現
--skynet/lualib/skynet.lua
function skynet.newservice(name, ...)
return skynet.call(".launcher", "lua" , "LAUNCH", "snlua", name, ...)
end
skynet.newservice
的程式碼很簡單,只呼叫了一個skynet.call
,而skynet.call
是阻塞的,所以skynet.newservice
也是阻塞的。
這個call
發了一條lua
訊息給.launcher
服務,接下來看看.launcher
服務相關的程式碼:
--skynet/service/launcher.lua
local function launch_service(service, ...)
local param = table.concat({...}, " ")
local inst = skynet.launch(service, param)
local session = skynet.context()
local response = skynet.response()
if inst then
services[inst] = service .. " " .. param
instance[inst] = response
launch_session[inst] = session
else
response(false)
return
end
return inst
end
function command.LAUNCH(_, service, ...)
launch_service(service, ...)
return NORET
end
這裡又呼叫到skynet.launch
,實際上是發了個LAUNCH
指令到底層,這裡建立了一個snlua
服務:
--skynet/lualib/skynet/manager.lua
function skynet.launch(...)
local addr = c.command("LAUNCH", table.concat({...}," "))
if addr then
return tonumber(string.sub(addr , 2), 16)
end
end
要注意前面的.launcher
服務的command.LAUNCH
函式是忽略返回的,所以此時skynet.newservice
還處於阻塞狀態,等待.launcher
的返回。
那什麼時候會返回響應呢?
回到前面的launch_service
函式,可以看到skynet.launch
成功後並沒有直接返回,而是生成一個響應函式response
,儲存在表instance
中。
搜尋這個instance
,我們可以在command.LAUNCHOK
中找到它的使用:
--skynet/service/launcher.lua
function command.LAUNCHOK(address)
-- init notice
local response = instance[address]
if response then
response(true, address)
instance[address] = nil
launch_session[address] = nil
end
return NORET
end
也就是說,要等到.launcher
服務收到LAUNCHOK
的指令之後,才會返回給newservice
的呼叫者。
問題又來了,什麼時候傳送LAUNCHOK
呢?答案是在skynet.init_service
中。
而呼叫skynet.init_service
的,一共有三個函式:
- skynet.start
- skynet.forward_type
- skynet.filter
所以,在服務的啟動指令碼中,我們必須呼叫這三個函式中的其中一個(通常都是skynet.start
),否則的話,呼叫方永遠都收不到返回的資料。
以在main
服務中,建立新服務test
為例,流程如下圖所示:
新服務啟動時,呼叫skynet.exit,呼叫者收到的addr是什麼?
我們看一下skynet.exit
:
--skynet/lualib/skynet.lua
function skynet.exit()
fork_queue = { h = 1, t = 0 } -- no fork coroutine can be execute after skynet.exit
skynet.send(".launcher","lua","REMOVE",skynet.self(), false)
--其他程式碼...
--...
end
這裡看到,新服務傳送了REMOVE
指令到.launcher
服務,而.launcher
對REMOVE
的處理如下:
--skynet/service/launcher.lua
function command.REMOVE(_, handle, kill)
services[handle] = nil
local response = instance[handle]
if response then
-- instance is dead
response(not kill) -- return nil to caller of newservice, when kill == false
instance[handle] = nil
launch_session[handle] = nil
end
-- don't return (skynet.ret) because the handle may exit
return NORET
end
對於剛啟動的服務來說,這裡會呼叫到對應的response
,response
需要兩個引數,這裡第一個引數是true
,第二個引數為nil
,而第二個引數是返回地址,也就是說,呼叫者收到的addr
是nil
值。
新服務啟動報錯的話,又返回什麼呢
新服務啟動的時候,無論是用skynet.start
還是skynet.forward_type
,最終都是呼叫skynet.init_service
,來看看程式碼:
--skynet/lualib/skynet.lua
function skynet.init_service(start)
local function main()
skynet_require.init_all()
start()
end
local ok, err = xpcall(main, traceback)
if not ok then
skynet.error("init service failed: " .. tostring(err))
skynet.send(".launcher","lua", "ERROR")
skynet.exit()
else
skynet.send(".launcher","lua", "LAUNCHOK")
end
end
可以看到,對start
函式的呼叫,是透過xpcall
來呼叫的,如果報錯的話,會傳送ERROR
到.launcher
服務。
--skynet/service/launcher.lua
function command.ERROR(address)
-- see serivce-src/service_lua.c
-- init failed
local response = instance[address]
if response then
response(false)
launch_session[address] = nil
instance[address] = nil
end
services[address] = nil
return NORET
end
這裡response
引數是false
,response
是skynet.response
生成的一個函式,相關程式碼如下:
--skynet/lualib/skynet.lua
function skynet.response(pack)
--其他程式碼...
--...
local function response(ok, ...)
--其他程式碼...
--...
if ok then
ret = c.send(co_address, skynet.PTYPE_RESPONSE, co_session, pack(...))
if ret == false then
-- If the package is too large, returns false. so we should report error back
c.send(co_address, skynet.PTYPE_ERROR, co_session, "")
end
else
ret = c.send(co_address, skynet.PTYPE_ERROR, co_session, "")
end
--其他程式碼...
--...
end
--其他程式碼...
--...
return response
end
可以看到,當傳入的ok
是false
的時候,會傳送一個PTYPE_ERROR
型別的訊息給呼叫者。
而當我們require"skynet"
時,對PTYPE_ERROR
預設的處理函式是_error_dispatch
,具體的流程可以看看原始碼,這裡簡而言之,就是呼叫call
的那條協程會觸發一個call fail
的error
。
所以,當新服務的啟動函式出錯時,在新服務中會報錯,中斷,而呼叫者在skynet.call()
中也會報call fail
的錯,從而中斷執行,也就不會有addr
的返回了。
如果啟動服務要傳比較複雜的引數,要怎麼做比較好
skynet.newservice(service_name, ...)
後面是可以帶多個引數的,但這些引數只能是數字或字串,回看前面的skynet.launch
的程式碼,裡面是呼叫了c.command("LAUNCH", table.concat({...}," "))
,這裡可以看到,傳遞的引數透過table.concat
打包成字串,以空格隔開。如果我們的引數中帶有空格,或者我們想要傳個table
,那就不支援了。
通常來說,我們可以先啟動服務,在skynet.start
中做些簡單的功能,呼叫skynet.dispatch("lua", ...)
來處理lua
訊息,透過lua
訊息來做初始化,這樣就能傳送複雜的引數了:
local addr = skynet.newservice("test")
skynet.send(addr, "lua", "start", {address='0.0.0.0',port=8888,nodelay=true})
總結
現在,我們可以回答最初的問題了:
-
呼叫
skynet.newservice
會不會發生阻塞?- 會阻塞,如果服務沒啟動完,會一直等待下去。
-
如果
test
服務在start
時呼叫了exit
,addr
會是什麼值?- nil
-
如果
test
服務在start
時出現錯誤,addr
又會是什麼值?- skynet.newservice會報錯,沒有返回值
-
test
服務是不是一定要呼叫skynet.start
?- 不一定,也可以呼叫
skynet.forward_type
或skynet.filter
- 不一定,也可以呼叫
-
如果要傳一些複雜的引數,又要怎麼做?
- 將服務的建立和啟動分開,建立後傳送
lua
訊息初始化服務。
- 將服務的建立和啟動分開,建立後傳送
最後再思考一個問題:啟動系統的時候,第一個服務又是什麼時候啟動的呢?答案可以看看這裡:skynet 之 main 服務的啟動