skynet.lua對比以前優化了一些函數,尤其是對協程的控制,使得消息的處理流程更加清晰。我們現在來一步步剖析這個消息執行流程,加深對skynet reactor模式的理解以及協程的應用。
首先看服務的第一條消息是怎麼產生,又是如何被處理的。在創建一個snlua服務後第一條消息靠什麼來驅動呢?答案是靠自己(第一步還是得靠自己,然後別人纔有機會接觸你),看看下面的代碼可以清楚的看到:
int snlua_init(struct snlua *l, struct skynet_context *ctx, const char * args) {
int sz = strlen(args);
char * tmp = skynet_malloc(sz);
memcpy(tmp, args, sz);
skynet_callback(ctx, l , launch_cb);
const char * self = skynet_command(ctx, "REG", NULL);
uint32_t handle_id = strtoul(self+1, NULL, 16);
// it must be first message
skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY,0, tmp, sz);
return 0;
}
static int launch_cb(struct skynet_context * context, void *ud, int type, int session, uint32_t source , const void * msg, size_t sz) {
assert(type == 0 && session == 0);
struct snlua *l = ud;
skynet_callback(context, NULL, NULL);
int err = init_cb(l, context, msg, sz);
if (err) {
skynet_command(context, "EXIT", NULL);
}
return 0;
}
從上面看到執行第一條的回調函數launch_cb會調用init_cb,就是在這個函數裏開始加載解析lua源文件並執行。
一般lua源文件的主函數爲skynet.start(),他的內部實現爲:
function skynet.start(start_func)
c.callback(skynet.dispatch_message)
init_thread = skynet.timeout(0, function()
skynet.init_service(start_func)
init_thread = nil
end)
end
function skynet.timeout(ti, func)
local session = c.intcommand("TIMEOUT",ti)
assert(session)
local co = co_create(func)
assert(session_id_coroutine[session] == nil)
session_id_coroutine[session] = co
return co -- for debug
end
可以看到實際上這個函數也是由一條消息驅動的,原因這篇文章skynet答疑一 --skynet.start參數爲什麼要在定時器中執行已經講過,這裏就不再多說。而且這個函數裏重新設置了回調函數,回調函數第一次被調用是由於timeout超時消息。我們看看這條消息的類型:
int skynet_timeout(uint32_t handle, int time, int session) {
if (time <= 0) {
struct skynet_message message;
message.source = 0;
message.session = session;
message.data = NULL;
message.sz = (size_t)PTYPE_RESPONSE << MESSAGE_TYPE_SHIFT;
if (skynet_context_push(handle, &message)) {
return -1;
}
} else {
struct timer_event event;
event.handle = handle;
event.session = session;
timer_add(TI, &event, sizeof(event), time);
}
return session;
}
可以看到是PTYPE_RESPONSE(1)類型的。這個消息被調用時又該如何執行呢?我們還是看看相關代碼吧:
local function raw_dispatch_message(prototype, msg, sz, session, source)
-- skynet.PTYPE_RESPONSE = 1, read skynet.h
if prototype == 1 then
local co = session_id_coroutine[session]
if co == "BREAK" then
session_id_coroutine[session] = nil
elseif co == nil then
unknown_response(session, source, msg, sz)
else
local tag = session_coroutine_tracetag[co]
if tag then c.trace(tag, "resume") end
session_id_coroutine[session] = nil
suspend(co, coroutine_resume(co, true, msg, sz))
end
else
......
end
end
首先他會查找相關聯的協程,這個協程在skynet.timeout中創建,並且和session id相關聯。創建協程又是一個很難啃的函數:
local function co_create(f)
local co = table.remove(coroutine_pool)
if co == nil then
co = coroutine_create(function(...)
f(...)
while true do
local session = session_coroutine_id[co]
if session and session ~= 0 then
local source = debug.getinfo(f,"S")
skynet.error(string.format("Maybe forgot response session %s from %s : %s:%d",
session,
skynet.address(session_coroutine_address[co]),
source.source, source.linedefined))
end
-- coroutine exit
local tag = session_coroutine_tracetag[co]
if tag ~= nil then
if tag then c.trace(tag, "end") end
session_coroutine_tracetag[co] = nil
end
local address = session_coroutine_address[co]
if address then
session_coroutine_id[co] = nil
session_coroutine_address[co] = nil
end
-- recycle co into pool
f = nil
coroutine_pool[#coroutine_pool+1] = co
-- recv new main function f
f = coroutine_yield "SUSPEND"
f(coroutine_yield())
end
end)
else
-- pass the main function f to coroutine, and restore running thread
local running = running_thread
coroutine_resume(co, f)
running_thread = running
end
return co
end
他利用了協程池的思想。我們再看看raw_dispatch_message函數,找到了session id對應的協程,就開始coroutine_resume執行協程了。協程函數就是skynet.start()裏面的參數函數。
如果這個函數裏沒有阻塞的調用,就像下面一個簡單的函數:
skynet.start(
function()
print('test')
end
)
那麼這個協程函數很快就執行完了。請看co_create的代碼和下面的圖:
接下來分析while循環,首先是查找協程對應的session id。目前我們並沒有把協程對應的session id記錄下來,只把session id對應的協程記錄了下來,所以這個爲空。接着是用於調試的一些分析,暫且跳過。
到了關鍵的地方,保存協程到協程池,然後該協程調用coroutine_yield "SUSPEND"掛起。回到主函數驅動協程的地方,將會調用suspend:
function suspend(co, result, command)
if not result then
local session = session_coroutine_id[co]
if session then -- coroutine may fork by others (session is nil)
local addr = session_coroutine_address[co]
if session ~= 0 then
-- only call response error
local tag = session_coroutine_tracetag[co]
if tag then c.trace(tag, "error") end
c.send(addr, skynet.PTYPE_ERROR, session, "")
end
session_coroutine_id[co] = nil
session_coroutine_address[co] = nil
session_coroutine_tracetag[co] = nil
end
skynet.fork(function() end) -- trigger command "SUSPEND"
error(debug.traceback(co,tostring(command)))
end
if command == "SUSPEND" then
dispatch_wakeup()
dispatch_error_queue()
elseif command == "QUIT" then
-- service exit
return
elseif command == "USER" then
-- See skynet.coutine for detail
error("Call skynet.coroutine.yield out of skynet.coroutine.resume\n" .. debug.traceback(co))
elseif command == nil then
-- debug trace
return
else
error("Unknown command : " .. command .. "\n" .. debug.traceback(co))
end
end
如果協程函數執行沒有錯誤,那麼第二個參數result爲true,第三個參數是協程掛起時傳入的參數,這裏爲"SUSPEND"。將會調用dispatch_wakeup和dispatch_error_queue,我們將有機會說到這兩個函數。
suspend調用結束,那麼消息回調函數執行完畢,一條消息的生命週期走完。
我們分析了一條很簡單,幾乎沒有什麼卵用,但是很完整的消息回調執行步驟。接下來試着分析稍微複雜點,會被阻塞,例如有call調用,假設一個服務A調用服務B:
skynet.start(
function()
print('in A service ')
skynet.call('B', 'lua', 'cmd', ...)
end
)
當調用call的時候會協程會阻塞:
function skynet.call(addr, typename, ...)
local p = proto[typename]
local session = c.send(addr, p.id , nil , p.pack(...))
if session == nil then
error("call to invalid address " .. skynet.address(addr))
end
return p.unpack(yield_call(addr, session))
end
我們看到通過底層的c調用,call首先會把消息發送給目的地,並且產生一個關聯的session id,然後調用yield_call將協程掛起:
local function yield_call(service, session)
watching_session[session] = service
session_id_coroutine[session] = running_thread
local succ, msg, sz = coroutine_yield "SUSPEND"
watching_session[session] = nil
if not succ then
error "call failed"
end
return msg,sz
end
在這個函數裏,將把session id對應的協程記錄下來,然後掛起協程。接下來的代碼分析和上面的一樣,此時A服務由timeout產生的消息將執行完畢了,但是這個函數的代碼卻還沒有執行完,第一次分析的時候還覺得有點不可思議。那麼什麼時候再來執行後面的代碼呢,我們慢慢道來。
假設B服務的主函數代碼也非常簡單,就和第一個例子一樣。第一條由timeout產生的消息執行完了,接下來收到了A服務發過來的消息,由於A是用lua協議發的,type是PTYPE_LUA(10),所以將執行raw_dispatch_message後面的部分:
local function raw_dispatch_message(prototype, msg, sz, session, source)
-- skynet.PTYPE_RESPONSE = 1, read skynet.h
if prototype == 1 then
......
else
local p = proto[prototype]
if p == nil then
......
end
local f = p.dispatch
if f then
local co = co_create(f)
session_coroutine_id[co] = session
session_coroutine_address[co] = source
......
suspend(co, coroutine_resume(co, session,source, p.unpack(msg,sz)))
else
trace_source[source] = nil
if session ~= 0 then
c.send(source, skynet.PTYPE_ERROR, session, "")
else
unknown_request(session, source, msg, sz, proto[prototype].name)
end
end
end
end
我們可以看到B服務要有相應協議的dispatch函數纔有正確的執行,他的動作首先是調用co_create創建一個協程,注意此時將走co_create的後半部分。要明白其中的邏輯,剛開始有點繞,和我初學的時候一樣,下面的圖幫你理解:
先是將原先掛起的協程恢復,此時coroutine_yield返回,返回值爲新的協程函數,然後再次掛起,最後返回一個協程。這段代碼着實繞,短短的數幾十行代碼,協程掛起恢復了好幾次。此時協程仍然是掛起的,raw_dispatch_message創建協程(更準確的說是獲取協程池中的一個)之後,先將協程對應的session記錄下來,同時也將協程對應的源地址也記錄下來。然後將再次調用coroutine_resume恢復協程,再次對比上面的圖,f(coroutine_yield()),此時將執行p.dispatch(session,source, p.unpack(msg,sz))函數,也就是B服務註冊的相關協議的函數了。
當協議處理函數執行完畢後,注意此時協程函數仍然在執行,因爲co_create有個while循環。此時將判斷該協程對應的session id還在不在,什麼意思呢?原來A服務是call調用,上面提到B服務收到消息後執行回調函數時記錄了協程對應的session值(注意是send調用是沒有session值的)。那麼這個記錄什麼時候去掉呢?不要忘記了,我們必須對A服務進行回覆,A服務才能恢復。所以某個服務被call時一定要調用skynet.ret或skynet.response給對方發送一條消息,這樣才能清除session記錄,避免了一條警告,同時對方服務恢復執行。
接下來co_create就會掛起了,還是主線程supend接管。這樣A發送給B的一條消息回調就執行完畢了。下次B服務再有消息將會循環上面過程,周而復始。我們看到這種情況下多條消息只會用到一個協程池的一個協程而已。
再來說A服務。A服務掛起後,等待B服務調用skynet.ret回覆,在回覆中會帶上A給B消息的session,同時消息類型爲PTYPE_RESPONSE,這樣一來A服務執行其回調函數,和最上面的一樣,通過session找到其協程,然後恢復。
這篇文章簡單介紹了幾個基本消息的執行流程情景分析,下次再講講fork和sleep消息的執行流程。
歡迎加入QQ羣 858791125 討論skynet,遊戲後臺開發,lua腳本語言等問題。