skynet範例研究-服務端

源碼分爲3個文件夾,分別爲service、lualib、src。

其中service主要是服務端 業務邏輯 ,lualib爲 基礎工具封裝 ,src爲 C語言服務封裝 。

一般閱讀代碼時先從main入手,跟着邏輯一步一步不斷深入。

main.lua

文件路徑:service/main.lua

local skynet = require "skynet"
skynet.start(function()
    skynet.error("Server start")    -- 打印Server start
    if not skynet.getenv "daemon" then
        local console = skynet.newservice("console")    -- 創建控制檯
    end
    skynet.newservice("debug_console",8000)            -- 啓動控制檯服務
    local proto = skynet.uniqueservice "protoloader"    -- 啓動封裝的protoloader服務
    skynet.call(proto, "lua", "load", {                -- 調用protoloader服務的load接口
        "proto.c2s",  -- 客戶端 to 服務器
        "proto.s2c",  -- 服務器 to 客戶端
    })
    local hub = skynet.uniqueservice "hub"              -- 啓動hub服務
    skynet.call(hub, "lua", "open", "0.0.0.0", 5678)    -- 調用hub服務中的open接口,監聽5678網絡端口
    skynet.exit()
end)

入口函數很簡介,首先啓動了控制檯服務,該服務使得skynet在運行時,可以打印log以及信息到終端界面。

protoloader服務爲proto協議的封裝,注意這裏的protoloader爲自定義封裝,而不是skynet核心代碼裏自帶的那個。

首先調用skynetuniqueservice掛起了protoloader服務,所以接下來看看protoloader是怎麼運行的。

protoloader.lua

文件路徑:service/protoloader.lua

local skynet = require "skynet"
local sprotoparser = require "sprotoparser" --加載skynet/lualib下的sproto解析器
local sprotoloader = require "sprotoloader" --sproto加器器
local service = require "service"          --加載範例根目錄lualib下的service
local log = require "log"                  --加載範例根目錄lualib下的log
local loader = {}  --保存函數
local data = {}    --保存加載後的sproto協議在skynet sprotoparser裏的序號,key值爲文件名
local function load(name)
    local filename = string.format("proto/%s.sproto", name)
    local f = assert(io.open(filename), "Can't open " .. name)
    local t = f:read "a"
    f:close()                      --以上爲讀取文件內容
    return sprotoparser.parse(t)    --調用skynet的sprotoparser解析sproto協議
end
function loader.load(list)
    for i, namein ipairs(list) do
        local p = load(name)    --加載sproto協議
        log("load proto [%s] in slot %d", name, i)
        data[name] = i
        sprotoloader.save(p, i) --保存解析後的sproto協議
    end
end
function loader.index(name)
    return data[name]  --返回sproto協議在skynet sprotoloader裏序號
end
-- 初始化服務的info信息和函數
service.init {
    command = loader,
    info = data
}

可以看到主要是定義瞭如何加載proto協議並保存,以及獲取協議的編號(slot),然後最後面調用了另一個lua文件的代碼service.init方法。

service.lua

文件路徑:lualib/service.lua

local skynet = require "skynet"
local log = require "log"
local service = {}
-- 初始化服務,主要功能爲:1:註冊服務info 2:註冊服務的命令函數 3:啓動服務
function service.init(mod)
    local funcs = mod.command
    if mod.infothen
        skynet.info_func(function() --
            return mod.info
        end)
        -- 這裏僅作調試用,當在調試模式下,輸入 “info 服務ID” 就會打印上面返回的信息
        -- 調試模式的啓動方法爲 nc 127.0.0.1 8000
    end
    skynet.start(function()
        if mod.require then
            local s = mod.require
            for _, namein ipairs(s) do
                service[name] = skynet.uniqueservice(name)  --啓動服務,並將該服務器保存在service下
            end
        end
        if mod.initthen
            mod.init()
        end
        skynet.dispatch("lua", function (_,_, cmd, ...) -- 修改lua協議的dispatch函數,對當前調用init的服務註冊函數
                                                        -- skynet.dispatch函數也是服務啓動的結束標示
            local f = funcs[cmd]    --獲取命令函數
            if f then
                skynet.ret(skynet.pack(f(...))) --返回命令調用結果,所有通過ret的返回值都要用pack打包
            else
                log("Unknown command : [%s]", cmd)
                skynet.response()(false)
            end
        end)
    end)
end
--[[skynet.ret 在當前協程(爲處理請求方消息而產生的協程)中給請求方(消息來源)的消息做迴應
skynet.retpack 跟skynet.ret的區別是向請求方作迴應時要用skynet.pack打包]]--
return service

該代碼做的主要有3個工作,

  • 設定當前skynet服務debug info信息
  • 設定當前skynet服務在接收到其他服務調用信息時的處理,比如上一段protoloader代碼中的load函數和index函數。其他服務只要發送”函數名”或者“參數”,即可調用對應的函數。其中一個值得注意的點是,skynet中服務與服務的通信必須通過 skynet.ret 和 skynet.retpack 函數進行打包和封包處理。
  • 加載其他服務,並保存在service這個table中。
  • 往後所有服務的代碼都會通過init函數處理info信息、註冊函數、加載其他服務。

回到main.lua中,可以看到調用了service.init中設定的load函數。接着掛起了hub服務。

爲了不讓文章變的太過冗長,下面會省略一些具體實現邏輯,主要以梳理流程爲主,只有在關鍵點纔會有詳細的解釋。

hub.lua

文件路徑:service/hub.lua

local skynet = require "skynet"
local socket = require "socket"
local proxy = require "socket_proxy"    --加載範例lualib目錄下的socket_proxy
local log = require "log"
local service = require "service"
local hub = {}  --保存函數
local data = { socket = {} }    --保存監聽到的鏈接
-- 調用auth服務
local function auth_socket(fd)
    ...
end
local function assign_agent(fd, userid)
    ...
end
--所有監聽到的新鏈接都會首先交給這個函數處理
function new_socket(fd, addr)
    ...
end
--打開監聽
function hub.open(ip, port)
    ...
end
--關閉監聽
function hub.close()
    ...
end
--服務初始化,並掛起auth和manager服務
service.init {
        ... 
    require = {
        "auth",
        "manager",
    }
}

可以看到沒有執行具體邏輯,主要以定義爲主,並啓動了兩個服務,manager和auth。

在最開始local proxy = require “socket_proxy”中還啓動了socket_proxyd服務。

不過在這邊並有具體的邏輯執行,所以先不管,順着流程繼續看。

監聽網絡端口

回到main.lua,調用了hub服務中的open函數,開始監聽網絡端口。來看看該函數的具體實現。

function hub.open(ip, port)
    log("Listen %s:%d", ip, port)
    assert(data.fd == nil, "Already open")  --判斷監聽是否打開
    data.fd = socket.listen(ip, port)      --新建監聽端口
    data.ip = ip
    data.port = port
    socket.start(data.fd, new_socket)      --開始監聽,將監聽到的鏈接返回到new_socket函數
end

這時如果客戶端發起請求,就會將監聽到的鏈接交給new_socket函數處理

function new_socket(fd, addr)
    data.socket[fd] = "[AUTH]"
    proxy.subscribe(fd) --將新鏈接提交給socketproxy
    local ok , userid =  pcall(auth_socket, fd)
    if okthen
        data.socket[fd] = userid
        if pcall(assign_agent, fd, userid) then
            return  -- succ
        else
            ...
        end
    else
        ...
    end
end

首先調用了socket_proxy.lua中的subscribe函數

文件路徑:lualib/socket_proxy.lua

function proxy.subscribe(fd)
    local addr = map[fd]
    if not addrthen
        addr = skynet.call(proxyd, "lua", fd)  --向proxyd 發送命令,創建基於fd鏈接的服務,詳見socket_proxyd.lua
        map[fd] = addr  --保存服務地址
    end
end

向proxyd服務發送鏈接對象

文件路徑:service/socket_proxyd.lua

local function subscribe(fd)
    local addr = socket_fd_addr[fd]
    if addrthen
        return addr    --如果連接已經保存則直接返回服務ID
    end
    addr = assert(skynet.launch("package", skynet.self(), fd))  --創建c語言服務package(連接代理),跑當前服務(self()返回當前服務ID),返回的是c服務的地址
    socket_fd_addr[fd] = addr  --保存c服務的地址,key值爲鏈接
    socket_addr_fd[addr] = fd  --保存鏈接,key值爲c服務的地址
    socket_init[addr] = skynet.response()  --保存該鏈接的response函數,迴應函數,這裏同時會迴應之前call過來的服務,告訴他addr
end

這裏創建了一個c語言編寫的package服務,不必關心具體實現,只要知道它的功能主要是讓當前鏈接成爲一個單獨的skynet服務即可,返回值是該服務的ID。

最後通過skynet.response函數返回服務ID給調用前的函數,也就是socket_proxy的subscribe。

處理鏈接數據

一路回到hub.lua的new_socket函數。又將鏈接提交給auth_socket函數

local function auth_socket(fd)
    return (skynet.call(service.auth, "lua", "shakehand" , fd))
end

通過service.init啓動的auth服務,調用其中的shakehand函數,併發送fd鏈接對象。

文件路徑:service/auth.lua

function auth.shakehand(fd)
    local c = client.dispatch { fd = fd }  --將鏈接交給client對信息進行處理
    return c.userid
end

這裏可以理解爲和客戶端一第次“握手”,調用client的dispatch函數,併發送鏈接。

文件路徑:lualib/client.lua

--消息處理
function client.dispatch( c )
    local fd = c.fd
    proxy.subscribe(fd)
    local ERROR = {}
    while true do
        local msg, sz = proxy.read(fd)  --讀取連接發來的數據
        local type, name, args, response = host:dispatch(msg, sz)  --sproto解析數據
        assert(type == "REQUEST")  --此處保證連接數據爲請求
        if c.exitthen  --exit參數爲退出循環標誌
            return c
        end
        local f = handler[name]    --通過sproto解析出來的數據,獲取回調函數
        if f then
            -- f may block , so fork and run
            -- 此處創建一個協程運行回調函數
            skynet.fork(function()
                local ok, result = pcall(f, c, args)    -- 回調函數具體詳見agent,auth,回調函數在那邊實現
                                                        -- 此處回調函數都爲顯式傳參,將c顯式傳到回調函數中
                if okthen
                    proxy.write(fd, response(result))  -- 使用sproto解析出來的response函數包裝返回值,併發送數據
                else
                    log("raise error = %s", result)
                    proxy.write(fd, response(ERROR, result))
                end
            end)
        else
            -- unsupported command, disconnected
            error ("Invalid command " .. name)
        end
    end
end

這個函數爲消息處理最關鍵的函數,所有請求都通過這裏進行分發,其中回調函數都保存在handler這個table中,當前範例的回調函數都在agent.lua和,auth.lua中定義。

通過代碼可以看,該函數是一個死循環,也就是說,會不斷的讀取客戶端發送過來的信息進行處理。只有當exit變量爲ture時才退出,這個exit怎麼來的先不管。

接下來通過客戶端發送的請求流程繼續梳理。

網絡請求流程

回顧下客戶端的消息流程:

首先會發送signin請求,發現sign失敗,用戶不存在,繼續發送signup請求註冊用戶,註冊成功後再次發送signin請求,登入成功,接着會發送login請求,後面的就是正常的業務請求了。請求順序如下:

signin > signup > signin > login > other

一步一步來,首先是signin,回到上面的dispatch,通過請求信息調用名爲signin回調函數。signin的回調實現在auth.lua中

文件路徑:service/auth.lua

-- signin登入請求回調
function cli:signin(args)
    log("signin userid = %s", args.userid)
    if users[args.userid] then
        self.userid = args.userid  --self爲修改隱式參數
        self.exit = true    --退出client中dispatch循環,表示登入成功,退出auth服務,進入下一個服務
        return SUCC
    else
        return FAIL
    end
end

可以看到,當客戶端第一次請求signin時,是返回失敗FAIL的。這時又會回到client的dispatch函數,返回客戶端註冊失敗消息後,繼續讀取鏈接信息。

當客戶端接到signin失敗後,會緊接着發送signup請求。

同樣來到auth.lua

-- signup註冊賬號請求回調
function cli:signup(args)
    log("signup userid = %s", args.userid)
    if users[args.userid] then
        return FAIL
    else
        users[args.userid] = true
        return SUCC
    end
end

這時就回返回成功,回到dispath發送註冊成功消息。

接下來客戶端再次發送sigin請求就會signin成功。

要注意的是,在signin回調函數中,如果signin成功則會修改exit變量,這會導致退出client的dispatch循環。爲什麼這裏能修改exit變量呢,原因是所有回調函數都是 隱式傳參 的,在函數定義中使用了“ 冒號 ”,表示隱式傳送 self參數 。在dispatch中可以看到調用回調時,都會傳進c變量。pcall(f, c, args)

登入成功後就會退出dispatch的循環,那麼跟蹤代碼退到hub中的new_socket方法,發現又調用了assign_agent函數。

local function assign_agent(fd, userid)
    skynet.call(service.manager, "lua", "assign", fd, userid)
end

assign_gent則調用了service中保存的manager服務,同時調用manage服務中的assign方法,並傳入鏈接對象和登入成功返回的userid。

文件路徑:service/manager.lua

function manager.assign(fd, userid)
    local agent
    repeat
        agent = users[userid]  --判斷是否有當前用戶的服務
        if not agentthen      --若沒有則創建一個
            agent = new_agent()
            if not users[userid] then
                -- double check
                users[userid] = agent
            else
                free_agent(agent)
                agent = users[userid]
            end
        end
    until skynet.call(agent, "lua", "assign", fd, userid)  --此處返回ture,跳出循環
    log("Assign %d to %s [%s]", fd, userid, agent)
end

順着流程走,由於客戶端第一次登入,會調用new_agent方法。

local function new_agent()
    -- todo: use a pool
    return skynet.newservice "agent"
end

這裏會爲每個用戶啓動一個agent服務。

接着until中會調用該agent服務的assign方法。

文件路徑:service/agent.lua

function agent.assign(fd, userid)
        ... 
    skynet.fork(new_user, fd)
    return true
end

做了很簡單一件事,調用了new_user這個方法,這裏要注意的是,使用了skynet.fork方法調用,該方法的作用是,不會阻塞線程,爲什麼要這麼做接着往後看。

local function new_user(fd)
    local ok, error = pcall(client.dispatch , { fd = fd })  --進入客戶端消息循環,若此處客戶端長時間沒有任何操作,而報超時錯誤返回
    ...
end

可以看到,這裏調用了client中的dispatch,前面知道該函數是個死循環,這就是爲什麼之前使用了skynet.fork的原因了。

好了,這樣就又進入消息處理循環了,之前客戶端已經發送了signin,signup,signin,這三個請求,接下來會發送login請求,去到login請求回調。該回調定義在agent.lua中。

function cli:login()
    assert(not self.login)
    if data.fdthen --重複登入
        log("login fail %s fd=%d", data.userid, self.fd)
        return { ok = false }
    end
    data.fd = self.fd
    self.login = true
    log("login succ %s fd=%d", data.userid, self.fd)
    client.push(self, "push", { text = "welcome" }) -- push message to client
    return { ok = true }
end

主要就是調用client.push函數發送welcome信息,再來看看client中push函數的實現

function client.push(c, t, data)
    proxy.write(c.fd, sender(t, data))
end

調用socket_proxy中的write

function proxy.write(fd, msg, sz)
    skynet.send(get_addr(fd), "client", msg, sz)
end

這裏用到了skynet.send函數,第一參數是服務地址,就是之前創建的package服務。

以上就是範例中服務端代碼的主要邏輯。另外我對服務端代碼進行了大部分註釋。

鏈接: http://pan.baidu.com/s/1dFKD2oh 密碼: rfc9

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章