Skynet服務器框架(四) Lua服務創建和啟動剖析


前言:

之前從Skynet啟動過程,解讀了skynet的啟動部分C語言編寫的底層源碼 Skynet服務器框架(二)C源碼剖析啟動流程,最后成功啟動了引導的lua服務bootstrap.lua,接下來我們要嘗試自定義一個lua服務,並讓它啟動起來。

bootstrap實現功能:

bootstrap.lua源碼:

local skynet = require "skynet" local harbor = require "skynet.harbor" require "skynet.manager" -- import skynet.launch, ... local memory = require "memory" skynet.start(function() local sharestring = tonumber(skynet.getenv "sharestring" or 4096) memory.ssexpand(sharestring) local standalone = skynet.getenv "standalone" local launcher = assert(skynet.launch("snlua","launcher")) skynet.name(".launcher", launcher) local harbor_id = tonumber(skynet.getenv "harbor" or 0) if harbor_id == 0 then assert(standalone == nil) standalone = true skynet.setenv("standalone", "true") local ok, slave = pcall(skynet.newservice, "cdummy") if not ok then skynet.abort() end skynet.name(".cslave", slave) else if standalone then if not pcall(skynet.newservice,"cmaster") then skynet.abort() end end local ok, slave = pcall(skynet.newservice, "cslave") if not ok then skynet.abort() end skynet.name(".cslave", slave) end if standalone then local datacenter = skynet.newservice "datacenterd" skynet.name("DATACENTER", datacenter) end skynet.newservice "service_mgr" --根據Conifg中配置的start項啟動下一個lua服務,假如無此項配置,則啟動main.lua pcall(skynet.newservice,skynet.getenv "start" or "main") --退出bootstrap服務 skynet.exit() end)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

源碼解析:

內容解析,在官方的文檔Bootstrap已經做了詳細的闡述:

這段腳本通常會:

  • 根據 standalone 配置項判斷你啟動的是一個 master 節點還是 slave 節點。如果是 master 節點還會進一步的通過 harbor 是否配置為 0 來判斷你是否啟動的是一個單節點 skynet 網絡。 
    • 單節點模式下: 
      是不需要通過內置的 harbor 機制做節點間通訊的。但為了兼容(因為你還是有可能注冊全局名字),需要啟動一個叫做 cdummy 的服務,它負責攔截對外廣播的全局名字變更
    • 多節點模式: 
      對於 master 節點,需要啟動 cmaster 服務作節點調度用。此外,每個節點(包括 master 節點自己)都需要啟動 cslave 服務,用於節點間的消息轉發,以及同步全局名字。
  • 接下來在 master 節點上,還需要啟動 DataCenter 服務
  • 然后,啟動用於 UniqueService 管理的 service_mgr 。
  • 最后,它從 config 中讀取 start 這個配置項,作為用戶定義的服務啟動入口腳本運行。成功后,把自己退出

這個 start 配置項,才是用戶定義的啟動腳本,默認值為 "main",對應main.lua腳本。

創建自定義lua服務:

在編寫自定義的lua服務時,可以查詢skynet lua開發的官方API

首先在test目錄下(也可以選擇其他目錄,只要是config配置文件中 luaservice 項包含的目錄即可)創建一個我們自己的lua腳本,這里我取名為firsttest.lua,表示自定義第一個服務的測試,簡單功能是直接在回調函數中輸出一段日志,然后按照以下步驟來實現代碼:

  • 引入或者說是創建一個skynet服務:

    local skynet = require "skynet"
    • 1
  • 調用skynet.start接口,並定義傳入回調函數:

    skynet.start(function() skynet.error("Server First Test") end)
    • 1
    • 2
    • 3

啟動lua服務:

1.原理解析:

參考 bootstrap.lua 中的源碼,我們得到在lua中啟動一個服務的接口:

skynet.newservice(name, ...)
  • 1

它的定義在 lualib/skynet.lua 中,傳入參數 name 是用來創建lua服務的 lua腳本名稱,這些lua腳本存放的目錄取決於config 配置文件中 luaservice 項的配置信息,例如這里我們配置信息為:

luaservice = root.."service.lua;"..root.."test.lua;"..root.."examples.lua"
  • 1

表示skynet通過 skynet.newservice 查詢與 name 參數匹配的lua腳本會查找 service 、test 和 example 這三個目錄下的 .lua 腳本。

2.實現步驟:

從bootstrap服務的解析,我們知道最后被啟動的用戶第一個lua服務是main.lua我們要讓上面的自定的firsttest.lua運行啟動,有兩種辦法:

  • 方法一:直接修改config配置文件中的start項為"firsttest"

    start = "firsttest"
    • 1
  • 方法二:在main.lua中通過skynet.newservice啟動:

    skynet.newservice("firsttest")
    • 1

用上述的任何一個步驟完成配置之后,回到skynet根目錄,運行skynet服務器啟動指令,如下:

linsh@ubuntu:/application/skynet$ ./skynet examples/config [:01000001] LAUNCH logger [:01000002] LAUNCH snlua bootstrap [:01000003] LAUNCH snlua launcher [:01000004] LAUNCH snlua cmaster [:01000004] master listen socket 0.0.0.0:2013 [:01000005] LAUNCH snlua cslave [:01000005] slave connect to master 127.0.0.1:2013 [:01000004] connect from 127.0.0.1:52686 4 [:01000006] LAUNCH harbor 1 16777221 [:01000004] Harbor 1 (fd=4) report 127.0.0.1:2526 [:01000005] Waiting for 0 harbors [:01000005] Shakehand ready [:01000007] LAUNCH snlua datacenterd [:01000008] LAUNCH snlua service_mgr [:01000009] LAUNCH snlua main [:01000009] Server start [:0100000a] LAUNCH snlua protoloader [:0100000b] LAUNCH snlua console [:0100000c] LAUNCH snlua firsttest [:0100000c] Server First Test! [:0100000d] LAUNCH snlua debug_console 8000 [:0100000d] Start debug console at 127.0.0.1:8000 [:0100000e] LAUNCH snlua simpledb [:0100000f] LAUNCH snlua watchdog [:01000010] LAUNCH snlua gate [:01000010] Listen on 0.0.0.0:8888 [:01000009] Watchdog listen on 8888 [:01000009] KILL self [:01000002] KILL self 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31


服務分類:

1.全局唯一服務:

全局唯一的服務等同於單例,即不管調用多少次創建接口,最后都只會創建一個此類型的服務實例且全局唯一。

  • 創建接口:

    skynet.uniqueservice(global,...) 
    • 1

    當參數 global=true 時,則表示此服務在所有節點之間是唯一的。

  • 查詢接口: 
    假如不清楚當前創建了此全局服務沒有,可以通過以下接口來查詢:

    skynet.queryservice(global,...) 
    • 1

    global=true 時如果還沒有創建過目標服務則一直等下去,直到目標服務被(其他服務觸發而)創建。

2.普通服務:

每調用一次創建接口就會創建出一個對應的服務實例,可以同時創建成千上萬個,用唯一的id來區分每個服務實例。使用的創建接口是:

skynet.newservice(name, ...)
  • 1

3.創建區別:

關於全局服務的創建,其實也跟普通服務一樣,是用 skynet.call (異步變同步)的方式讓 service_mgr 服務創建目標服務(類似於通知 launch 服創建服務一樣)。區別在於創建全局服務時,假如 service_mgr 這邊如已創建則直接返回服務地址;如沒則創建;如正在創建則等結果。


skynet.newservice 源碼剖析:

上面的操作我們已經知道如何調用此接口來創建和啟動lua服務了,但是關於啟動的具體實現過程需要深入剖析一下:

首先,在 lualib/skynet.lua 中找到了 skynet.newservice 接口定義的位置:

function skynet.newservice(name, ...) return skynet.call(".launcher", "lua" , "LAUNCH", "snlua", name, ...) end
  • 1
  • 2
  • 3

通過調用 skynet.call 接口,向 laucher 服務(源碼是laucher.lua)發送一個 LAUNCH 消息,從而調用 local function launch_service(service, ...) 接口來實現的。也就是說,所有創建skynet服務(除了laucher 服務自身)的操作都由 laucher 服務統一實現和管理的。

1. skynet.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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

先在 proto 進程管理器字典中獲取指定服務類型 typename 對應的進程:

local p = proto[typename]
  • 1

再通過 skynet.core 這個C模塊的 send 功能,向指定服務發送數據:

local session = c.send(addr, p.id , nil , p.pack(...))
  • 1
  • addr 是服務的地址;
  • p 是處理 addr 對應服務的進程;
  • c 是一個C模塊:local c = require "skynet.core" ,即 skynet.core 模塊。

2. skynet.send 接口:

skynet.core 模塊的源碼在 lualib-src/lua-skynet.c 中,查看 luaopen_skynet_core 可以看到 { "send" , lsend },,即 skynet.core.send 其實就對應 lsend 方法:

//lua-skynet.c /* uint32 address/string address integer type integer session string message lightuserdata message_ptr integer len */ static int lsend(lua_State *L) { struct skynet_context * context = lua_touserdata(L, lua_upvalueindex(1)); //獲取第一個形參(uint32或string) uint32_t dest = (uint32_t)lua_tointeger(L, 1); const char * dest_string = NULL; if (dest == 0) { if (lua_type(L,1) == LUA_TNUMBER) { return luaL_error(L, "Invalid service address 0"); } dest_string = get_dest_string(L, 1); } //獲取第二個形參 int type = luaL_checkinteger(L, 2); int session = 0; if (lua_isnil(L,3)) { type |= PTYPE_TAG_ALLOCSESSION; } else { //獲取第三個形參 session = luaL_checkinteger(L,3); } //獲取第四個形參 int mtype = lua_type(L,4); switch (mtype) { case LUA_TSTRING: { size_t len = 0; void * msg = (void *)lua_tolstring(L,4,&len); if (len == 0) { msg = NULL; } if (dest_string) { session = skynet_sendname(context, 0, dest_string, type, session , msg, len); } else { session = skynet_send(context, 0, dest, type, session , msg, len); } break; } case LUA_TLIGHTUSERDATA: { void * msg = lua_touserdata(L,4); //獲取第五個形參 int size = luaL_checkinteger(L,5); if (dest_string) { session = skynet_sendname(context, 0, dest_string, type | PTYPE_TAG_DONTCOPY, session, msg, size); } else { session = skynet_send(context, 0, dest, type | PTYPE_TAG_DONTCOPY, session, msg, size); } break; } default: luaL_error(L, "skynet.send invalid param %s", lua_typename(L, lua_type(L,4))); } if (session < 0) { // send to invalid address // todo: maybe throw an error would be better return 0; } lua_pushinteger(L,session); return 1; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

當然,launch 服務最終還是調用的 skynet.launch("snlua","xxx") 來創建服務。


總結:

在skynet中的lua服務都是通過 laucher 服務(laucher.lua)來創建的(當然,除了 laucher 服務自身),而不是直接在調用 skynet.newservice 的服務上直接執行創建操作,這種實現方式的好處就是方便所有服務的跟蹤和管理。

參考資料:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM