12-主动连接之socketChannel

新入门skynet系列视频b站网址 https://www.bilibili.com/video/BV19d4y1678X


主动连接之socketChannel

当我们打算主动跟外部通讯时,我们一般使用socketchannel。

socketchannel有两种工作模式。一种是 order ,一种是session。

我们一般使用socketchannel的过程就是两步

  1. 创建一个soketchannel对象 local channel = socketchannel.channel(conf)
  2. 利用这个对象发送请求即 local data = channel:request(request, response, padding)

image-20220916113822399

我们看看一个例子

 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

image-20220916113915491

连接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的基本使用后,就应该知道该怎么理解验证了。

posted @ 2022-12-10 20:15  程序员阿钢  阅读(1090)  评论(0编辑  收藏  举报