在與外部服務交互式時,請求迴應模式是最常用模式之一。通常的協議設計方式有兩種。
每個請求包對應一個迴應包,由 TCP 協議保證時序。
發起每個請求時帶一個唯一 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