skynet框架應用 (十) socketChannel

10 socketChannel

​ 在與外部服務交互式時,請求迴應模式是最常用模式之一。通常的協議設計方式有兩種。

  1. 每個請求包對應一個迴應包,由 TCP 協議保證時序。

  2. 發起每個請求時帶一個唯一 session 標識,在發送迴應時,帶上這個標識。這樣設計可以不要求每個請求都一定要有迴應,且不必遵循先提出的請求先回應的時序。

​ 對於第一種模式,用 skynet 的 Socket API 很容易實現,但如果在一個 coroutine 中讀寫一個 socket 的話,由於讀的過程是阻塞的,這會導致吞吐量下降(前一個迴應沒有收到時,無法發送下一個請求,9.8我們就是這麼設計的)。

​ 對於第二種模式,需要用 skynet.fork 開啓一個新線程來收取回響應包,並自行和請求對應起來,實現比較繁瑣,比如9.9中我們遇到的困惑。

​ 所以skynet 提供了一個更高層的封裝:socket channel

10.1 第一種模式的socketChannel

示例代碼如下:


local skynet = require "skynet"
require "skynet.manager"
local sc = require "skynet.socketchannel"

local channel = sc.channel {  --創建一個 channel 對象出來,其中 host 可以是 ip 地址或者域名,port 是端口號。
  host = "127.0.0.1",
  port = 8001,
}

--接收響應的數據必須這麼定義,sock就是與遠端的TCP服務相連的套接字,通過這個套接字可以把數據讀出來
function response(sock)    
--返回值必須要有兩個,第一個如果是true表示響應數據是有效的,
    return true, sock:read()   
end

local function task()
    local resp
    local i = 0
    while(i < 3) do
        --第一參數是需要發送的請求,第二個參數是一個函數,用來接收響應的數據。
        --調用channel:request會自動連接指定的TCP服務,並且發送請求消息。
        --該函數阻塞,返回讀到的內容。
        resp = channel:request("data"..i.."\n", response)  
        skynet.error("recv", resp)
        i = i + 1
    end
    --channel:close()   --channel可以不用關閉,當前服務退出的時候會自動關閉掉
    skynet.exit()
end

skynet.start(function()
    skynet.fork(task)
end)

看下運行結果(serverreadline.lua是9.8中編寫的):


$ ./skynet examples/config
serverreadline #先運行serverreadline.lua
[:01000010] LAUNCH snlua serverreadline
[:01000010] listen 0.0.0.0:8001
channelclient
[:01000012] LAUNCH snlua channelclient
[:01000010] 127.0.0.1:44098 accepted
[:01000010] recv data0
[:01000012] recv DATA0
[:01000010] recv data1
[:01000012] recv DATA1
[:01000010] recv data2
[:01000012] recv DATA2
[:01000012] KILL self
[:01000010] 127.0.0.1:44098 disconnect

​ sock 是由 request 方法傳入的一個對象,sock 有兩個方法:read(self, sz)readline(self, sep)

​ response 函數的第一個返回值需要是一個 boolean ,如果爲 true 表示協議解析正常;如果爲 false 表示協議出錯,這會導致連接斷開且讓 request 的調用者也獲得一個 error 。

​ 在 response 函數內的任何異常以及 sock:read 或 sock:readline 讀取出錯,都會以 error 的形式拋給 request 的調用者。

​ 比如將上面的response函數第一個返回值改爲false,運行結果如下:


$ ./skynet examples/config
serverreadline
[:01000010] LAUNCH snlua serverreadline
[:01000010] listen 0.0.0.0:8001
channelclient
[:01000012] LAUNCH snlua channelclient
[:01000010] 127.0.0.1:44120 accepted
[:01000010] recv data0
[:01000012] lua call [0 to :1000012 : 0 msgsz = 24] error : ./lualib/skynet.lua:534: ./lualib/skynet.lua:156: ./lualib/skynet/socketchannel.lua:377: DATA0
stack traceback:
    [C]: in function 'assert'
    ./lualib/skynet/socketchannel.lua:377: in function <./lualib/skynet/socketchannel.lua:360>
    (...tail calls...)
    ./my_workspace/channelclient.lua:19: in upvalue 'func'
    ./lualib/skynet.lua:468: in upvalue 'f'
    ./lualib/skynet.lua:106: in function <./lualib/skynet.lua:105>
stack traceback:
    [C]: in function 'assert'
    ./lualib/skynet.lua:534: in function 'skynet.manager.dispatch_message'

10.2 第二種模式的socketChannel

​ 第二種模式需要在 channel 創建時給出一個通用的 response 解析函數。


local channel = sc.channel {
  host = "127.0.0.1",
  port = 8002,
  response = dispatch,
}
--這裏 dispatch 是一個解析迴應包的函數,和上面提到的模式 1 中的解析函數類似。但其返回值需要有三個。第一個是這個迴應包的 session,第二個是包是否解析正確(同模式 1 ),第三個是迴應內容。

socket channel 就是依靠創建時是否提供 response 函數來決定工作在模式 1 還是模式 2 下的。

​ 在模式 2 下,channel.request 的參數有所變化。第 2 個參數不再是 response 函數(它已經在創建時給出),而是一個 session 。這個 session 可以是任意類型,但需要和 dispatch函數返回的類型一致。socket channel 會幫你匹配 session 而讓 channel.request 返回正確的值。

示例代碼:channelclient2.lua


local skynet = require "skynet"
require "skynet.manager"
local sc = require "skynet.socketchannel"



function dispatch(sock)   
    local r = sock:readline() 
    local session = tonumber(string.sub(r,5))
    return session, true, r  --返回值必須要有三個,第一個session
end
 --創建一個 channel 對象出來,其中 host 可以是 ip 地址或者域名,port 是端口號。
local channel = sc.channel { 
  host = "127.0.0.1",
  port = 8001,
  response = dispatch   --處理消息的函數
}


local function task()
    local resp
    local i = 0
    while(i < 3) do
        skynet.fork(function(session)
            resp = channel:request("data"..session.."\n", session)  
            skynet.error("recv", resp, session)
        end, i)
        i = i + 1
    end
end

skynet.start(function()
    skynet.fork(task)
end)

運行結果:


$ ./skynet examples/config
serverreadline
[:01000010] LAUNCH snlua serverreadline
[:01000010] listen 0.0.0.0:8001
channelclient2
[:01000012] LAUNCH snlua channelclient2
[:01000010] 127.0.0.1:44172 accepted
[:01000010] recv data0
[:01000010] recv data1
[:01000010] recv data2
[:01000012] recv DATA1 1   #能夠知道DATA1就是對應session 1的應答
[:01000012] recv DATA2 2
[:01000012] recv DATA0 0

發佈了101 篇原創文章 · 獲贊 117 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章