今天我們來讀service_lua.c文件。
這個文件很重要,它是模塊snlua的源文件,也是各個lua服務節點的製造者。比如:agent服務節點,watchdog服務節點,launch服務節點等。
讓我們來看看這個製造者是如何運作的。
拿agent舉例說:
gate節點在服務端與新到的客戶端連接建立成功之後,會向watchdog服務節點發送一條文本消息,消息內容含有open指令,意在生成兩個新的服務節點,分別是client和agent服務節點。
watchdog收到消息後會運行command:open接口:
- function command:open(parm)
- local fd,addr = string.match(parm,"(%d+) ([^%s]+)")
- fd = tonumber(fd)
- print("agent open", self, string.format("%d %d %s", self, fd, addr))
- local client = skynet.launch("client", fd)
- local agent = skynet.launch("snlua","agent",skynet.address(client))
- if agent then
- agent_all[self] = { agent , client }
- skynet.send(gate, "text", "forward" , self, skynet.address(agent) , skynet.address(client))
- end
- end
執行該接口時,watchdog會執行兩條指令skynet.launch(client, ...)和skynet.launch(snalu, agent, ....)分別載入client節點,和agent節點。agent節點是由模塊snlua製造的。讓我們看看接下來會發生什麼。
watchdog通過腳本調用執行skynet.so庫文件中的skynet_command接口載入節點agent。而後,初始化節點agent的上下文,並調用模塊snlua的snlua_create接口,生成一個節點agent基礎的基礎數據,調用snlua_init初始化agent節點的數據併發送了一條消息(執行腳本agent.lua,當然現在還未執行)。此時,最基礎的一個agent服務節點就已經產生了,當然,它收到的第一條消息就是讀取agent.lua文件,並執行。
新的worker線程,執行到agent節點時,首先,執行agent.lua腳本文件。裏面包括,引入skynet.lua文件,設置client的handle值。skynet.lua比較複雜。skynet.lua中會引入skynet.so
庫文件並調用luaopen_skynet_c接口,向當前lua狀態機上的添加一張表,而後將表的地址賦給本地變量c。本地變量c表中記錄了很多函數指針。當然經常導C++對象或者函數指針到lua中的人對這一塊肯定不陌生。不過,我要說的是接下來要發生的邏輯。
- local skynet = require "skynet"
- local client = ...
- skynet.register_protocol {
- name = "client",
- id = 3,
- pack = function(...) return ... end,
- unpack = skynet.tostring,
- dispatch = function (session, address, text)
- -- It's client, there is no session
- skynet.send("LOG", "text", "client message :" .. text)
- local result = skynet.call("SIMPLEDB", "text", text)
- skynet.ret(result)
- end
- }
- skynet.start(function()
- skynet.send(client,"text","Welcome to skynet")
- end)
首先通過skynet.register_protocol註冊一個客戶端消息處理函數,裏面包含發送內容到log服務,調用simpledb執行指令,最後返回結果到客戶端。
接下來是skynet.start接口,這個接口很重要。我們看看裏面寫了什麼。
- function skynet.start(start_func)
- c.callback(dispatch_message)
- trace_handle = assert(c.stat "trace")
- skynet.timeout(0, function()
- init_service(start_func)
- end)
- end
首先,執行c.callback,調用skynet.so庫文件的接口_callback,設置agent節點的回調函數,這樣每次有消息發送到agent節點,都會用函數dispatch_message來處理。而後,纔會執行傳進來的閉包start_func。
接下來dispatch_message通過pcall調用了接口raw_dispatch_message,而dispatch_message本事並沒有太多邏輯。我們看看raw_dispatch_message接口是在幹什麼。
- local function raw_dispatch_message(prototype, msg, sz, session, source, ...)
- -- 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
- c.trace_switch(trace_handle, session)
- session_id_coroutine[session] = nil
- suspend(co, coroutine.resume(co, msg, sz))
- end
- else
- local p = assert(proto[prototype], prototype)
- 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
- print("Unknown request :" , p.unpack(msg,sz))
- error(string.format("Can't dispatch type %s : ", p.name))
- end
- end
- end
若消息類型值爲1,則是返回消息,比方說向simpledb發送了一條文本消息,而後收到其返回結果。當然,不管raw_dispatch_message中走哪條分支,最終基本上都會調用suspend接口,並調用coroutine.resume接口,也就是用協程的方式運行某個lua閉包。解釋一下lua協程。按我的理解lua協程其實只開闢了一個線程棧,但沒有像操作系統的線程那樣,成爲一個調度單位。只是在lua主線程執行到某一處時,通過coroutine.resume切換到協程中執行,當執行完之後,通過coroutine.yield返回到主線程棧上繼續執行。也就是說協程只是新建了一個空間來執行邏輯,執行完將執行結果返回給主線程,或者執行到一半,切換到主線程,等待下次被喚起繼續執行。而之前註冊的消息處理函數,就是在協程中執行的。如果已經理解了協程的概念,繼續看其中的函數就不會太麻煩。比方說看skynet.call接口時,看到接口skynet.call時:
- return p.unpack(coroutine_yield("CALL", assert(session, "call to invalid address")))
- -- suspend is local function
- function suspend(co, result, command, param, size)
- if not result then
- trace_count()
- error(debug.traceback(co,command))
- end
- if command == "CALL" then
- c.trace_register(trace_handle, param)
- session_id_coroutine[param] = co
- elseif command == "SLEEP" then
- c.trace_register(trace_handle, param)
- session_id_coroutine[param] = co
- sleep_session[co] = param
- elseif command == "RETURN" then
- local co_session = session_coroutine_id[co]
- local co_address = session_coroutine_address[co]
- -- PTYPE_RESPONSE = 1 , see skynet.h
- if param == nil then
- trace_count()
- error(debug.traceback(co))
- end
- -- c.send maybe throw a error, so call trace_count first.
- -- The coroutine execute time after skynet.ret() will not be trace.
- trace_count()
- c.send(co_address, 1, co_session, param, size)
- return suspend(co, coroutine.resume(co))
- elseif command == "EXIT" then
- -- coroutine exit
- session_coroutine_id[co] = nil
- session_coroutine_address[co] = nil
- else
- trace_count()
- error("Unknown command : " .. command .. "\n" .. debug.traceback(co))
- end
- trace_count()
- dispatch_wakeup()
- end
第一個co就不解釋了,result是執行協程的結果,若成功返回true,否則返回false。command就是“CALL”,後面的是返回的參數。比方說現在param 就等於 session。此時,執行command == “call” 將 key = session,value = co保存到表session_id_coroutine表中。例如:當simpledb節點返回數據給agent節點時,首先,在simpledb中執行dispatch接口,執行skynet.ret接口,此時,協程會被掛起,且切換到simpledb主線程,而後返回一條“RETURN”command給suspend接口,suspend接口中從session_coroutine_id中取出session_id,從session_coroutine_address取出handle值。最後發送消息給該handle,也就是agent節點。如果要問爲什麼消息類型是返回類型,請看上段代碼的:
- -- c.send maybe throw a error, so call trace_count first.
- -- The coroutine execute time after skynet.ret() will not be trace.
- trace_count()
- c.send(co_address, 1, co_session, param, size)