skynet源碼分析 cluster與socketchannel


cluster就是集羣的意思了,和master/slave模式不同的是,它支持斷線重連,但是不能在同一條連接上既主動請求又能主動推送消息。

由於cluster和socketchannel分不開,並且skynet框架本身用到socketchannel的地方不多,我又暫時不想去研究mysql、redis庫,所以就把它們放在一起了。

本文在理解 skynet源碼分析07_socket阻塞庫(socket.lua)的基礎上配合2016年下旬最新版skynet源碼註釋更佳

先看看clusterd服務的創建與初始化

創建clusterd服務:

skynet.init(function()
    clusterd = skynet.uniqueservice("clusterd")
end)

調用 skynet.init 以便在"適當"的調用"clusterd = skynet.uniqueservice("clusterd")"時候創建 clusterd 服務

clusterd服務的初始化:

local config_name = skynet.getenv "cluster"

skynet.start(function()
    loadconfig()
    local function loadconfig()
        local f = assert(io.open(config_name))
        local source = f:read "*a"
        f:close()
        local tmp = {}
        assert(load(source, "@"..config_name, "t", tmp))()
        for name,address in pairs(tmp) do
            assert(type(address) == "string")
            if node_address[name] ~= address then
                -- address changed
                if rawget(node_channel, name) then
                    node_channel[name] = nil    -- reset connection
                end
                node_address[name] = address
            end
        end
    end
    skynet.dispatch("lua", function(session , source, cmd, ...)
        local f = assert(command[cmd])
        f(source, ...)
    end)
end)

先調用loadconfig將配置文件的cluster讀取出來,配置文件的cluster是一個文件路徑,裏面存放的是各一條或多條cluster節點的配置,通過node_channel與node_address將它們儲存起來。

再從最上層的API分析

結合cluster的wiki從 cluster.open、cluster.register兩個 API 入手。

cluster.open

function cluster.open(port)
    if type(port) == "string" then
        skynet.call(clusterd, "lua", "listen", port)
    else
        skynet.call(clusterd, "lua", "listen", "0.0.0.0", port)
    end

end

看看 clusterd 收到 listen 消息時怎麼處理的

function command.listen(source, addr, port)
    local gate = skynet.newservice("gate")
    if port == nil then
        addr, port = string.match(node_address[addr], "([^:]+):(.*)$")
    end
    skynet.call(gate, "lua", "open", { address = addr, port = port })
    skynet.ret(skynet.pack(nil))
end

通過向gate發送一個"open"消息來監聽這個端口。

cluster.register

當一個cluster節點調用 cluster.open 監聽一個端口以後,另一個cluster可以調用 cluster.call 連向監聽一方的 cluster 發送消息了。但是監聽一方的cluster節點的服務地址怎麼得到是一個問題,所以一般會先調用 cluster.register 註冊一個字符串地址。

function command.register(source, name, addr)
    assert(register_name[name] == nil)
    addr = addr or source
    local old_name = register_name[addr]
    if old_name then
        register_name[old_name] = nil
    end
    register_name[addr] = name
    register_name[name] = addr
    skynet.ret(nil)
    skynet.error(string.format("Register [%s] :%08x", name, addr))
end

在cluster節點中註冊一個字符串地址很簡單, 將節點的數字地址存在register_name即完成了。

A向B請求然後得到B返回的過程

分析下A向B請求然後得到B返回的過程

A節點調用cluster.call發送消息給B節點

function cluster.call(node, address, ...)
    -- skynet.pack(...) will free by cluster.core.packrequest
    return skynet.call(clusterd, "lua", "req", node, address, skynet.pack(...))
        function command.req(...)
            local ok, msg, sz = pcall(send_request, ...)
            if ok then
                skynet.ret(xxx)
end

可見會調用 send_request 來向另外一個節點發消息的,那麼是怎麼做到的呢?

local function send_request(source, node, addr, msg, sz)
    local session = node_session[node] or 1
    -- msg is a local pointer, cluster.packrequest will free it
    -- request 爲前面10個字節[(sz+9)、0(0x80, 0x81)、addr(int32)、session(int32)]加上msg組成的字符串
    -- session 會自增1
    local request, new_session, padding = cluster.packrequest(addr, session, msg, sz)
    node_session[node] = new_session

    -- node_channel[node] may yield or throw error
    local c = node_channel[node]

    return c:request(request, session, padding)
end

底層的接口就不看了,無非就是打包、解包。當首次發起 cluster.call 時, node_channel[node]爲nil,但是 node_channel 有元表:local node_channel = setmetatable({}, { __index = open_channel }),所以會在 open_channel 中"找到"這個值。

local function open_channel(t, key)
    local host, port = string.match(node_address[key], "([^:]+):(.*)$")
    local c = sc.channel {
        host = host,
        port = tonumber(port),
        response = read_response,
        nodelay = true,
    }
    assert(c:connect(true))
    t[key] = c
    return c
end

socket_channel.channel的看着很簡單,這裏貼上註釋過後的代碼先過一遍:

function socket_channel.channel(desc)
    local c = {
        __host = assert(desc.host),    -- ip地址
        __port = assert(desc.port),    -- 端口
        __backup = desc.backup,        -- 備用地址(成員需有一個或多個{host=xxx, port=xxx})
        __auth = desc.auth,            -- 認證函數
        __response = desc.response,    -- It's for session mode 如果是session模式,則需要提供此函數
        __request = {},    -- request seq { response func or session }    -- It's for order mode -- 消息處理函數,成員爲函數
        __thread = {}, -- coroutine seq or session->coroutine map    -- 存儲等待迴應的協程
        __result = {}, -- response result { coroutine -> result }    -- 存儲返回的結果,以便喚醒的時候能從對應的協程裏面拿出
        __result_data = {},                                            -- 存儲返回的結果,以便喚醒的時候能從對應的協程裏面拿出
        __connecting = {},    -- 用於儲存等待完成的隊列,隊列的成員爲協程:co
        __sock = false,        -- 連接成功以後是一個 table,第一次元素爲 fd,元表爲 channel_socket_meta
        __closed = false,    -- 是否已經關閉
        __authcoroutine = false,    -- 如果存在 __auth,那麼這裏存儲的是認證過程的協程
        __nodelay = desc.nodelay,    -- 配置是否啓用 TCP 的 Nagle 算法
        -- __dispatch_thread 消息處理函數的協程co
        -- __connecting_thread 等待連接完成的協程
    }

    return setmetatable(c, channel_meta)
end

然後來看看 c:connect,由於 c 中沒有 connect ,去它的元table:channel_meta 中找, channel_meta 中也沒 connect ,去 channel_meta 的元table:channel 中找,這下能找到了:

function channel:connect(once)
    if self.__closed then
        if self.__dispatch_thread then
            -- closing, wait
            assert(self.__connecting_thread == nil, "already connecting")
            local co = coroutine.running()
            self.__connecting_thread = co
            skynet.wait(co)
            self.__connecting_thread = nil
        end
        self.__closed = false
    end

    return block_connect(self, once)
end

由於是首次執行 channel:connect ,所以直接執行 block_connect

local function block_connect(self, once)
    local r = check_connection(self)
    if r ~= nil then
        return r
    end
    local err

    -- 如果正在等待連接完成的隊列大於0,則將當前協程加入隊列
    if #self.__connecting > 0 then
        -- connecting in other coroutine
        local co = coroutine.running()
        table.insert(self.__connecting, co)
        skynet.wait(co)
    else    -- 嘗試連接,如果連接成功,依次喚醒等待連接完成的隊列
        self.__connecting[1] = true
        err = try_connect(self, once)
        self.__connecting[1] = nil
        for i=2, #self.__connecting do
            local co = self.__connecting[i]
            self.__connecting[i] = nil
            skynet.wakeup(co)
        end
    end

    r = check_connection(self)
    if r == nil then
        skynet.error(string.format("Connect to %s:%d failed (%s)", self.__host, self.__port, err))
        error(socket_error)
    else
        return r
    end
end

是首次執行:直接跑到 else 分支,調用 try_connect 函數,然後調用 skynet.wakeup(co) 掛起當前的協程。 try_connect 應該就是重點了。

-- 嘗試連接,如果沒有明確指定只連接一次,那麼一直嘗試重連
local function try_connect(self , once)
    local t = 0
    while not self.__closed do
        local ok, err = connect_once(self)
        if ok then
            if not once then
                skynet.error("socket: connect to", self.__host, self.__port)
            end
            return
        elseif once then
            return err
        else
            skynet.error("socket: connect", err)
        end
        if t > 1000 then    -- 如果 once 不爲真,則一直嘗試連接
            skynet.error("socket: try to reconnect", self.__host, self.__port)
            skynet.sleep(t)
            t = 0
        else
            skynet.sleep(t)
        end
        t = t + 100
    end
end

由於最初初始化的時候 self.__closed 爲false,所以這裏while成立。執行 connect_once(去掉"干擾代碼"),如果連接失敗,從此函數後面的代碼可以看出如果 once 不爲真,就會一直嘗試連接。

local function connect_once(self)
    local fd,err = socket.open(self.__host, self.__port)
    if not fd then    -- 如果連接不成功,連接備用的地址
        fd = connect_backup(self)
    end
    if self.__nodelay then
        socketdriver.nodelay(fd)
    end

    self.__sock = setmetatable( {fd} , channel_socket_meta )
    self.__dispatch_thread = skynet.fork(dispatch_function(self), self)

    if self.__auth then
        self.__authcoroutine = coroutine.running()
        local ok , message = pcall(self.__auth, self)
        if not ok then
            close_channel_socket(self)
            if message ~= socket_error then
                self.__authcoroutine = false
                skynet.error("socket: auth failed", message)
            end
        end
        self.__authcoroutine = false
        if ok and not self.__sock then
            -- auth may change host, so connect again
            return connect_once(self)
        end
        return ok
    end

    return true
end

從上面可以看出, connect_once函數的工作爲:

  1. 調用 socket.open 主動連接遠端cluster節點,如果連接不成功,嘗試連接備用地址
  2. 如果 __nodelay 爲 true,設置不使用Nagle算法
  3. 設置 __sock = {fd} 並且元表爲 channel_socket_meta
  4. 調用skynet.fork開闢一個消息處理協程,並設置 __dispatch_thread 處理協程爲skynet.fork的返回值
  5. 如果__auth存在,則執行認證過程

如果連接成功的話,那麼 c:connect 函數會正常返回。 所以 send_request 函數中 node_channel[node] 也正常返回了。接下來執行 return c:request(request, session, padding)

function channel:request(request, response, padding)
    assert(block_connect(self, true))    -- connect once
    local fd = self.__sock[1]

    if padding then
        -- padding may be a table, to support multi part request
        -- multi part request use low priority socket write
        -- socket_lwrite returns nothing
        socket_lwrite(fd , request)
        for _,v in ipairs(padding) do
            socket_lwrite(fd, v)
        end
    else
        if not socket_write(fd , request) then
            close_channel_socket(self)
            wakeup_all(self)
            error(socket_error)
        end
    end

    if response == nil then
        -- no response
        return
    end

    return wait_for_response(self, response)
end

channel:request 的一般工作流程爲:

  1. 調用一次 block_connect,從前面可以看到 block_connect 函數的功用在於:如果沒有連接對端cluster節點,那麼連接;如果已經連接,直接返回
  2. 調用socket_write(fd , request) 將消息包發出去
  3. 調用 wait_for_response 等待消息返回

由此可知,調用 channel:request 將消息發到對端 cluster 節點,調用 wait_for_response 等待消息返回(從字面意思都能看出),看看 wait_for_response 的實現(主要代碼)。

local function wait_for_response(self, response)
    local co = coroutine.running()
    push_response(self, response, co)
    skynet.wait(co)

    local result = self.__result[co]
    self.__result[co] = nil
    local result_data = self.__result_data[co]
    self.__result_data[co] = nil

    return result_data
end

調用 push_response 將 response 加入到隊列__request(以當前協程爲key),這裏的response就是前面提到的 open_channel 中的 read_response,然後讓出協程,此次消息發送結束。

B節點收到A節點的消息請求

由於B節點是監聽的一方,前面提到過,是通過向 gate 服務發送一個 "open" 消息完成監聽的。那麼當B節點收到A節點的請求時,gate服務會先收到這個數據,然後將數據轉發給B節點的clusterd服務:skynet.send(watchdog, "lua", "socket", "open", fd, addr)。clusterd收到從gate服務轉發過來的socket消息的處理函數爲:command.socket

function command.socket(source, subcmd, fd, msg)
    if subcmd == "data" then
        local sz
        local addr, session, msg, padding = cluster.unpackrequest(msg)
        if padding then
            local req = large_request[session] or { addr = addr }
            large_request[session] = req
            table.insert(req, msg)
            return
        else
            local req = large_request[session]
            if req then
                large_request[session] = nil
                table.insert(req, msg)
                msg,sz = cluster.concat(req)
                addr = req.addr
            end
            if not msg then
                local response = cluster.packresponse(session, false, "Invalid large req")
                socket.write(fd, response)
                return
            end
        end
        local ok, response
        if addr == 0 then       -- 如果爲 0 代表是查詢地址
            local name = skynet.unpack(msg, sz)
            local addr = register_name[name]
            if addr then
                ok = true
                msg, sz = skynet.pack(addr)
            else
                ok = false
                msg = "name not found"
            end
        else
            ok , msg, sz = pcall(skynet.rawcall, addr, "lua", msg, sz)
        end
        if ok then
            response = cluster.packresponse(session, true, msg, sz)
            if type(response) == "table" then
                for _, v in ipairs(response) do
                    socket.lwrite(fd, v)
                end
            else
                socket.write(fd, response)
            end
        else
            response = cluster.packresponse(session, false, msg)
            socket.write(fd, response)
        end
    elseif subcmd == "open" then
        skynet.error(string.format("socket accept from %s", msg))
        skynet.call(source, "lua", "accept", fd)

當收到 "lua" "open" 時,向gate服務發送一個 "accept" 函數完成三路握手。

當收到 "lua" "data" 時,代表有請求過來了,處理請求(只考慮簡單情況):

  • 調用 cluster.unpackrequest(msg) 將網絡包解析出來(對應cluster.packrequest)
  • 如果是查詢字符串地址(後面會說到),從本地的 register_name 中取出數字地址,再調用 socket.write(fd, response)
  • 如果是A節點發送一個請求給B節點中的服務的請求,那麼調用pcall(skynet.rawcall, addr, "lua", msg, sz)向B節點本身的服務請求數據,返回的數據調用 socket.write 發送出去

從上面代碼可以看出,被請求的一方總是會有返回值返回給遠端的。

B節點收到A節點的消息返回

從前面的分析可以知道,B是主動調用 connect 函數進行連接的一方,它是通過 socketchannel來實現連接的,而socketchannel又會通過 socket.lua 來建立連接,所以返回的消息會先發送到 socket.lua 中,所以必須要調用 socket.read 讀取系列函數中的一個來接收數據。 從前面可以知道B節點註冊的消息處理函數爲:dispatch_by_session

local function dispatch_by_session(self)
    local response = self.__response
    while self.__sock do
        local ok , session, result_ok, result_data, padding = pcall(response, self.__sock)

其中pcall(response, self.__sock)中response函數是最初提供的,見 open_channel 函數中的 response = read_response, read_response 函數的實現:

local function read_response(sock)
    local sz = socket.header(sock:read(2))    -- sock:read(2)爲讀取前兩個字節 socket.header(sock:read(2))爲得到前兩個字節表示的 number 類型的值
    local msg = sock:read(sz)
    return cluster.unpackresponse(msg)    -- session, ok, data, padding
end

可見read_response函數會返回解包後的數據。回到 dispatch_by_session 函數(一般流程的主要代碼):

local function dispatch_by_session(self)
    local response = self.__response
    -- response() return session
    while self.__sock do
        local ok , session, result_ok, result_data, padding = pcall(response, self.__sock)
        if ok and session then
            local co = self.__thread[session]
            if co then
                self.__thread[session] = nil
                self.__result[co] = result_ok
                if result_ok and self.__result_data[co] then
                    table.insert(self.__result_data[co], result_data)
                else
                    self.__result_data[co] = result_data
                end
                skynet.wakeup(co)
            end
end

結合 read_response函數 可見 dispatch_by_session 的工作爲:

1. 調用 read_response 將遠端cluster節點發送過來的數據讀取出來
2. 然後將其返回值放在 __result 與 __result_data 然後調用 skynet.wakeup 喚醒 cluster.call 中的 wait_for_response 函數
3. wait_for_response 函數將結果從 __result 與 __result_data 中取出然後返回。

至此一次跨cluster節點的請求與返回流程結束。

cluster.query

cluster.query函數是用來查詢遠程節點的字符串地址

function cluster.query(node, name)
    -- 注意第5個參數爲0
    return skynet.call(clusterd, "lua", "req", node, 0, skynet.pack(name))

和發送數據請求差不多,它會請求 clusterd 服務發送一個請求消息給對端的cluster節點,對端cluster節點收到後會返回給它。

代理服務的實現

代理服務是通過cluster.proxy函數來實現的。

skynet.forward_type

在看cluster.proxy之前需要先看看看 skynet.forward_type 的實現:

function skynet.forward_type(map, start_func)
    c.callback(function(ptype, msg, sz, ...)
        local prototype = map[ptype]
        if prototype then
            dispatch_message(prototype, msg, sz, ...)
        else
            dispatch_message(ptype, msg, sz, ...)
            c.trash(msg, sz)
        end
    end, true)
    skynet.timeout(0, function()
        skynet.init_service(start_func)
    end)
end

skynet.forward_type 的工作爲:

  1. c.callback 爲服務註冊一個消息處理函數
  2. skynet.forward_type 的第一個參數是需要轉換消息類型的表,如果在表中存在的消息類型,就會轉換成另外一個類型
  3. 調用 dispatch_message 處理收到的消息,由於是代理的,所以c.callback的第二個參數爲 true,代表處理完成後不釋放消息的內存(因爲還要轉發到另外的服務當中去)
  4. 調用 skynet.init_service(start_func) 來調用 skynet.forward_type 的第二個函數

cluster.proxy

cluster.proxy可以生成一個本地代理服務(發送到此服務的消息都會被轉發到遠端的cluster節點)

function cluster.proxy(node, name)
    return skynet.call(clusterd, "lua", "proxy", node, name)
        local fullname = node .. "." .. name
        proxy[fullname] = skynet.newservice("clusterproxy", node, name)
        skynet.ret(skynet.pack(proxy[fullname]))
end

調用 cluster.proxy 會創建一個 clusterproxy 服務, 並得到一個本地服務的地址,看看 clusterproxy 服務的實現:

local forward_map = {
    [skynet.PTYPE_SNAX] = skynet.PTYPE_SYSTEM,
    [skynet.PTYPE_LUA] = skynet.PTYPE_SYSTEM,
    [skynet.PTYPE_RESPONSE] = skynet.PTYPE_RESPONSE,    -- don't free response message
}

skynet.forward_type( forward_map ,function()
    local clusterd = skynet.uniqueservice("clusterd")
    local n = tonumber(address)
    if n then
        address = n
    end
    skynet.dispatch("system", function (session, source, msg, sz)
        skynet.ret(skynet.rawcall(clusterd, "lua", skynet.pack("req", node, address, msg, sz)))
    end)
end)

由於 PTYPE_SNAX、PTYPE_LUA類型的消息被轉換成了 PTYPE_SYSTEM 類型,所以只要是這兩類的消息都會被轉發成 PTYPE_SYSTEM 類型的消息,而 PTYPE_SYSTEM 類型的消息處理函數工作爲:向clusterd發送一個"req"類型的消息,這樣消息能發到對端的cluster節點。 當對端cluster節點收到這個請求返回,那麼 PTYPE_SYSTEM 類型的消息處理函數也返回,這樣會返回到代理服務中。

簡單說說socketchannel的兩個模式

直接借用skynet SocketChannel的wiki來介紹這兩種模式:

  1. 每個請求包對應一個迴應包,由 TCP 協議保證時序。redis 的協議就是一個典型。每個 redis 請求都必須有一個迴應,但不必收到迴應纔可以發送下一個請求。
  2. 發起每個請求時帶一個唯一 session 標識,在發送迴應時,帶上這個標識。這樣設計可以不要求每個請求都一定要有迴應,且不必遵循先提出的請求先回應的時序。MongoDB 的通訊協議就是這樣設計的。

所以在socketchannel中對應的第一種模式爲 order模式,相應的消息處理函數爲:dispatch_by_order

第二種模式爲 session模式,相應的消息處理函數爲:dispatch_by_session

這裏 order模式就不拿代碼詳細分析了,和 session模式差不了太多,結合wiki和cluster的分析很容易得出兩者的區別

小結

  1. socketchannel有兩種模式,分爲: order模式與cluster模式
  2. socketchannel支持斷線重連,主要是依賴每次發包如果發現斷線了就嘗試重連,而且兩個cluster節點連接過程是在第一個消息請求時(即如果沒有消息要發送,那麼兩個節點就不會連接)。
  3. cluster模式的兩個節點主動監聽的一方不能主動發起請求,如果A B兩個cluster節點想要實現A能向B請求而且B也能向A請求,需要建立兩條連接
  4. A節點向B節點請求消息,B節點總會有返回給A的

Permanent link of this article:http://nulls.cc/post/skynet_srccode_analysis09_cluster_and_socketchannel

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