12-主动连接之socketChannel
新入门skynet系列视频b站网址 https://www.bilibili.com/video/BV19d4y1678X
主动连接之socketChannel
当我们打算主动跟外部通讯时,我们一般使用socketchannel。
socketchannel有两种工作模式。一种是 order ,一种是session。
我们一般使用socketchannel的过程就是两步
- 创建一个soketchannel对象
local channel = socketchannel.channel(conf)
- 利用这个对象发送请求即
local data = channel:request(request, response, padding)
我们看看一个例子
db = redis.connect {
host = "127.0.0.1",
port = 6300,
db = 0,
auth = "foobared"
}
print("dbsize:", db:dbsize())
上面就是我们主动连接redis的例子。redis.connect
函数实际上就是利用socketchannel连接redis的。当拿到一个连接对象 db后,发送请求给redis,等待redis回应即可。这里发送请求就是调用 db:dbsize()
。下面继续看看 redis.connect
function redis.connect(db_conf)
local channel = socketchannel.channel {
host = db_conf.host,
port = db_conf.port or 6379,
auth = redis_login(db_conf), -- 设置验证函数
nodelay = true,
overload = db_conf.overload,
}
-- try connect first only once
channel:connect(true)
return setmetatable( { channel }, meta )
end
上面做的事情就是
- 利用
socketchannel.channel
创建一个channel, - 发起连接redis一次。
- 返回一个对象。这个对象就是我们用来发送请求给数据库的工具。
更具体的过程是这样:当我们创建一个socketchannel对象时,并没有发起主动连接。只有当发送请求时才会开始连接。发送请求的时候,会判断连接是否成功,否则会主动阻塞的去连接目标服务器。如果连接是成功的,那么就会调用socket.write发送请求,同时会把响应回调
准备好。此时协程会挂起。等到响应数据到达时,会调用回调响应
把数据从底层提取出来,然后唤醒发起请求的协程,最后 channel:request(request, response, padding)
返回给调用者需要的响应数据
主动调用
channel:connect(true)
是会发起一次连接的。
order mode
连接redis就是使用order模式。这种特点是,假设按照顺序发送三个请求 a b c,那么响应的顺序一定是 a的响应 b的响应 c的响应。模式的设置是在创建channel时是否给出 回调响应函数
来区分的。如果有,就是session模式。注意看下面行7
function socket_channel.channel(desc)
local c = {
__host = assert(desc.host),
__port = assert(desc.port),
__backup = desc.backup,
__auth = desc.auth,
__response = desc.response, -- It's for session mode
}
return setmetatable(c, channel_meta)
end
接下来看发送请求过程的代码。即 channel:request(request, response, padding)
function channel:request(request, response, padding)
assert(block_connect(self, true)) -- connect once
local fd = self.__sock[1]
if not socket_write(fd , request) then
sock_err(self)
end
if response == nil then
-- no response 不要响应 只发请求
return
end
return wait_for_response(self, response) --response 可以是响应函数 或者是 session
end
tip:关于lua中冒号的用法
local obj = socket_channel.channel(desc)
obj:request(requst,response) --此时上面代码中的self就是obj对象
行2 就是先保证连接是成功的。
行5 把请求数据交给底层处理
行9 是一种特殊用法。表示我们不关注响应。
行14 就是等待回应数据。如果对端的响应数据达到,这里最终会返回给requst的调用者。
我们来分析这样一个场景:
我们先创建了一个channel。开始在一个协程中发送请求req 给redis,当时这个连接还没建立。在建立连接时我们还需要进行验证,第一次验证我们失败了,第二次验证才成功。同时在第一次验证的过程中,有其他协程也用这个channel发送请求给redis。
下面具体看 代码 block_connect(self, true)
,即下面的 行11开始
local function check_connection(self)
if self.__sock then -- 此时self.__sock 为 nil
--
end
if self.__closed then -- 默认 self.__closed 为 false
return false
end
-- return nil
end
local function block_connect(self, once)
local r = check_connection(self) --此时返回值为 nil
if r ~= nil then
return r
end
local err
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) --阻塞协程
for i=2, #self.__connecting do
local co = self.__connecting[i]
self.__connecting[i] = nil
skynet.wakeup(co)
end
self.__connecting[1] = nil
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
上面行11 即check_connection
返回nil。我们关注 行21 这个分支。
行23 会设置连接正在进行。
行24 去尝试连接。
在看 try_connect(self, once)
这里主要是调用 connect_once
去建立连接。当然如果设置了反复连接的话,会每隔一秒钟连接一次。
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
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
到 connect_once
里面看。实际上主要是分析他调用的 _connect_once
local function connect_once(self)
if self.__closed then return false end
local addr_list = {}
local addr_set = {}
local function _add_backup()end -- 添加备用地址
local function _next_addr()end --获取下一个地址
local function _connect_once(self, addr)
local fd,err = socket.open(addr.host, addr.port) --发起主动连接
if not fd then
-- try next one
addr = _next_addr()
if addr == nil then
return false, err
end
return _connect_once(self, addr)
end
self.__host = addr.host
self.__port = addr.port
assert(not self.__sock and not self.__authcoroutine)
-- term current dispatch thread (send a signal)
term_dispatch_thread(self) --终止分发协程
-- register overload warning
local overload = self.__overload_notify --过载通知
if overload then end
while self.__dispatch_thread do
-- wait for dispatch thread exit
skynet.yield()
end
self.__sock = setmetatable( {fd} , channel_socket_meta )
self.__dispatch_thread = skynet.fork(function() --开启一个分发协程
pcall(dispatch_function(self), self)-- 这里是order mode 的分发协程 dispatch_by_order
-- clear dispatch_thread
self.__dispatch_thread = nil
end)
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 then --验证成功
if not self.__sock then --mongodb验证的时候就可能有这种情况
-- auth may change host, so connect again
return connect_once(self)
end
-- auth succ, go through
else -- 本次验证失败 需要尝试新地址
-- auth failed, try next addr
_add_backup() -- auth may add new backup hosts
addr = _next_addr()
if addr == nil then
return false, "no more backup host"
end
return _connect_once(self, addr)
end
end
return true
end
_add_backup()
return _connect_once(self, { host = self.__host, port = self.__port })
end
上面的连接主要过程是:连接目标服务器,然后开启一个分发协程。然后开始验证。实际上验证也是调用 channel:request(),所以此时当前协程,也叫验证协程 y
会挂起。让出协程后,分发协程开始执行。他会从一个阻塞的队列中把已经发送的请求
对应的响应处理函数
取出来(当前取出的响应函数正好是验证响应处理函数
),然后等待读取网络数据(实际上就是利用socket.read相关函数),当数据到达时,唤醒之前挂起的协程y
,分发协程继续挂起等待别人push响应处理函数
。协程y会继续执行,如果验证通过,最终导致 block_connect 返回成功,也就是我们的连接成功了,后续可以发送请求了。
但是我们假设此次验证失败了。那么会继续验证,而其他协程如果想要发送请求给redis,那么其他协程都会被挂起,因为要发送请求,必须要保证先连接成功。验证没有成功,连接当然就是没成功的。也就是最终验证成功,其他协程才会被唤醒,才会有机会发送请求。
我们看看 当我们验证协程把请求发送出去,挂起自己,然后分发协程是怎么发现验证失败的。看分发协程的任务函数 dispatch_by_order
local function dispatch_by_order(self)
while self.__sock do
local func, co = pop_response(self) -- 从队列中取出 响应函数 如果 没有响应函数 就挂起
if not co then -- 收到了关闭的通知
-- close signal
wakeup_all(self, "channel_closed")
break
end
local sock = self.__sock
if not sock then -- 虽然队列里面有响应函数 但是对端把连接关闭了
-- closed by peer
self.__result[co] = socket_error
skynet.wakeup(co)
wakeup_all(self)
break
end --下面不ok 说明网络出错result_ok是socket_error;如果redis正常返回值 result_ok可以为false
local ok, result_ok, result_data = pcall(get_response, func, sock) --取出响应数据
if ok then
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) -- 唤醒发起请求的协程
else
close_channel_socket(self)
local errmsg
if result_ok ~= socket_error then --这里相当于是在网络出错下,再给出更具体的错误信息
errmsg = result_ok
end
self.__result[co] = socket_error
self.__result_data[co] = errmsg --网络出错下更具体的报错提示
skynet.wakeup(co)
wakeup_all(self, errmsg)
end
end
end
-
行3 我们取出验证响应函数
-
行17 阻塞读取响应数据。下面讨论此后可能发生的两种情况。1.验证失败 2.网络断开
- 1.此后如果验证失败,那么走 行19。毕竟验证返回数据都是正常的。此时 result_ok是false。result_data是错误提示字符串。我们看此时验证协程会唤醒,那么发送请求后得到的数据是什么呢 看 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] --此时是 false self.__result[co] = nil local result_data = self.__result_data[co] -- 此时是redis返回的错误提示 self.__result_data[co] = nil if result == socket_error then --网络出错 if result_data then --有更具体的报错提示 error(result_data) else --没有更具体的报错提示 error(socket_error) end else -- 正常情况下执行这里,最终返回result_data;但是也可能是正常情况下的报错,比如验证密码不对 assert(result, result_data) --如果result是false是 这里会抛出异常 result_data是错误提示 return result_data end end
好在验证协程是以保护模式运行,代码是
local ok , message = pcall(self.__auth, self)
,所以这里ok是false。后续会主动关闭连接,然后重新验证.重新验证的时候会把当前分发协程关闭。当然这里重新验证的意思是 有备用地址。不然就会直接报错,比如密码不正确。这个错误一般是不会捕获的,因为重新验证的本质是默认你的密码是正确的,连密码都搞错了,这个后果自负了。
- 2.此后如果网络断开,那么 get_response函数无法正常执行,那么走 上面
dispatch_by_order
函数的行27。具体原因是 get_response 会调用响应回调函数
获取数据,而这个响应回调函数
会抛出异常。我们看下面redis是发送请求的的代码 注意行7 ,相应回调函数
是read_response
。一旦连接断开了,然后再去调用该函数,则会出现异常。 最终会重新验证。
local function wrapper_socket_function(f) --f就是 socket.readline return function(self, ...) local result = f(self[1], ...) --self 就是 self.__sock if not result then --只要返回false就表示网络出错 error(socket_error) else return result end end end channel_socket.readline = wrapper_socket_function(socket.readline) local function read_response(fd) --fd就是 self.__sock local result = fd:readline "\r\n" --channel_socket.readline()此时会raise socketerror local firstchar = string.byte(result) local data = string.sub(result,2) return redcmd[firstchar](fd,data) end so:request(compose_message("AUTH", auth), read_response) --so是self 即一开始创建的channel
session mode
大致原理是相同的。当然验证过程会有不同,这个跟mogodb的验证设计有关。你在了解mogodb的基本使用后,就应该知道该怎么理解验证了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)