合宙lua库详解-socket

简介

作为通讯的基础,只要你上网就需要用到lua的socket模块,即使使用mqtt,http等其它模块间接也调用了socket模块接口。所以对于socket的分析显得非常重要。
在分析之前请准备一些基础知识:

  1. lua的协程
  2. lua的元表

对于数据业务的使用,我们一般就会使用到:

  1. 建立连接
  2. 发送数据
  3. 接收数据
  4. 关闭连接

下面就从这几个方面进行分析:

START

可以看到这个socket的函数主要创建了一个元表,这个元表贯穿socket的全部使用过程,所以非常重要。

-- 创建socket函数
local mt = {}
mt.__index = mt
local function socket(protocol, cert)
    local ssl = protocol:match("SSL")
    local co = coroutine.running()
    --判断是否在协程中创建的socket链接,如果不是就返回nil
    --所以socket的创建必须在一个协程中创建
    if not co then
        log.warn("socket.socket: socket must be called in coroutine")
        return nil
    end
    -- 实例的属性参数表,socket对象主要一些的属性
    local o = {
        id = nil,  --socket id,用于创建socket时,底层返回的socket id
        protocol = protocol, -- 协议 “tcp”,"udp",
        ssl = ssl,
        cert = cert, --ssl连接需要用到的证书
        co = co,  --创建时的当前协程号
        input = {},  --发送表,看成发送缓存
        output = {}, --接收表,看成接收缓存
        wait = "", -- 当前等待的信号消息
        connected = false, --是否连接成功
        iSubscribe = false, --是否在订阅
        subMessage = nil, --是否在等待消息
        isBlock = false, --是否阻塞
        msg = nil,
        rcvProcFnc = nil,
    }
    return setmetatable(o, mt) --返回为元表,所以socket可以当成元表使用
end

建立连接

代码太多就不贴了,主要说下流程:
在这里插入图片描述
代码中很多接口前面都有上面这段代码,判断是否调用的协程是创建的时候的协程是否一致。不一致的话就会直接死机,这就有重要的一点:
socket接口只要有上图代码,就表示该接口必须在创建时的协程中调用,否则会死机

主要流程:

  1. 判断网络是否连接成功,没有就返回false
  2. 赋值相应参数,判断协议类型,调用socketcore.sock_conn_extsocketcore.sock_conn进行底层的socket连接,socket对象的id参数也是这该函数返回。
local socket_connect_fnc = (type(socketcore.sock_conn_ext)=="function") and socketcore.sock_conn_ext or socketcore.sock_conn
这段代码应该是底层添加了新的接口 socketcore.sock_conn_ext,为了兼容之前的版本,所以加上这段代码。
  1. 下图中,主要是进行http方式的域名查询。一般都是采用UDP进行查询。这里了解就行。
    在这里插入图片描述
  2. 挂起协程,等待连接成功
	--这里判断底层返回的socket id是否有效
    if not self.id then
        log.info("socket:connect: core sock conn error", self.protocol, address, port, self.cert)
        return false
    end
    log.info("socket:connect-coreid,prot,addr,port,cert,timeout", self.id, self.protocol, address, port, self.cert, timeout or 120)
    --将对象存在sockets表中,key为底层返回的socket_id
    sockets[self.id] = self
    --等待消息类型为 SOCKET_CONNECT
    self.wait = "SOCKET_CONNECT"
    --建立定时器,为了防止一直连接不上
    self.timerId = sys.timerStart(coroutine.resume, (timeout or 120) * 1000, self.co, false, "TIMEOUT")
    --挂起协程
    local result, reason = coroutine.yield()
    -- 获取返回值,是底层上报的,还是定时器超时,如果不是定时器超时就关闭定时器
    if self.timerId and reason ~= "TIMEOUT" then sys.timerStop(self.timerId) end
    if not result then
        log.info("socket:connect: connect fail", reason)
        --抛出连接失败的消息
        sys.publish("LIB_SOCKET_CONNECT_FAIL_IND", self.ssl, self.protocol, address, port)
        return false
    end
    log.info("socket:connect: connect ok")
   
    if not self.connected then
        self.connected = true
        socketsConnected = socketsConnected+1
        --抛出连接成功的消息
        sys.publish("SOCKET_ACTIVE", socketsConnected>0)
    end
    return true, self.id

从上可以分析出有几个重要的地方:

  • socket的维护是通过sockets表进行维护的
  • socket的连接对于lua是说是阻塞的,协程会被挂起,底层是通过消息通知上层连接结果的
  • 连接的时候有起定时器,防止一直连接不成功
    那底层是如何通知上层尼?
local function on_response(msg)
	--消息对用的字符串映射表
    local t = {
        [rtos.MSG_SOCK_CLOSE_CNF] = 'SOCKET_CLOSE',
        [rtos.MSG_SOCK_SEND_CNF] = 'SOCKET_SEND',
        [rtos.MSG_SOCK_CONN_CNF] = 'SOCKET_CONNECT',
    }
    --判断上报的socket是否存在于sockets表中,这个上面说过,lua会通过这个表维护的
    if not sockets[msg.socket_index] then
        log.warn('response on nil socket', msg.socket_index, t[msg.id], msg.result)
        return
    end
    --判断上报的消息是否正式现在等待的
    if sockets[msg.socket_index].wait ~= t[msg.id] then
        log.warn('response on invalid wait', sockets[msg.socket_index].id, sockets[msg.socket_index].wait, t[msg.id], msg.socket_index)
        return
    end
    log.info("socket:on_response:", msg.socket_index, t[msg.id], msg.result)
    if type(socketcore.sock_destroy) == "function" then
    	--如果连接失败了,或者是close关闭的响应消息就调用sock_destroy,主要是为了释放底层的socket资源
        if (msg.id == rtos.MSG_SOCK_CONN_CNF and msg.result ~= 0) or msg.id == rtos.MSG_SOCK_CLOSE_CNF then
            socketcore.sock_destroy(msg.socket_index)
        end
    end
    -- 恢复相应的挂起协程,并带上第一参数是结果,第二个参数是说明,上面为了区分是TIMEOUT还是底层响应
    -- 然后流程就到了mt:connect的 local result, reason = coroutine.yield() 可以对照代码看下
    coroutine.resume(sockets[msg.socket_index].co, msg.result == 0, "RESPONSE")
end

rtos.on(rtos.MSG_SOCK_CLOSE_CNF, on_response)
rtos.on(rtos.MSG_SOCK_CONN_CNF, on_response)
rtos.on(rtos.MSG_SOCK_SEND_CNF, on_response)

可以看到这里注册了rtos.MSG_SOCK_CONN_CNF消息,该消息就是底层处理socket时发出的。消息对应的处理函数是
on_response。函数解释详见上面注释。
关于底层消息的处理详见:合宙lua库详解-sys

发送数据

function mt:send(data, timeout)
	//判断是否是创建时的协程
    assert(self.co == coroutine.running(), "socket:send: coroutine mismatch")
    //判断socket是否已经发生了错误,如果是直接退出
    if self.error then
        log.warn('socket.client:send', 'error', self.error)
        return false
    end
    //打印发送的钱30个字节
    log.debug("socket.send", "total " .. string.len(data or "") .. " bytes", "first 30 bytes", (data or ""):sub(1, 30))
    //对发送的数据进行分片发送,单次发送数据最大值 local SENDSIZE = 11200
    for i = 1, string.len(data or ""), SENDSIZE do
        -- 按最大MTU单元对data分包
        // 等待的消息是 'SOCKET_SEND',详见上面的on_response函数,里面会判断当前socket等待的消息和
        // 底层此时上报的消息
        self.wait = "SOCKET_SEND"
        // 调用底层接口发送数据
        socketcore.sock_send(self.id, data:sub(i, i + SENDSIZE - 1))
        // 启动定时器(防止发送很长时间都没有响应)
        self.timerId = sys.timerStart(coroutine.resume, (timeout or 120) * 1000, self.co, false, "TIMEOUT")
        // 挂测线程
        local result, reason = coroutine.yield()
        // 判断恢复协程的是定时器还是底层的响应
        if self.timerId and reason ~= "TIMEOUT" then sys.timerStop(self.timerId) end
        // 如果失败
        if not result then
            log.info("socket:send", "send fail", reason)
            // LIB_SOCKET_SEND_FAIL_IND这个消息,我看errDump中有订阅,用作上报错误信息吧
            sys.publish("LIB_SOCKET_SEND_FAIL_IND", self.ssl, self.protocol, self.address, self.port)
            return false
        end
    end
    return true
end

从上面的分析可以看出lua的发送也是阻塞的,可能有人要问,那我有多个链接会不会受影响啊?
可能,取决于怎么写
为什么?
因为这是协程,如果你多个链接写在同一个协程,这个协程如果正在发送,那么他就会被挂起。这样想想其实还有点问题,多个socket写在同一个协程,如果一个正在发送,一个正好有数据上报怎么办?
接下来看下怎么接收的吧。

接收数据

lua的接收并不是直接调用底层的接口去接收。先看具体的函数实现:

--[[该函数有三个参数,根据官方说明分别为:
 @number[opt=0] timeout 可选参数,接收超时时间,单位毫秒
 @string[opt=nil] msg 可选参数,控制socket所在的线程退出recv阻塞状态
 @bool[opt=nil] msgNoResume 可选参数,控制socket所在的线程退出recv阻塞状态
]]
function mt:recv(timeout, msg, msgNoResume)
    assert(self.co == coroutine.running(), "socket:recv: coroutine mismatch")
    if self.error then
        log.warn('socket.client:recv', 'error', self.error)
        return false
    end
    --前面的已经说过,不累赘了,下面将msgNoResume赋值给socket对象的msgNoResume属性。
    self.msgNoResume = msgNoResume
    --[[如果传入了msg参数,并且iSubscribe属性为空,因为iSubscribe属性紧接这下面就进行了赋值,这说明是带消息传入的下次再进去就不需要重复操作了]]
    if msg and not self.iSubscribe then
        self.iSubscribe = msg
        --[[这里主要是注册了一个函数,这个函数的作用是当有 msg 消息被publish的时候就释放该socket挂起的协程,原因值是0xAA。]]
        self.subMessage = function(data)
            --if data then table.insert(self.output, data) end
            --[[ 这里注意,这个函数是publish设置的msg的时候才被调用,这里这是判断当前阻塞是否是
            +RECEIVE
            ]]
            if self.wait == "+RECEIVE" and not self.msgNoResume then
            --如果有数据就直接插到发送缓存里,然后恢复协程
                if data then table.insert(self.output, data) end
                coroutine.resume(self.co, 0xAA)
            end
        end
        --注册消息
        sys.subscribe(msg, self.subMessage)
    end
    --[[这里主要的作用是判断发送队列是否有数据,有数据的话就publish一条设置的msg。
	作用是什么尼? 下面会说到,recv实际上的操作是阻塞等待底层的RECV_IND的消息上报,设置了
	msg的消息,我可以在有数据的时候,恢复这个协程,这个的功能主要是上面注册的函数self.subMessage
	--]]
    if msg and #self.output > 0 then sys.publish(msg, false) end
    --判断发送队列是否为0
    if #self.input == 0 then
    	--为0的情况,socket此时等待底层的消息是 +RECEIVE
        self.wait = "+RECEIVE"
        if timeout and timeout > 0 then
        	--这里主要作用是,当有设置超时时间时,会调用sys.wait 挂起线程(详见sys库详解)
            local r, s = sys.wait(timeout)
            --判断返回原因值,nil代表超时
            if r == nil then
                return false, "timeout"
            --0xAA前面说过是代表发送缓存有数据,该协程是被上面的self.subMessage恢复 
            elseif r == 0xAA then
            	--将发送缓存的所有数据都拼接起来,然后返回数据和消息
                local dat = table.concat(self.output)
                self.output = {}
                return false, msg, dat
            else
                return r, s
            end
        else
        	--如果没有设置超时,就直接挂起协程
            local r, s = coroutine.yield()
            --和上个意义一样,不累赘
            if r == 0xAA then
                local dat = table.concat(self.output)
                self.output = {}
                return false, msg, dat
            else
                return r, s
            end
        end
    end
    --如果协议是UDP,返回接收缓存第一块数据
    if self.protocol == "UDP" then
        local s = table.remove(self.input)
        return true, s
    else
    	--这里肯定就是TCP和SSL的了,将接收缓存的数据拼接起来
        log.warn("-------------------使用缓冲区---------------")
        local s = table.concat(self.input)
        self.input = {}
        --这里是针对异步,我们这里说不到
        if self.isBlock then table.insert(self.input, socketcore.sock_recv(self.msg.socket_index, self.msg.recv_len)) end
        return true, s
    end
end

从上面的代码我们基本可以获取到:

  1. recv是阻塞的,通过挂起协程
  2. recv协程的恢复是通过publish设置的消息,或者超时时间
  3. recv的时候会将wait属性设置为 +RECEIVE,这个wait一般是针对底层的消息处理。
  4. 其它协程可以发送设置的msg,然后退出recv,从而进行其它操作。
  5. 至此我们还没有看到去取数据的地方。
    说到底层的RECEIVE,我们再看下是怎么对底层的消息进行处理的:
--这里订阅了一个底层的消息处理回调,底层消息是rtos.MSG_SOCK_RECV_IND
rtos.on(rtos.MSG_SOCK_RECV_IND, function(msg)
	--如果匹配到lua的sockets中没有有意义的socket就直接返回
    if not sockets[msg.socket_index] then
        log.warn('close ind on nil socket', msg.socket_index, msg.id)
        return
    end
    -- local s = socketcore.sock_recv(msg.socket_index, msg.recv_len)
    -- log.debug("socket.recv", "total " .. msg.recv_len .. " bytes", "first " .. 30 .. " bytes", s:sub(1, 30))
    log.debug("socket.recv", msg.recv_len, sockets[msg.socket_index].rcvProcFnc)
    --如果应用直接定义了回调处理函数,直接调用应用设置的回调
    if sockets[msg.socket_index].rcvProcFnc then
        sockets[msg.socket_index].rcvProcFnc(socketcore.sock_recv, msg.socket_index, msg.recv_len)
    else
        if sockets[msg.socket_index].wait == "+RECEIVE" then
        	--[[这时候正好等待的是+RECEIVE,恢复协程,并调用socketcore.sock_recv,
        	该函数看上下文应该直接返回的数据,回答了上面提出的第5点问题。--]]
            coroutine.resume(sockets[msg.socket_index].co, true, socketcore.sock_recv(msg.socket_index, msg.recv_len))
        else -- 数据进缓冲区,缓冲区溢出采用覆盖模式
        	--[[ 如果此时等待的并不是+RECEIVE,有可能是SEDN啊,或者其它,将数据读出来
        	然后储存到接收缓存中去,处理之前会判断缓存够不够 ]]
            if #sockets[msg.socket_index].input > INDEX_MAX then
                log.error("socket recv", "out of stack", "block")
                -- sockets[msg.socket_index].input = {}
                sockets[msg.socket_index].isBlock = true
                sockets[msg.socket_index].msg = msg
            else
                sockets[msg.socket_index].isBlock = false
                table.insert(sockets[msg.socket_index].input, socketcore.sock_recv(msg.socket_index, msg.recv_len))
            end
            sys.publish("SOCKET_RECV", msg.socket_index)
        end
    end
end)

至此我们已经将recv的逻辑说完了。为了更好的理解,我们带入demo的几段代码看下:

--[[接收处理函数]]
function proc(socketClient)
    local result,data
    while true do
    	--接收的时候传入了超时和一个"APP_SOCKET_SEND_DATA"消息
    	--按照我们前面说的,这个消息是用作其它操作打断recv阻塞用的,
    	--最有可能的就是send
        result,data = socketClient:recv(60000,"APP_SOCKET_SEND_DATA")
        --接收到数据
        if result then
            log.info("socketInMsg.proc",data)
            --TODO:根据需求自行处理data
        else
            break
        end
    end
    return result or data=="timeout" or data=="APP_SOCKET_SEND_DATA"
end

--果不其然
--send模块的操作的是将数据插入到msgQueue表中,然后发布一个APP_SOCKET_SEND_DATA消息
--这时候recv就会退出,进入发送处理函数
local function insertMsg(data,user)
    table.insert(msgQueue,{data=data,user=user})
    sys.publish("APP_SOCKET_SEND_DATA")
end
--发送处理函数
function proc(socketClient)
    while #msgQueue>0 do
    	--将msgQueue表中的数据都移除来,通过send接口发送出去
        local outMsg = table.remove(msgQueue,1)
        local result = socketClient:send(outMsg.data)
        if outMsg.user and outMsg.user.cb then outMsg.user.cb(result,outMsg.user.para) end
        if not result then return end
    end
    return true
end

一切逻辑都捋通了,有什么问题留言。下面看关闭操作:

关闭连接

关闭分为主动关闭和被动关闭,主动关闭就是客户端主动调用close接口去关闭,被动关闭就是服务器主动关闭客户端。

主动关闭

function mt:close()
    assert(self.co == coroutine.running(), "socket:close: coroutine mismatch")
    if self.iSubscribe then
        sys.unsubscribe(self.iSubscribe, self.subMessage)
        self.iSubscribe = false
    end
    --此处不要再判断状态,否则在连接超时失败时,conneted状态仍然是未连接,会导致无法close
    --if self.connected then
    log.info("socket:sock_close", self.id)
    local result, reason
    --主动关闭其实没啥好看的,就是调用socketcore.sock_close接口主动关闭,等到
    --底层的SOCKET_CLOSE消息,消息处理部分就是前面说的on_response函数,当收到底层
    --的消息时会和self.wait比对,如果一致,先去调用socketcore.sock_destroy销毁socket
    --然后恢复协程返回RESPONSE
    if self.id then
        socketcore.sock_close(self.id)
        self.wait = "SOCKET_CLOSE"
        while true do
            result, reason = coroutine.yield()
            if reason == "RESPONSE" then break end
        end
    end
    --下面就是将对象从sockets表中删掉
    if self.connected then
        self.connected = false
        if socketsConnected>0 then
            socketsConnected = socketsConnected-1
        end
        sys.publish("SOCKET_ACTIVE", socketsConnected>0)
    end
    if self.input then
        self.input = {}
    end
    --end
    if self.id ~= nil then
        sockets[self.id] = nil
    end
end

被动关闭

--主动关闭的消息是rtos.MSG_SOCK_CLOSE_IND
rtos.on(rtos.MSG_SOCK_CLOSE_IND, function(msg)
    log.info("socket.rtos.MSG_SOCK_CLOSE_IND")
    if not sockets[msg.socket_index] then
        log.warn('close ind on nil socket', msg.socket_index, msg.id)
        return
    end
    if sockets[msg.socket_index].connected then
        sockets[msg.socket_index].connected = false
        if socketsConnected>0 then
            socketsConnected = socketsConnected-1
        end
        sys.publish("SOCKET_ACTIVE", socketsConnected>0)
    end
    sockets[msg.socket_index].error = 'CLOSED'
    -- 主要操作是将对象的error置为CLOSED	
    --[[
    if type(socketcore.sock_destroy) == "function" then
        socketcore.sock_destroy(msg.socket_index)
    end]]
    --发布一个LIB_SOCKET_CLOSE_IND的内部消息
    sys.publish("LIB_SOCKET_CLOSE_IND", sockets[msg.socket_index].ssl, sockets[msg.socket_index].protocol, sockets[msg.socket_index].address, sockets[msg.socket_index].port)
    --恢复当前挂起的对象协程,给的参数是CLOSED,这里主要是第一个参数false,因为不管是
    --send还是recv的时候协程被恢复时,都会直接将这个结果返回给应用,应用再做相应的处理
    coroutine.resume(sockets[msg.socket_index].co, false, "CLOSED")
end)

至此socket模块已经说完,肝了好久,有什么地方有问题,麻烦留言指出

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值