在与外部服务交互式时,请求回应模式是最常用模式之一。通常的协议设计方式有两种。
每个请求包对应一个回应包,由 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