skynet.newservice簡介:服務的啟動

看热闹的咸鱼發表於2024-05-22

skynet是一個輕量級的遊戲伺服器框架。

簡介

skynet的體系中,服務是一個基礎概念。通常,我們使用skynet.newservice來啟動一個snlua服務。
那麼,當我們寫下local addr = skynet.newservice("test")這行程式碼時,系統是怎麼運作的呢?
思考一下這些問題:

  • 呼叫skynet.newservice會不會發生阻塞?
  • 如果test服務在skynet.start時呼叫了skynet.exitaddr會是什麼值?
  • 如果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_service

新服務啟動時,呼叫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服務,而.launcherREMOVE的處理如下:

--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

對於剛啟動的服務來說,這裡會呼叫到對應的responseresponse需要兩個引數,這裡第一個引數是true,第二個引數為nil,而第二個引數是返回地址,也就是說,呼叫者收到的addrnil值。
skynet_service_exit

新服務啟動報錯的話,又返回什麼呢

新服務啟動的時候,無論是用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引數是falseresponseskynet.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

可以看到,當傳入的okfalse的時候,會傳送一個PTYPE_ERROR型別的訊息給呼叫者。
而當我們require"skynet"時,對PTYPE_ERROR預設的處理函式是_error_dispatch,具體的流程可以看看原始碼,這裡簡而言之,就是呼叫call的那條協程會觸發一個call failerror

所以,當新服務的啟動函式出錯時,在新服務中會報錯,中斷,而呼叫者在skynet.call()中也會報call fail的錯,從而中斷執行,也就不會有addr的返回了。
skynet_service_error

如果啟動服務要傳比較複雜的引數,要怎麼做比較好

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時呼叫了exitaddr會是什麼值?

    • nil
  • 如果test服務在start時出現錯誤,addr又會是什麼值?

    • skynet.newservice會報錯,沒有返回值
  • test服務是不是一定要呼叫skynet.start

    • 不一定,也可以呼叫skynet.forward_typeskynet.filter
  • 如果要傳一些複雜的引數,又要怎麼做?

    • 將服務的建立和啟動分開,建立後傳送lua訊息初始化服務。

最後再思考一個問題:啟動系統的時候,第一個服務又是什麼時候啟動的呢?答案可以看看這裡:skynet 之 main 服務的啟動

相關文章