Luat系列教程:5、socket代码详解

LUAT系列全部教程可以点击下面链接查看(建议保存书签):

https://www.chenxublog.com/tag/luat系列教程

写在前面:
由于本人并未学习过具体原理,所以本文可能会有多处常识性错误,如有发现请留言指出,谢谢!


阅读本文需要具有的技能:
看过该系列前几篇文章或明白前几篇文章内容的
熟悉lua语法,尤其是数组部分
可以明白字符串、字节码之间的区别
可以自己实践操作
对tcp/udp通讯有基本的了解
用过这东西

各位想看mqtt解释的,请等待下一篇文章,不过也可以顺便看看这一篇嘛,二者都差不多的

Socket(TCP/UDP)

TCP和UDP除了在lua代码声明时有一些不同,其他地方完全一样,所以下面的代码将以TCP长连接的数据收发作为示例,如果需要UDP连接,只需要改声明对象时的三个字母即可

先定义一个假装能用来测试的TCP协议(需求)

  • 客户端每10秒发送一条字符串heart beat
  • 客户端接收到back开头的数据要回复相同的数据
  • 客户端收到bin回复二进制数组0x11 0x22 0x33
  • 客户端收到time要回复当前时间的时间戳字符串

示例时序如下:

代码详解

官方demo提供的示例代码

官方代码可以在github(https://github.com/openLuat/Luat_2G_RDA_8955/)的Luat_2G_RDA_8955/script_LuaTask/demo/socket/longConnection目录或luatools的LuaTools 1.x.x\script\script_LuaTask\demo\socket\longConnection目录找到

如果你能看懂官方例程,那么可以直接去使用,不需要再看本文了

socket连接代码的拆解分析

这一部分会将官方demo的代码拆开来,只保留基础部分,放到一个文件中来解释

建立文件

首先先新建两个文件,用于测试这个工程

main.lua

PROJECT = "SOCKET-TEST"
VERSION = "1.0.0"

require "log"
LOG_LEVEL = log.LOGLEVEL_TRACE

require "sys"

--每1分钟查询一次GSM信号强度,每1分钟查询一次基站信息
require "net"
net.startQueryAll(60000, 60000)

--加载硬件看门狗功能模块
require "wdt"
wdt.setup(pio.P0_30, pio.P0_31)

--加载网络指示灯功能模块
require "netLed"
netLed.setup(true,pio.P1_1)

require"longlink"

--启动系统框架
sys.init(0, 0)
sys.run()

longlink.lua

module(...,package.seeall)

require"socket"
--下面代码一会儿写

找一个测试用的服务器

2G模块socket测试和wifi有着本质的区别:没法使用内网来调试,必须要使用一个公网服务器来调试

为了解决这个问题,luat官方提供了一个tcp测试实验室网站服务:http://tcplab.openluat.com/
这个工具有一个坏处,就是三分钟没有客户端连接的话就会被强行关闭服务。我们可以在本地用一个tcp调试工具提前连上,就不会被强制关闭服务了。

为了针对这种情况,我临时写了一个工具:
https://github.com/chenxuuu/tcplab.openluat.com
编译好的文件可以点我下载

打开后可以直接获取从服务器分配的ip、端口,还能接收客户端数据、主动发送数据:

记住自己获取到的ip和端口,在下面的代码中会被使用到

建立socket线程

一般来说,socket连接都是异步运行的,何时应该发送数据,何时应该接收数据,这些逻辑应该让socket收发的进程自己进行控制

所以我们在longlink.lua中添加一个新的线程(看不懂的回去看前几篇文章),文件改成如下(注意要自己改东西!):

longlink.lua

module(...,package.seeall)

require"socket"

--测试用的服务器信息,上一部分获取到的那个
local testip,testport = "",""

--启动socket客户端任务
sys.taskInit(
function()
    while true do
    --该区域的代码会永久循环运行(除非出现语法错误)
    end
end)

进行socket连接

一般来说,我们会在模块成功获取基站分配的ip后,才会进行网络的连接操作,所以我们需要使用socket.isReady()函数来判断是否连接网络,然后再进行网络操作

在成功获取ip后,我们才能新建一个tcp对象,对其进行联网操作,socket客户端线程代码改为如下:

--启动socket客户端任务
sys.taskInit(
function()
    while true do
        --是否获取到了分配的ip(是否连上网)
        if socket.isReady() then
            --新建一个socket对象,如果是udp只需要把tcp改成udp即可
            local socketClient = socket.tcp()
            --尝试连接指定服务器
            if socketClient:connect(testip,testport) then
                --连接成功
                log.info("longlink.socketClient","connect success")
            else
                log.info("longlink.socketClient","connect fail")
                --连接失败
            end
        else
            --没连上网,原地等待一秒,一秒后会循环回去重试
            sys.wait(1000)
        end
    end
end)

对连接失败的处理

上述代码只是一个简单的连接服务器的代码,并且连上之后没有进行任何的其他操作

为了增加代码的稳健性,我们可以利用sys.waitUntil()函数,设置五分钟内没有获取到ip就开启飞行模式几秒,再关闭,让模块重新去获取GPRS连接:

--启动socket客户端任务
sys.taskInit(
function()
    while true do
        --是否获取到了分配的ip(是否连上网)
        if socket.isReady() then
            --新建一个socket对象,如果是udp只需要把tcp改成udp即可
            local socketClient = socket.tcp()
            --尝试连接指定服务器
            if socketClient:connect(testip,testport) then
                --连接成功
                log.info("longlink.socketClient","connect success")
            else
                log.info("longlink.socketClient","connect fail")
                --连接失败
            end
        else
            --没连上网
            --等待网络环境准备就绪,超时时间是5分钟
            sys.waitUntil("IP_READY_IND",300000)
            --等完了还没连上?
            if not socket.isReady() then
                --进入飞行模式,20秒之后,退出飞行模式
                net.switchFly(true)
                sys.wait(20000)
                net.switchFly(false)
            end
        end
    end
end)

同样,我们也可以给socketClient:connect(testip,testport)的连接加上错误次数的判断,连接错误超过五次,强制断开socket连接,等待五秒后重试:

--启动socket客户端任务
sys.taskInit(
function()
    local retryConnectCnt = 0   --失败次数统计
    while true do
        --是否获取到了分配的ip(是否连上网)
        if socket.isReady() then
            --新建一个socket对象,如果是udp只需要把tcp改成udp即可
            local socketClient = socket.tcp()
            --尝试连接指定服务器
            if socketClient:connect(testip,testport) then
                --连接成功
                log.info("longlink.socketClient","connect success")
                retryConnectCnt = 0 --失败次数清零
            else
                log.info("longlink.socketClient","connect fail")
                --连接失败
                retryConnectCnt = retryConnectCnt+1 --失败次数加一
            end
            socketClient:close()    --断开socket连接
            if retryConnectCnt>=5 then  --失败次数大于五次了
                link.shut()         --强制断开TCP/UDP连接
                retryConnectCnt=0   --失败次数清零
            end
            sys.wait(5000)
        else
            retryConnectCnt = 0     --没连上网,失败次数清零
            --没连上网
            --等待网络环境准备就绪,超时时间是5分钟
            sys.waitUntil("IP_READY_IND",300000)
            --等完了还没连上?
            if not socket.isReady() then
                --进入飞行模式,20秒之后,退出飞行模式
                net.switchFly(true)
                sys.wait(20000)
                net.switchFly(false)
            end
        end
    end
end)

添加发送/接收处理函数

到了这一步,整个的socket线程只剩下循环处理接收和发送的数据这一部分与demo不同了,我们直接把这两句话加到socket线程的代码中吧:

--启动socket客户端任务
sys.taskInit(
function()
    local retryConnectCnt = 0   --失败次数统计
    while true do
        --是否获取到了分配的ip(是否连上网)
        if socket.isReady() then
            --新建一个socket对象,如果是udp只需要把tcp改成udp即可
            local socketClient = socket.tcp()
            --尝试连接指定服务器
            if socketClient:connect(testip,testport) then
                --连接成功
                log.info("longlink.socketClient","connect success")
                retryConnectCnt = 0 --失败次数清零

                --循环处理接收和发送的数据
                while true do
                    if not inMsgProcess(socketClient) then  --接收消息处理函数
                        log.error("longlink.inMsgProcess error")
                        break
                    end
                    if not outMsgprocess(socketClient) then --发送消息处理函数
                        log.error("longlink.outMsgprocess error")
                        break
                    end
                end

            else
                log.info("longlink.socketClient","connect fail")
                --连接失败
                retryConnectCnt = retryConnectCnt+1 --失败次数加一
            end
            socketClient:close()    --断开socket连接
            if retryConnectCnt>=5 then  --失败次数大于五次了
                link.shut()         --强制断开TCP/UDP连接
                retryConnectCnt=0   --失败次数清零
            end
            sys.wait(5000)
        else
            retryConnectCnt = 0     --没连上网,失败次数清零
            --没连上网
            --等待网络环境准备就绪,超时时间是5分钟
            sys.waitUntil("IP_READY_IND",300000)
            --等完了还没连上?
            if not socket.isReady() then
                --进入飞行模式,20秒之后,退出飞行模式
                net.switchFly(true)
                sys.wait(20000)
                net.switchFly(false)
            end
        end
    end
end)

可以看到,在接收和发送函数不返回false的情况下,接收和发送循环会一直进行下去;只有当两个函数之一返回false时,才会触发break导致退出该接收和发送循环

inMsgProcess(socketClient)函数

这段的代码相对来说比较简单,我们可以直接使用socketClient:recv(毫秒数)来接收我们的tcp消息。
我们在合适的地方,新建一个inMsgProcess(socketClient)函数:

function inMsgProcess(socketClient)
    local result,data
    while true do
        result,data = socketClient:recv(2000)
        --接收数据
        if result then  --接收成功
            log.info("longlink.inMsgProcess",data)
            --处理data数据,现在还没代码,空着
        else    --接收失败
            break
        end
    end
    --返回结果,处理成功返回true,处理出错返回false
    return result or data=="timeout"
end

这段代码就是循环获取socket消息,如果没获取到,socketClient:recv(2000)就会返回false,"timeout";如果获取到了,就会返回true,获取到的数据字符串;如果返回了false,不为"timeout",则表示数据处理出错,说明socket连接有了什么问题

细心的读者可能看出来了,如果接收函数一直在2秒内有接收到数据,那么这段函数会永远无限循环下去,没办法到达outMsgprocess(socketClient)函数进行发送数据的操作,所以我们先去讲outMsgprocess(socketClient)函数的实现过程,再回来改进inMsgProcess(socketClient)函数

outMsgprocess(socketClient)函数

由于发送函数在socket线程中是一个循环的小部分,所以我们要建立一个消息发送的队列:有要发送的发数据时,将数据放到这个队列中;等运行到outMsgprocess(socketClient)函数时,将队列中的数据一个一个发出去

首先我们要建一个放这种队列的数组,在合适位置声明一下这个数组:

--数据发送的消息队列
local msgQuene = {}

接着我们构造一个可以往数组里插入数据的函数,table.insert()可以向数组添加数据,所以我们新建一个insertMsg函数:

local function insertMsg(data)
    table.insert(msgQuene,data)
end

还记得上面说过的消息接收函数函数会永远无限循环下去的问题吗?我们在合适的地方新建一个判断发送消息队列是否为空的函数:

function waitForSend()
    return #msgQuene > 0
end

在数组有数据时,这个函数会返回true,我们可以将inMsgProcess(socketClient)接收到数据后的代码添加一行判断发送队列是否有数据的代码,当检测到发送队列有数据时,就立即退出接收函数,转而去进行发送动作,接收函数最终改为了这样:

function inMsgProcess(socketClient)
    local result,data
    while true do
        result,data = socketClient:recv(2000)
        --接收到数据
        if result then  --接收成功
            log.info("longlink.inMsgProcess",data)
            --处理data数据,现在还没代码,空着
            --如果msgQuene中有等待发送的数据,则立即退出本循环
            if waitForSend() then return true end
        else    --接收失败
            break
        end
    end
    --返回结果,处理成功返回true,处理出错返回false
    return result or data=="timeout"
end

最后我们终于可以开始写消息发送函数了,整体的函数就是检查队列是否为空,不为空的话就发一条消息并将其从队列中删除,然后重复这一操作,函数代码如下:

function outMsgprocess(socketClient)
    --队列中有消息
    while #msgQuene>0 do
        --获取消息,并从队列中删除
        local outMsg = table.remove(msgQuene,1)
        --发送这条消息,并获取发送结果
        local result = socketClient:send(outMsg)
        --发送失败的话立刻返回nil(等同于false)
        if not result then return end
    end
    return true
end

完成基本的socket线程

经过上述的更改,最终,longlink.lua已经实现了连接服务器并自动处理错误的功能,并且预留了消息接收以及向发送队列添加数据的接口,文件的所有代码如下:

longlink.lua

module(...,package.seeall)

require"socket"

--测试用的服务器信息
local testip,testport = "180.97.81.180","50798"

--数据发送的消息队列
local msgQuene = {}

local function insertMsg(data)
    table.insert(msgQuene,data)
end

function waitForSend()
    return #msgQuene > 0
end

function outMsgprocess(socketClient)
    --队列中有消息
    while #msgQuene>0 do
        --获取消息,并从队列中删除
        local outMsg = table.remove(msgQuene,1)
        --发送这条消息,并获取发送结果
        local result = socketClient:send(outMsg)
        --发送失败的话立刻返回nil(等同于false)
        if not result then return end
    end
    return true
end

function inMsgProcess(socketClient)
    local result,data
    while true do
        result,data = socketClient:recv(2000)
        --接收到数据
        if result then  --接收成功
            log.info("longlink.inMsgProcess",data)
            --处理data数据,现在还没代码,空着
            --如果msgQuene中有等待发送的数据,则立即退出本循环
            if waitForSend() then return true end
        else    --接收失败
            break
        end
    end
    --返回结果,处理成功返回true,处理出错返回false
    return result or data=="timeout"
end



--启动socket客户端任务
sys.taskInit(
function()
    local retryConnectCnt = 0   --失败次数统计
    while true do
        --是否获取到了分配的ip(是否连上网)
        if socket.isReady() then
            --新建一个socket对象,如果是udp只需要把tcp改成udp即可
            local socketClient = socket.tcp()
            --尝试连接指定服务器
            if socketClient:connect(testip,testport) then
                --连接成功
                log.info("longlink.socketClient","connect success")
                retryConnectCnt = 0 --失败次数清零

                --循环处理接收和发送的数据
                while true do
                    if not inMsgProcess(socketClient) then  --接收消息处理函数
                        log.error("longlink.inMsgProcess error")
                        break
                    end
                    if not outMsgprocess(socketClient) then --发送消息处理函数
                        log.error("longlink.outMsgprocess error")
                        break
                    end
                end

            else
                log.info("longlink.socketClient","connect fail")
                --连接失败
                retryConnectCnt = retryConnectCnt+1 --失败次数加一
            end
            socketClient:close()    --断开socket连接
            if retryConnectCnt>=5 then  --失败次数大于五次了
                link.shut()         --强制断开TCP/UDP连接
                retryConnectCnt=0   --失败次数清零
            end
            sys.wait(5000)
        else
            retryConnectCnt = 0     --没连上网,失败次数清零
            --没连上网
            --等待网络环境准备就绪,超时时间是5分钟
            sys.waitUntil("IP_READY_IND",300000)
            --等完了还没连上?
            if not socket.isReady() then
                --进入飞行模式,20秒之后,退出飞行模式
                net.switchFly(true)
                sys.wait(20000)
                net.switchFly(false)
            end
        end
    end
end)

烧录到模块中,可以得到正常的连接结果:

实现协议需求

心跳包需求

这个需求极其简单,只需要建立一个新的线程,在联网后往消息队列中添加数据即可,代码如下:

--启动心跳包任务
sys.taskInit(
function()
    while true do
        if socket.isReady() then    --连上网再开始运行
            insertMsg("heart beat") --队列里塞个消息
            sys.wait(10000)         --等待10秒
        else    --没连上网别忘了延时!不然会陷入while true死循环,导致模块无法运行其他代码
            sys.wait(1000)          --等待1秒
        end
    end
end)

收到back开头的数据要回复相同的数据

这一条功能十分简单,只需要在inMsgProcess()函数中,写了--处理data数据,现在还没代码,空着这段注释的地方添加相应代码即可,代码如下:

--处理data数据
if data:sub(1,4) == "back" then --收到back开头的数据要回复相同的数据
    insertMsg(data)
end

客户端收到bin要回复二进制数组0x11 0x22 0x33

这个功能和上面差不多,返回的东西不一样而已,拼接字节码我们可以用pack,也可以直接用fromHex,下面两种方法都示范一下:

pack方式:

if data == "bin" then --收到bin要回复二进制数组0x11 0x22 0x33
    insertMsg(pack.pack(">bbb",0x11,0x22,0x33))
end

fromHex方式:

if data == "bin" then --收到bin要回复二进制数组0x11 0x22 0x33
    insertMsg(string.fromHex("112233"))
end

收到time要回复当前时间的时间戳字符串

进行这个操作,要在开机的时候先同步一下时间,我们可以使用demo中的同步ntp来实现。在main.lua中的启动系统框架上方添加两行同步代码即可:

require"ntp"
ntp.timeSync()

接着和上面一样,在消息接收处返回需要的数据即可:

if data == "time" then --收到time要回复当前时间的时间戳字符串
    insertMsg(tostring(os.time()))
end

完整代码

经过上面的删删改改,功能以及基本实现了,整个文件的代码如下:

longlink.lua

module(...,package.seeall)

require"socket"

--测试用的服务器信息
local testip,testport = "180.97.81.180","50798"

--数据发送的消息队列
local msgQuene = {}

local function insertMsg(data)
    table.insert(msgQuene,data)
end

function waitForSend()
    return #msgQuene > 0
end

function outMsgprocess(socketClient)
    --队列中有消息
    while #msgQuene>0 do
        --获取消息,并从队列中删除
        local outMsg = table.remove(msgQuene,1)
        --发送这条消息,并获取发送结果
        local result = socketClient:send(outMsg)
        --发送失败的话立刻返回nil(等同于false)
        if not result then return end
    end
    return true
end

function inMsgProcess(socketClient)
    local result,data
    while true do
        result,data = socketClient:recv(2000)
        --接收到数据
        if result then  --接收成功
            log.info("longlink.inMsgProcess",data)
            --处理data数据
            if data:sub(1,4) == "back" then --收到back开头的数据要回复相同的数据
                insertMsg(data)
            elseif data == "bin" then --收到bin要回复二进制数组0x11 0x22 0x33
                insertMsg(string.fromHex("112233"))
            elseif data == "time" then --收到time要回复当前时间的时间戳字符串
                insertMsg(tostring(os.time()))
            end
            --如果msgQuene中有等待发送的数据,则立即退出本循环
            if waitForSend() then return true end
        else    --接收失败
            break
        end
    end
    --返回结果,处理成功返回true,处理出错返回false
    return result or data=="timeout"
end



--启动socket客户端任务
sys.taskInit(
function()
    local retryConnectCnt = 0   --失败次数统计
    while true do
        --是否获取到了分配的ip(是否连上网)
        if socket.isReady() then
            --新建一个socket对象,如果是udp只需要把tcp改成udp即可
            local socketClient = socket.tcp()
            --尝试连接指定服务器
            if socketClient:connect(testip,testport) then
                --连接成功
                log.info("longlink.socketClient","connect success")
                retryConnectCnt = 0 --失败次数清零

                --循环处理接收和发送的数据
                while true do
                    if not inMsgProcess(socketClient) then  --接收消息处理函数
                        log.error("longlink.inMsgProcess error")
                        break
                    end
                    if not outMsgprocess(socketClient) then --发送消息处理函数
                        log.error("longlink.outMsgprocess error")
                        break
                    end
                end

            else
                log.info("longlink.socketClient","connect fail")
                --连接失败
                retryConnectCnt = retryConnectCnt+1 --失败次数加一
            end
            socketClient:close()    --断开socket连接
            if retryConnectCnt>=5 then  --失败次数大于五次了
                link.shut()         --强制断开TCP/UDP连接
                retryConnectCnt=0   --失败次数清零
            end
            sys.wait(5000)
        else
            retryConnectCnt = 0     --没连上网,失败次数清零
            --没连上网
            --等待网络环境准备就绪,超时时间是5分钟
            sys.waitUntil("IP_READY_IND",300000)
            --等完了还没连上?
            if not socket.isReady() then
                --进入飞行模式,20秒之后,退出飞行模式
                net.switchFly(true)
                sys.wait(20000)
                net.switchFly(false)
            end
        end
    end
end)

--启动心跳包任务
sys.taskInit(
function()
    while true do
        if socket.isReady() then    --连上网再开始运行
            insertMsg("heart beat") --队列里塞个消息
            sys.wait(10000)         --等待10秒
        else    --没连上网别忘了延时!不然会陷入while true死循环,导致模块无法运行其他代码
            sys.wait(1000)          --等待1秒
        end
    end
end)

main.lua

PROJECT = "SOCKET-TEST"
VERSION = "1.0.0"

require "log"
LOG_LEVEL = log.LOGLEVEL_TRACE

require "sys"

--每1分钟查询一次GSM信号强度,每1分钟查询一次基站信息
require "net"
net.startQueryAll(60000, 60000)

--加载硬件看门狗功能模块
require "wdt"
wdt.setup(pio.P0_30, pio.P0_31)

--加载网络指示灯功能模块
require "netLed"
netLed.setup(true,pio.P1_1)

require"longlink"
require"ntp"
ntp.timeSync()

--启动系统框架
sys.init(0, 0)
sys.run()

验证功能

把最终代码烧录进去,按需求测试即可:

很明显,功能符合预期。

如有错误或疑问请在下方留言,谢谢!

9 Comments

  1. Vivaldi 1.97.1259.3 Vivaldi 1.97.1259.3 Windows 10 x64 Edition Windows 10 x64 Edition

    写这篇教程从六点写到凌晨零点一刻。。。
    为了方便解释,把所有复杂的东西都去掉了。。。
    还写了个tcplab工具。。

  2. Vivaldi 1.97.1259.3 Vivaldi 1.97.1259.3 Windows 10 x64 Edition Windows 10 x64 Edition

    补充:

    demo中的数据发送处理消息队列是插入一个包含了回调函数的数组,insertMsg()像如下这样:

    local function insertMsg(data,user)
        table.insert(msgQuene,{data=data,user=user})
    end
    

    传入的user参数即为该条消息的回调函数,为了使回调函数可以再发送数据后运行,tcp发送函数也需要添加一行:

    function outMsgprocess(socketClient)
        while #msgQuene>0 do
            local outMsg = table.remove(msgQuene,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
    

    outMsg.user即为消息队列数组中的,消息数组中的,包含了回调函数的,数组(反正挺绕的)

    具体就像下面这样用:

    insertMsg("test",{cb=testcb})
    
    local function testcb()
        log.info("test.testcb","test message sent")
    end
    

    这样,该条消息发送后就会执行指定的回调函数

    1. Vivaldi 1.97.1259.3 Vivaldi 1.97.1259.3 Windows 10 x64 Edition Windows 10 x64 Edition

      掉线会导致接收/发送函数返回false,然后socket的while循环就被break,向下运行会主动切断socket连接,然后重连
      可以再看一遍代码重新理解一下

  3. Firefox 57.0 Firefox 57.0 Windows 7 x64 Edition Windows 7 x64 Edition

    -!嘿嘿老哥,不按照评论的改是能玩的。按照上边的demo改着玩算了!感谢你啊老哥

  4. Microsoft Edge 125.0.0.0 Microsoft Edge 125.0.0.0 Windows 10 x64 Edition Windows 10 x64 Edition

    任务和函数的执行我看了之前的还是没理解,就比如我想阻塞一个任务,回调另外一个任务,这怎么写

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注