跳转至

12 Socket长连接

作者:王城钧

一、Socket 长连接概述

Socket 长连接是一种通过模组串口实现与不同 Socket(TCP/UDP/TCP SSL)服务器的长连接进行数据交互的通信机制,支持自动重连、串口/定时器数据透传、看门狗网络检测及多网卡(4G/WiFi/以太网)优先级配置,可以确保稳定通信。

本文使用的 demo 所演示功能如下:

(1)创建四路 socket 连接

  • 创建一个 tcp client,连接 tcp server;
  • 创建一个 udp client,连接 udp server;
  • 创建一个 tcp ssl client,连接 tcp ssl server,不做证书校验;
  • 创建一个 tcp ssl client,连接 tcp ssl server,client 仅单向校验 server 的证书,server 不校验 client 的证书和密钥文件;

(2)每一路 socket 连接异常后,自动重连;

(3)每一路 socket 连接,client 按照以下几种逻辑发送数据给 server;

  • 通过 uart1 接收到串口数据,将串口数据增加 send from uart: 前缀后发送给 server;
  • 定时器定时产生数据,将数据增加 send from timer:前缀后发送给 server;

(4)通过串口发送每一路收到的数据;

(5)启动看门狗,检测网络环境;

(6)配置连接外网使用的网卡(四选一)

  • 4G 网卡
  • WIFI STA 网卡
  • 通过 SPI 外挂 CH390H 芯片的以太网卡
  • 支持以上三种网卡,可以配置三种网卡的优先级

补充

1>TCP 概述

TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。它主要用于在不可靠的网络环境中提供稳定的数据传输服务,确保数据能够按照顺序、无错误地到达接收端。TCP 通过三次握手建立连接,使用滑动窗口进行流量控制,以及通过校验和、确认应答、超时重传等机制来保证数据的可靠性。它是互联网协议套件(TCP/IP 协议组)的核心组成部分,广泛应用于各种网络应用中。

工作原理:

(1) 连接建立:TCP 协议使用三次握手协议来建立连接。

  • 客户端发送一个 SYN(同步序列编号)报文给服务端,并携带一个随机生成的初始序列号。
  • 服务端收到 SYN 报文后,发送一个 SYN+ACK(同步序列编号 + 确认应答)报文给客户端,表示确认收到了客户端的 SYN 报文,并携带自己的初始序列号。
  • 客户端收到服务端的 SYN+ACK 报文后,发送一个 ACK(确认应答)报文给服务端,表示确认收到了服务端的 SYN+ACK 报文。至此,TCP 连接建立完成。

(2) 数据传输:

在连接建立后,双方就可以开始传输数据了。TCP 协议会将应用层发送的数据分割成适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传输单元 MTU 的限制),并为每个报文段分配一个序号。接收端在收到报文段后,会按照序号进行排序,并发送确认应答(ACK)给发送端。如果发送端在合理的往返时延(RTT)内未收到确认应答,则会重传对应的报文段。

(3) 连接释放:TCP 协议使用四次挥手协议来终止连接。

  • 客户端发送一个 FIN(结束)报文给服务端,表示自己想要关闭连接。
  • 服务端收到 FIN 报文后,发送一个 ACK 报文给客户端,表示确认收到了客户端的 FIN 报文。此时,客户端到服务端的连接关闭,但服务端到客户端的连接仍然打开。
  • 服务端在发送完所有剩余数据后,也发送一个 FIN 报文给客户端,表示自己也想要关闭连接。
  • 客户端收到服务端的 FIN 报文后,发送一个 ACK 报文给服务端,表示确认收到了服务端的 FIN 报文。至此,TCP 连接完全关闭。

2>UDP 概述

UDP(用户数据报协议,User Datagram Protocol)是一种无连接的、不可靠的传输层协议,主要用于实现网络中的快速通讯。以下是 UDP 通讯的主要特点:

(1) 无连接通讯:

UDP 在发送数据之前不需要建立连接,这大大减少了通讯的延迟。发送方只需将数据包封装成 UDP 报文,并附上目的地址和端口号,即可直接发送。

(2) 不可靠传输:

UDP 不保证数据包的顺序性、完整性和可靠性。数据包在传输过程中可能会丢失、重复或乱序到达。因此,UDP 通讯需要应用层自行处理这些问题,如实现错误检测、数据重传等机制。

(3) 面向报文:

UDP 以报文为单位进行数据传输,每个报文都是独立的。这种面向报文的特性使得 UDP 能够保持数据的完整性,并且便于进行错误检测和处理。

(4) 高效性:

UDP 的头部结构非常简单,只包含必要的字段,如源端口、目的端口、数据长度和校验和。这种简洁的头部设计使得 UDP 在处理数据包时更加高效,减少了网络延迟。

(5) 实时性:

UDP 通讯具有较快的传输速度,适用于对实时性要求较高的应用场景,如视频通话、在线游戏等。在这些场景中,即使数据包偶尔丢失或延迟,也不会对整体功能产生严重影响。

二、准备硬件环境

Air8000 开发板一块 + 可上网的 sim 卡一张 +4g 天线一根 +wifi 天线一根 + 网线一根:

  • sim 卡插入开发板的 sim 卡槽
  • 天线装到开发板上
  • 网线一端插入开发板网口,另外一端连接可以上外网的路由器网口

TYPE-C USB 数据线一根 + USB 转串口数据线一根,Air8000 开发板和数据线的硬件接线方式为:

  • Air8000 开发板通过 TYPE-C USB 口供电;(外部供电/USB 供电 拨动开关 拨到 USB 供电一端)
  • TYPE-C USB 数据线直接插到核心板的 TYPE-C USB 座子,另外一端连接电脑 USB 口;
  • USB 转串口数据线,一般来说,白线连接开发板的 UART1_TX,绿线连接开发板的 UART1_RX,黑线连接核心板的 GND,另外一端连接电脑 USB 口;

三、软件环境

3.1、合宙模组相关

在开始实践本示例之前,先筹备一下软件环境:

1.Luatools 工具

2.Air8000 V2011 版本固件(理论上,2025 年 7 月 26 日之后发布的固件都可以)

3.luatos 需要的脚本和资源文件

脚本和资源文件:点我,查看 demo 链接

lib 脚本文件:使用 Luatools 烧录时,勾选 添加默认 lib 选项,使用默认 lib 脚本文件;

3.2、合宙 TCP/UDP web 测试工具

为了方便测试,合宙提供了免费的不可商用的 TCP/UDP web 测试工具:合宙 TCP/UDP web 工具 (luatos.com)

详细使用说明参考:合宙 TCP/UDP web 测试工具使用说明

3.3、PC 端串口工具

SSCOM 的下载链接:SSCOM ,详细使用说明可以直接参考下载网站。

Air8000 支持 4 个串口,分别是主串口 UART1(MAIN_UART), UART2(AUX_UART)和 UART3, 调试串口 UART0(DBG_UART)。本次演示使用的是主串口 UART1。

串口工具中的更多设置需要设置数据位 8,停止位 1,无奇偶校验位。

四、socket 长连接的实现

本小节教你怎么使用 LuatOS 脚本语言,就可以让 Air8000 模组连接上 TCP,UDP,TCP_SSL,TCP_SSL_CA 并且模组和服务器之间实现数据的交互!

4.1、功能总体设计框图

4.2、Socket 长连接相关 API:

sys 库:https://docs.openluat.com/osapi/core/sys/

libnet 库:https://docs.openluat.com/osapi/ext/libnet/

socket 库:https://docs.openluat.com/osapi/core/socket/

4.3、核心脚本代码详解

4.3.1 TCP 服务器数据的收发

tcp_client_main.lua

本文件为 tcp client socket 主应用功能模块,核心业务逻辑为:

1、创建一个 tcp client socket,连接 server;

2、处理连接异常,出现异常后执行重连动作;

3、调用 tcp_client_receiver 和 tcp_client_sender 中的外部接口,进行数据收发处理;

local libnet = require "libnet"

-- 加载tcp client socket数据接收功能模块
local tcp_client_receiver = require "tcp_client_receiver"
-- 加载tcp client socket数据发送功能模块
local tcp_client_sender = require "tcp_client_sender"

-- 电脑访问:https://netlab.luatos.com/
-- 点击 打开TCP 按钮,会创建一个TCP server
-- 将server的地址和端口赋值给下面这两个变量
local SERVER_ADDR = "112.125.89.8"
local SERVER_PORT = 42610

-- tcp_client_main的任务名
local TASK_NAME = tcp_client_sender.TASK_NAME


-- 处理未识别的消息
local function tcp_client_main_cbfunc(msg)
        log.info("tcp_client_main_cbfunc", msg[1], msg[2], msg[3], msg[4])
end

-- tcp client socket的任务处理函数
local function tcp_client_main_task_func() 

    local socket_client
    local result, para1, para2

    while true do
        -- 如果当前时间点设置的网卡还没有连接成功,一直在这里循环等待
        while not socket.adapter(socket.dft()) do
            log.warn("tcp_client_main_task_func", "wait IP_READY", socket.dft())
            -- 在此处阻塞等待网卡连接成功的消息"IP_READY"
            -- 或者等待1秒超时退出阻塞等待状态;
            -- 注意:此处的1000毫秒超时不要修改的更长;
            -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改当前使用的网卡
            -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
            -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
            sys.waitUntil("IP_READY", 1000)
        end

        -- 检测到了IP_READY消息
        log.info("tcp_client_main_task_func", "recv IP_READY", socket.dft())

        -- 创建socket client对象
        socket_client = socket.create(nil, TASK_NAME)
        -- 如果创建socket client对象失败
        if not socket_client then
            log.error("tcp_client_main_task_func", "socket.create error")
            goto EXCEPTION_PROC
        end

        -- 配置socket client对象为tcp client
        result = socket.config(socket_client)
        -- 如果配置失败
        if not result then
            log.error("tcp_client_main_task_func", "socket.config error")
            goto EXCEPTION_PROC
        end

        -- 连接server
        result = libnet.connect(TASK_NAME, 15000, socket_client, SERVER_ADDR, SERVER_PORT)
        -- 如果连接server失败
        if not result then
            log.error("tcp_client_main_task_func", "libnet.connect error")
            goto EXCEPTION_PROC
        end

        log.info("tcp_client_main_task_func", "libnet.connect success")

        -- 数据收发以及网络连接异常事件总处理逻辑
        while true do
            -- 数据接收处理(接收处理必须写在libnet.wait之前,因为老版本的内核固件要求必须这样,新版本的内核固件没这个要求,为了不出问题,写在libnet.wait之前就行了)
            -- 如果处理失败,则退出循环
            if not tcp_client_receiver.proc(socket_client) then
                log.error("tcp_client_main_task_func", "tcp_client_receiver.proc error")
                break
            end

            -- 数据发送处理
            -- 如果处理失败,则退出循环
            if not tcp_client_sender.proc(TASK_NAME, socket_client) then
                log.error("tcp_client_main_task_func", "tcp_client_sender.proc error")
                break
            end

            -- 阻塞等待socket.EVENT事件或者15秒钟超时
            -- 以下三种业务逻辑会发布事件:
            -- 1、socket client和server之间的连接出现异常(例如server主动断开,网络环境出现异常等),此时在内核固件中会发布事件socket.EVENT
            -- 2、socket client接收到server发送过来的数据,此时在内核固件中会发布事件socket.EVENT
            -- 3、socket client需要发送数据到server, 在tcp_client_sender.lua中会发布事件socket.EVENT
                        result, para1, para2 = libnet.wait(TASK_NAME, 15000, socket_client)
            log.info("tcp_client_main_task_func", "libnet.wait", result, para1, para2)

                        -- 如果连接异常,则退出循环
                        if not result then
                                log.warn("tcp_client_main_task_func", "connection exception")
                                break
            end
        end


        -- 出现异常    
        ::EXCEPTION_PROC::

        -- 数据发送应用模块对来不及发送的数据做清空和通知失败处理
        tcp_client_sender.exception_proc()

        -- 如果存在socket client对象
        if socket_client then
            -- 关闭socket client连接
            libnet.close(TASK_NAME, 5000, socket_client)

            -- 释放socket client对象
            socket.release(socket_client)
            socket_client = nil
        end

        -- 5秒后跳转到循环体开始位置,自动发起重连
        sys.wait(5000)
    end
end

--创建并且启动一个task
--运行这个task的主函数tcp_client_main_task_func
sysplus.taskInitEx(tcp_client_main_task_func, TASK_NAME, tcp_client_main_cbfunc)
tcp_client_receiver.lua

本文件为 tcp client socket 数据接收应用功能模块,核心业务逻辑为:

从内核读取接收到的数据,然后将数据发送给其他应用功能模块做进一步处理;

local tcp_client_receiver = {}

-- socket数据接收缓冲区
local recv_buff = nil

--[[
检查socket client是否收到数据,如果收到数据,读取并且处理完所有数据

@api tcp_client_receiver.proc(socket_client)

@param1 socket_client userdata
表示由socket.create接口创建的socket client对象
必须传入,不允许为空或者nil

@return1 result bool
表示处理结果,成功为true,失败为false

@usage
-- 
tcp_client_receiver.proc(socket_client)
]]
function tcp_client_receiver.proc(socket_client)
    -- 如果socket数据接收缓冲区还没有申请过空间,则先申请内存空间
    if recv_buff==nil then
        recv_buff = zbuff.create(1024)
        -- 当recv_buff不再使用时,不需要主动调用recv_buff:free()去释放
        -- 因为Lua的垃圾处理器会自动释放recv_buff所申请的内存空间
        -- 如果等不及垃圾处理器自动处理,在确定以后不会再使用recv_buff时,则可以主动调用recv_buff:free()释放内存空间
    end

    -- 循环从内核的缓冲区读取接收到的数据
    -- 如果读取失败,返回false,退出
    -- 如果读取成功,处理数据,并且继续循环读取
    -- 如果读取成功,并且读出来的数据为空,表示已经没有数据可读,返回true,退出
    while true do
        -- 从内核的缓冲区中读取数据到recv_buff中
        -- 如果recv_buff的存储空间不足,会自动扩容
        local result = socket.rx(socket_client, recv_buff)

        -- 读取数据失败
        -- 有两种情况:
        -- 1、recv_buff扩容失败
        -- 2、socket client和server之间的连接断开
        if not result then
            log.error("tcp_client_receiver.proc", "socket.rx error")
            return false
        end

        -- 如果读取到了数据, used()就必然大于0, 进行处理
        if recv_buff:used() > 0 then
            log.info("tcp_client_receiver.proc", "recv data len", recv_buff:used())

            -- 读取socket数据接收缓冲区中的数据,赋值给data
            local data = recv_buff:query()

            -- 将数据data通过"RECV_DATA_FROM_SERVER"消息publish出去,给其他应用模块处理
            sys.publish("RECV_DATA_FROM_SERVER", "recv from tcp server: ", data)

            -- 接收到数据,通知网络环境检测看门狗功能模块进行喂狗
            sys.publish("FEED_NETWORK_WATCHDOG")

            -- 清空socket数据接收缓冲区中的数据
            recv_buff:del()
            -- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
        else
            break
        end
    end

    return true
end

return tcp_client_receiver
tcp_client_sender.lua

本文件为 tcp client socket 数据发送应用功能模块,核心业务逻辑为:

1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)订阅"SEND_DATA_REQ"消息,将其他应用模块需要发送的数据存储到队列 send_queue 中;

2、tcp_client_main 主任务调用 tcp_client_sender.proc 接口,遍历队列 send_queue,逐条发送数据到 server;

3、tcp client socket 和 server 之间的连接如果出现异常,tcp_client_main 主任务调用 tcp_client_sender.exception_proc 接口,丢弃掉队列 send_queue 中未发送的数据;

4、任何一条数据无论发送成功还是失败,只要这条数据有回调函数,都会通过回调函数通知数据发送方;

local tcp_client_sender = {}

local libnet = require "libnet"

--[[
数据发送队列,数据结构为:
{
    [1] = {data="data1", cb={func=callback_function1, para=callback_para1}},
    [2] = {data="data2", cb={func=callback_function2, para=callback_para2}},
}
data的内容为真正要发送的数据,必须存在;
func的内容为数据发送结果的用户回调函数,可以不存在
para的内容为数据发送结果的用户回调函数的回调参数,可以不存在;
]]
local send_queue = {}

-- tcp_client_main的任务名
tcp_client_sender.TASK_NAME = "tcp_client_main"

-- "SEND_DATA_REQ"消息的处理函数
local function send_data_req_proc_func(tag, data, cb)
    -- 将原始数据增加前缀,然后插入到发送队列send_queue中
    table.insert(send_queue, {data="send from "..tag..": "..data, cb=cb})
    -- 通知tcp_client_main主任务有数据需要发送
    -- tcp_client_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
    sysplus.sendMsg(tcp_client_sender.TASK_NAME, socket.EVENT, 0)
end

--[[
检查socket client是否需要发送数据,如果需要发送数据,读取并且发送完发送队列中的所有数据

@api tcp_client_sender.proc(task_name, socket_client)

@param1 task_name string
表示socket.create接口创建socket client对象时所处的task的name
必须传入,不允许为空或者nil

@param2 socket_client userdata
表示由socket.create接口创建的socket client对象
必须传入,不允许为空或者nil

@return1 result bool
表示处理结果,成功为true,失败为false

@usage
tcp_client_sender.proc("tcp_client_main", socket_client)
]]
function tcp_client_sender.proc(task_name, socket_client)
    local send_item
    local result, buff_full

    -- 遍历数据发送队列send_queue
    while #send_queue>0 do
        -- 取出来第一条数据赋值给send_item
        -- 同时从队列send_queue中删除这一条数据
        send_item = table.remove(send_queue,1)

        -- 发送这条数据,超时时间15秒钟
        result, buff_full = libnet.tx(task_name, 15000, socket_client, send_item.data)

        -- 发送失败
        if not result then
            log.error("tcp_client_sender.proc", "libnet.tx error")

            -- 如果当前发送的数据有用户回调函数,则执行用户回调函数
            if send_item.cb and send_item.cb.func then
                send_item.cb.func(false, send_item.cb.para)
            end

            return false
        end

        -- 如果内核固件中缓冲区满了,则将send_item再次插入到send_queue的队首位置,等待下次尝试发送
        if buff_full then
            log.error("tcp_client_sender.proc", "buffer is full, wait for the next time")
            table.insert(send_queue, 1, send_item)
            return true
        end

        log.info("tcp_client_sender.proc", "send success")
        -- 发送成功,如果当前发送的数据有用户回调函数,则执行用户回调函数
        if send_item.cb and send_item.cb.func then
            send_item.cb.func(true, send_item.cb.para)
        end

        -- 发送成功,通知网络环境检测看门狗功能模块进行喂狗
        sys.publish("FEED_NETWORK_WATCHDOG")
    end

    return true
end

--[[
socket client连接出现异常时,清空等待发送的数据,并且执行发送方的回调函数

@api tcp_client_sender.exception_proc()

@usage
tcp_client_sender.exception_proc()
]]
function tcp_client_sender.exception_proc()
    -- 遍历数据发送队列send_queue
    while #send_queue>0 do
        local send_item = table.remove(send_queue,1)
        -- 发送失败,如果当前发送的数据有用户回调函数,则执行用户回调函数
        if send_item.cb and send_item.cb.func then
            send_item.cb.func(false, send_item.cb.para)
        end
    end
end

-- 订阅"SEND_DATA_REQ"消息;
-- 其他应用模块如果需要发送数据,直接sys.publish这个消息即可,将需要发送的数据以及回调函数和毁掉参数一起publish出去;
-- 本demo项目中uart_app.lua和timer_app.lua中publish了这个消息;
sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)

return tcp_client_sender

4.3.2 UDP 服务器数据的收发

udp_client_main.lua

本文件为 udp client socket 主应用功能模块,核心业务逻辑为:

1、创建一个 udp client socket,连接 server;

2、处理连接异常,出现异常后执行重连动作;

3、调用 udp_client_receiver 和 udp_client_sender 中的外部接口,进行数据收发处理;

local libnet = require "libnet"

-- 加载udp client socket数据接收功能模块
local udp_client_receiver = require "udp_client_receiver"
-- 加载udp client socket数据发送功能模块
local udp_client_sender = require "udp_client_sender"

-- 电脑访问:https://netlab.luatos.com/
-- 点击 打开UDP 按钮,会创建一个UDP server
-- 将server的地址和端口赋值给下面这两个变量
local SERVER_ADDR = "112.125.89.8"
local SERVER_PORT = 46819

-- udp_client_main的任务名
local TASK_NAME = udp_client_sender.TASK_NAME


-- 处理未识别的消息
local function udp_client_main_cbfunc(msg)
        log.info("udp_client_main_cbfunc", msg[1], msg[2], msg[3], msg[4])
end

-- udp client socket的任务处理函数
local function udp_client_main_task_func() 

    local socket_client
    local result, para1, para2

    while true do
        -- 如果当前时间点设置的网卡还没有连接成功,一直在这里循环等待
        while not socket.adapter(socket.dft()) do
            log.warn("udp_client_main_task_func", "wait IP_READY", socket.dft())
            -- 在此处阻塞等待网卡连接成功的消息"IP_READY"
            -- 或者等待1秒超时退出阻塞等待状态;
            -- 注意:此处的1000毫秒超时不要修改的更长;
            -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改当前使用的网卡
            -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
            -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
            sys.waitUntil("IP_READY", 1000)
        end

        -- 检测到了IP_READY消息
        log.info("udp_client_main_task_func", "recv IP_READY", socket.dft())

        -- 创建socket client对象
        socket_client = socket.create(nil, TASK_NAME)
        -- 如果创建socket client对象失败
        if not socket_client then
            log.error("udp_client_main_task_func", "socket.create error")
            goto EXCEPTION_PROC
        end

        -- 配置socket client对象为udp client
        result = socket.config(socket_client, nil, true)
        -- 如果配置失败
        if not result then
            log.error("udp_client_main_task_func", "socket.config error")
            goto EXCEPTION_PROC
        end

        -- 连接server
        result = libnet.connect(TASK_NAME, 15000, socket_client, SERVER_ADDR, SERVER_PORT)
        -- 如果连接server失败
        if not result then
            log.error("udp_client_main_task_func", "libnet.connect error")
            goto EXCEPTION_PROC
        end

        log.info("udp_client_main_task_func", "libnet.connect success")

        -- 数据收发以及网络连接异常事件总处理逻辑
        while true do
            -- 数据接收处理(接收处理必须写在libnet.wait之前,因为老版本的内核固件要求必须这样,新版本的内核固件没这个要求,为了不出问题,写在libnet.wait之前就行了)
            -- 如果处理失败,则退出循环
            if not udp_client_receiver.proc(socket_client) then
                log.error("udp_client_main_task_func", "udp_client_receiver.proc error")
                break
            end

            -- 数据发送处理
            -- 如果处理失败,则退出循环
            if not udp_client_sender.proc(TASK_NAME, socket_client) then
                log.error("udp_client_main_task_func", "udp_client_sender.proc error")
                break
            end

            -- 阻塞等待socket.EVENT事件或者15秒钟超时
            -- 以下三种业务逻辑会发布事件:
            -- 1、socket client和server之间的连接出现异常(例如server主动断开,网络环境出现异常等),此时在内核固件中会发布事件socket.EVENT
            -- 2、socket client接收到server发送过来的数据,此时在内核固件中会发布事件socket.EVENT
            -- 3、socket client需要发送数据到server, 在udp_client_sender.lua中会发布事件socket.EVENT
                        result, para1, para2 = libnet.wait(TASK_NAME, 15000, socket_client)
            log.info("udp_client_main_task_func", "libnet.wait", result, para1, para2)

                        -- 如果连接异常,则退出循环
                        if not result then
                                log.warn("udp_client_main_task_func", "connection exception")
                                break
            end
        end


        -- 出现异常    
        ::EXCEPTION_PROC::

        -- 数据发送应用模块对来不及发送的数据做清空和通知失败处理
        udp_client_sender.exception_proc()

        -- 如果存在socket client对象
        if socket_client then
            -- 关闭socket client连接
            libnet.close(TASK_NAME, 5000, socket_client)

            -- 释放socket client对象
            socket.release(socket_client)
            socket_client = nil
        end

        -- 5秒后跳转到循环体开始位置,自动发起重连
        sys.wait(5000)
    end
end

--创建并且启动一个task
--运行这个task的主函数udp_client_main_task_func
sysplus.taskInitEx(udp_client_main_task_func, TASK_NAME, udp_client_main_cbfunc)
udp_client_receiver.lua

本文件为 udp client socket 数据接收应用功能模块,核心业务逻辑为:

从内核读取接收到的数据,然后将数据发送给其他应用功能模块做进一步处理;

local udp_client_receiver = {}

-- socket数据接收缓冲区
local recv_buff = nil

--[[
检查socket client是否收到数据,如果收到数据,读取并且处理完所有数据

@api udp_client_receiver.proc(socket_client)

@param1 socket_client userdata
表示由socket.create接口创建的socket client对象
必须传入,不允许为空或者nil

@return1 result bool
表示处理结果,成功为true,失败为false

@usage
-- 
udp_client_receiver.proc(socket_client)
]]
function udp_client_receiver.proc(socket_client)
    -- 如果socket数据接收缓冲区还没有申请过空间,则先申请内存空间
    if recv_buff==nil then
        recv_buff = zbuff.create(1024)
        -- 当recv_buff不再使用时,不需要主动调用recv_buff:free()去释放
        -- 因为Lua的垃圾处理器会自动释放recv_buff所申请的内存空间
        -- 如果等不及垃圾处理器自动处理,在确定以后不会再使用recv_buff时,则可以主动调用recv_buff:free()释放内存空间
    end

    -- 循环从内核的缓冲区读取接收到的数据
    -- 如果读取失败,返回false,退出
    -- 如果读取成功,处理数据,并且继续循环读取
    -- 如果读取成功,并且读出来的数据为空,表示已经没有数据可读,返回true,退出
    while true do
        -- 从内核的缓冲区中读取数据到recv_buff中
        -- 如果recv_buff的存储空间不足,会自动扩容
        local result = socket.rx(socket_client, recv_buff)

        -- 读取数据失败
        -- 有两种情况:
        -- 1、recv_buff扩容失败
        -- 2、socket client和server之间的连接断开
        if not result then
            log.error("udp_client_receiver.proc", "socket.rx error")
            return false
        end

        -- 如果读取到了数据, used()就必然大于0, 进行处理
        if recv_buff:used() > 0 then
            log.info("udp_client_receiver.proc", "recv data len", recv_buff:used())

            -- 读取socket数据接收缓冲区中的数据,赋值给data
            local data = recv_buff:query()

            -- 将数据data通过"RECV_DATA_FROM_SERVER"消息publish出去,给其他应用模块处理
            sys.publish("RECV_DATA_FROM_SERVER", "recv from udp server: ", data)

            -- 接收到数据,通知网络环境检测看门狗功能模块进行喂狗
            sys.publish("FEED_NETWORK_WATCHDOG")

            -- 清空socket数据接收缓冲区中的数据
            recv_buff:del()
            -- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
        else
            break
        end
    end

    return true
end

return udp_client_receiver
udp_client_sender.lua

本文件为 udp client socket 数据发送应用功能模块,核心业务逻辑为:

1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)订阅"SEND_DATA_REQ"消息,将其他应用模块需要发送的数据存储到队列 send_queue 中;

2、udp_client_main 主任务调用 udp_client_sender.proc 接口,遍历队列 send_queue,逐条发送数据到 server;

3、udp client socket 和 server 之间的连接如果出现异常,udp_client_main 主任务调用 udp_client_sender.exception_proc 接口,丢弃掉队列 send_queue 中未发送的数据;

4、任何一条数据无论发送成功还是失败,只要这条数据有回调函数,都会通过回调函数通知数据发送方;

local udp_client_sender = {}

local libnet = require "libnet"

--[[
数据发送队列,数据结构为:
{
    [1] = {data="data1", cb={func=callback_function1, para=callback_para1}},
    [2] = {data="data2", cb={func=callback_function2, para=callback_para2}},
}
data的内容为真正要发送的数据,必须存在;
func的内容为数据发送结果的用户回调函数,可以不存在
para的内容为数据发送结果的用户回调函数的回调参数,可以不存在;
]]
local send_queue = {}

-- udp_client_main的任务名
udp_client_sender.TASK_NAME = "udp_client_main"

-- "SEND_DATA_REQ"消息的处理函数
local function send_data_req_proc_func(tag, data, cb)
    -- 将原始数据增加前缀,然后插入到发送队列send_queue中
    table.insert(send_queue, {data="send from "..tag..": "..data, cb=cb})
    -- 通知udp_client_main主任务有数据需要发送
    -- udp_client_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
    sysplus.sendMsg(udp_client_sender.TASK_NAME, socket.EVENT, 0)
end

--[[
检查socket client是否需要发送数据,如果需要发送数据,读取并且发送完发送队列中的所有数据

@api udp_client_sender.proc(task_name, socket_client)

@param1 task_name string
表示socket.create接口创建socket client对象时所处的task的name
必须传入,不允许为空或者nil

@param2 socket_client userdata
表示由socket.create接口创建的socket client对象
必须传入,不允许为空或者nil

@return1 result bool
表示处理结果,成功为true,失败为false

@usage
udp_client_sender.proc("tcp_client_main", socket_client)
]]
function udp_client_sender.proc(task_name, socket_client)
    local send_item
    local result, buff_full

    -- 遍历数据发送队列send_queue
    while #send_queue>0 do
        -- 取出来第一条数据赋值给send_item
        -- 同时从队列send_queue中删除这一条数据
        send_item = table.remove(send_queue,1)

        -- 发送这条数据,超时时间15秒钟
        result, buff_full = libnet.tx(task_name, 15000, socket_client, send_item.data)

        -- 发送失败
        if not result then
            log.error("udp_client_sender.proc", "libnet.tx error")

            -- 如果当前发送的数据有用户回调函数,则执行用户回调函数
            if send_item.cb and send_item.cb.func then
                send_item.cb.func(false, send_item.cb.para)
            end

            return false
        end

        -- 如果内核固件中缓冲区满了,则将send_item再次插入到send_queue的队首位置,等待下次尝试发送
        if buff_full then
            log.error("udp_client_sender.proc", "buffer is full, wait for the next time")
            table.insert(send_queue, 1, send_item)
            return true
        end

        log.info("udp_client_sender.proc", "send success")
        -- 发送成功,如果当前发送的数据有用户回调函数,则执行用户回调函数
        if send_item.cb and send_item.cb.func then
            send_item.cb.func(true, send_item.cb.para)
        end
    end

    return true
end

--[[
socket client连接出现异常时,清空等待发送的数据,并且执行发送方的回调函数

@api udp_client_sender.exception_proc()

@usage
udp_client_sender.exception_proc()
]]
function udp_client_sender.exception_proc()
    -- 遍历数据发送队列send_queue
    while #send_queue>0 do
        local send_item = table.remove(send_queue,1)
        -- 发送失败,如果当前发送的数据有用户回调函数,则执行用户回调函数
        if send_item.cb and send_item.cb.func then
            send_item.cb.func(false, send_item.cb.para)
        end
    end
end

-- 订阅"SEND_DATA_REQ"消息;
-- 其他应用模块如果需要发送数据,直接sys.publish这个消息即可,将需要发送的数据以及回调函数和毁掉参数一起publish出去;
-- 本demo项目中uart_app.lua和timer_app.lua中publish了这个消息;
sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)

return udp_client_sender

4.3.3 TCP_SSL 服务器数据的收发

tcp_ssl_main.lua

本文件为 tcp_ssl client socket 主应用功能模块,核心业务逻辑为:

1、创建一个 tcp_ssl client socket,连接 server;

2、处理连接异常,出现异常后执行重连动作;

3、调用 tcp_ssl_receiver 和 tcp_ssl_sender 中的外部接口,进行数据收发处理;

local libnet = require "libnet"

-- 加载tcp_ssl client socket数据接收功能模块
local tcp_ssl_receiver = require "tcp_ssl_receiver"
-- 加载tcp_ssl client socket数据发送功能模块
local tcp_ssl_sender = require "tcp_ssl_sender"

-- 电脑访问:https://netlab.luatos.com/
-- 点击 打开TCP SSL 按钮,会创建一个TCP SSL server
-- 将server的地址和端口赋值给下面这两个变量
local SERVER_ADDR = "112.125.89.8"
local SERVER_PORT = 42429

-- tcp_ssl_main的任务名
local TASK_NAME = tcp_ssl_sender.TASK_NAME


-- 处理未识别的消息
local function tcp_ssl_main_cbfunc(msg)
        log.info("tcp_ssl_main_cbfunc", msg[1], msg[2], msg[3], msg[4])
end

-- tcp_ssl client socket的任务处理函数
local function tcp_ssl_main_task_func() 

    local socket_client
    local result, para1, para2

    while true do
        -- 如果当前时间点设置的网卡还没有连接成功,一直在这里循环等待
        while not socket.adapter(socket.dft()) do
            log.warn("tcp_ssl_main_task_func", "wait IP_READY", socket.dft())
            -- 在此处阻塞等待网卡连接成功的消息"IP_READY"
            -- 或者等待1秒超时退出阻塞等待状态;
            -- 注意:此处的1000毫秒超时不要修改的更长;
            -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改当前使用的网卡
            -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
            -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
            sys.waitUntil("IP_READY", 1000)
        end

        -- 检测到了IP_READY消息
        log.info("tcp_ssl_main_task_func", "recv IP_READY", socket.dft())

        -- 创建socket client对象
        socket_client = socket.create(nil, TASK_NAME)
        -- 如果创建socket client对象失败
        if not socket_client then
            log.error("tcp_ssl_main_task_func", "socket.create error")
            goto EXCEPTION_PROC
        end

        -- 配置socket client对象为tcp_ssl client
        -- 不做证书校验
        result = socket.config(socket_client, nil, nil, true)
        -- 如果配置失败
        if not result then
            log.error("tcp_ssl_main_task_func", "socket.config error")
            goto EXCEPTION_PROC
        end

        -- 连接server
        result = libnet.connect(TASK_NAME, 15000, socket_client, SERVER_ADDR, SERVER_PORT)
        -- 如果连接server失败
        if not result then
            log.error("tcp_ssl_main_task_func", "libnet.connect error")
            goto EXCEPTION_PROC
        end

        log.info("tcp_ssl_main_task_func", "libnet.connect success")

        -- 数据收发以及网络连接异常事件总处理逻辑
        while true do
            -- 数据接收处理(接收处理必须写在libnet.wait之前,因为老版本的内核固件要求必须这样,新版本的内核固件没这个要求,为了不出问题,写在libnet.wait之前就行了)
            -- 如果处理失败,则退出循环
            if not tcp_ssl_receiver.proc(socket_client) then
                log.error("tcp_ssl_main_task_func", "tcp_ssl_receiver.proc error")
                break
            end

            -- 数据发送处理
            -- 如果处理失败,则退出循环
            if not tcp_ssl_sender.proc(TASK_NAME, socket_client) then
                log.error("tcp_ssl_main_task_func", "tcp_ssl_sender.proc error")
                break
            end

            -- 阻塞等待socket.EVENT事件或者15秒钟超时
            -- 以下三种业务逻辑会发布事件:
            -- 1、socket client和server之间的连接出现异常(例如server主动断开,网络环境出现异常等),此时在内核固件中会发布事件socket.EVENT
            -- 2、socket client接收到server发送过来的数据,此时在内核固件中会发布事件socket.EVENT
            -- 3、socket client需要发送数据到server, 在tcp_ssl_sender.lua中会发布事件socket.EVENT
                        result, para1, para2 = libnet.wait(TASK_NAME, 15000, socket_client)
            log.info("tcp_ssl_main_task_func", "libnet.wait", result, para1, para2)

                        -- 如果连接异常,则退出循环
                        if not result then
                                log.warn("tcp_ssl_main_task_func", "connection exception")
                                break
            end
        end


        -- 出现异常    
        ::EXCEPTION_PROC::

        -- 数据发送应用模块对来不及发送的数据做清空和通知失败处理
        tcp_ssl_sender.exception_proc()

        -- 如果存在socket client对象
        if socket_client then
            -- 关闭socket client连接
            libnet.close(TASK_NAME, 5000, socket_client)

            -- 释放socket client对象
            socket.release(socket_client)
            socket_client = nil
        end

        -- 5秒后跳转到循环体开始位置,自动发起重连
        sys.wait(5000)
    end
end

--创建并且启动一个task
--运行这个task的主函数tcp_ssl_main_task_func
sysplus.taskInitEx(tcp_ssl_main_task_func, TASK_NAME, tcp_ssl_main_cbfunc)
tcp_ssl_receiver.lua

本文件为 tcp_ssl client socket 数据接收应用功能模块,核心业务逻辑为: 从内核读取接收到的数据,然后将数据发送给其他应用功能模块做进一步处理;

local tcp_ssl_receiver = {}

-- socket数据接收缓冲区
local recv_buff = nil

--[[
检查socket client是否收到数据,如果收到数据,读取并且处理完所有数据

@api tcp_ssl_receiver.proc(socket_client)

@param1 socket_client userdata
表示由socket.create接口创建的socket client对象
必须传入,不允许为空或者nil

@return1 result bool
表示处理结果,成功为true,失败为false

@usage
-- 
tcp_ssl_receiver.proc(socket_client)
]]
function tcp_ssl_receiver.proc(socket_client)
    -- 如果socket数据接收缓冲区还没有申请过空间,则先申请内存空间
    if recv_buff==nil then
        recv_buff = zbuff.create(1024)
        -- 当recv_buff不再使用时,不需要主动调用recv_buff:free()去释放
        -- 因为Lua的垃圾处理器会自动释放recv_buff所申请的内存空间
        -- 如果等不及垃圾处理器自动处理,在确定以后不会再使用recv_buff时,则可以主动调用recv_buff:free()释放内存空间
    end

    -- 循环从内核的缓冲区读取接收到的数据
    -- 如果读取失败,返回false,退出
    -- 如果读取成功,处理数据,并且继续循环读取
    -- 如果读取成功,并且读出来的数据为空,表示已经没有数据可读,返回true,退出
    while true do
        -- 从内核的缓冲区中读取数据到recv_buff中
        -- 如果recv_buff的存储空间不足,会自动扩容
        -- 如果使用netlab.luatos.com创建的tcl ssl server来配合测试,注意server存在一个问题,后续可能会解决,也可能没解决,问题如下:
        -- netlab创建的tcp ssl server,第一次下发数据给client时,会截取输入的完整数据的一半数据做为一包发送,剩余一半做为第二包发送;
        -- 以后server再次发送其他数据,一包的最大长度一直是第一次下发使用的长度;
        -- 假设第一次在编辑框输入了12字节的数据,则会拆分成2包数据进行发送,每包6字节;
        -- 假设第一次在编辑框输入了120字节的数据,则会拆分成20包数据进行发送,每包仍然6字节;
        -- 如果出现了这个问题,不用担心,和client无关,最终你使用自己的server时只要保证自己的server没问题就行;
        local result = socket.rx(socket_client, recv_buff)

        -- 读取数据失败
        -- 有两种情况:
        -- 1、recv_buff扩容失败
        -- 2、socket client和server之间的连接断开
        if not result then
            log.error("tcp_ssl_receiver.proc", "socket.rx error")
            return false
        end

        -- 如果读取到了数据, used()就必然大于0, 进行处理
        if recv_buff:used() > 0 then
            log.info("tcp_ssl_receiver.proc", "recv data len", recv_buff:used())

            -- 读取socket数据接收缓冲区中的数据,赋值给data
            local data = recv_buff:query()

            -- 将数据data通过"RECV_DATA_FROM_SERVER"消息publish出去,给其他应用模块处理
            sys.publish("RECV_DATA_FROM_SERVER", "recv from tcp_ssl server: ", data)

            -- 接收到数据,通知网络环境检测看门狗功能模块进行喂狗
            sys.publish("FEED_NETWORK_WATCHDOG")

            -- 清空socket数据接收缓冲区中的数据
            recv_buff:del()
        -- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
        else
            break
        end
    end

    return true
end

return tcp_ssl_receiver
tcp_ssl_sender.lua

本文件为 tcp_ssl client socket 数据发送应用功能模块,核心业务逻辑为:

1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)订阅"SEND_DATA_REQ"消息,将其他应用模块需要发送的数据存储到队列 send_queue 中;

2、tcp_ssl_main 主任务调用 tcp_ssl_sender.proc 接口,遍历队列 send_queue,逐条发送数据到 server;

3、tcp_ssl client socket 和 server 之间的连接如果出现异常,tcp_ssl_main 主任务调用 tcp_ssl_sender.exception_proc 接口,丢弃掉队列 send_queue 中未发送的数据;

4、任何一条数据无论发送成功还是失败,只要这条数据有回调函数,都会通过回调函数通知数据发送方;

local tcp_ssl_sender = {}

local libnet = require "libnet"

--[[
数据发送队列,数据结构为:
{
    [1] = {data="data1", cb={func=callback_function1, para=callback_para1}},
    [2] = {data="data2", cb={func=callback_function2, para=callback_para2}},
}
data的内容为真正要发送的数据,必须存在;
func的内容为数据发送结果的用户回调函数,可以不存在
para的内容为数据发送结果的用户回调函数的回调参数,可以不存在;
]]
local send_queue = {}

-- tcp_ssl_main的任务名
tcp_ssl_sender.TASK_NAME = "tcp_ssl_main"

-- "SEND_DATA_REQ"消息的处理函数
local function send_data_req_proc_func(tag, data, cb)
    -- 将原始数据增加前缀,然后插入到发送队列send_queue中
    table.insert(send_queue, {data="send from "..tag..": "..data, cb=cb})
    -- 通知tcp_ssl_main主任务有数据需要发送
    -- tcp_ssl_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
    sysplus.sendMsg(tcp_ssl_sender.TASK_NAME, socket.EVENT, 0)
end

--[[
检查socket client是否需要发送数据,如果需要发送数据,读取并且发送完发送队列中的所有数据

@api tcp_ssl_sender.proc(task_name, socket_client)

@param1 task_name string
表示socket.create接口创建socket client对象时所处的task的name
必须传入,不允许为空或者nil

@param2 socket_client userdata
表示由socket.create接口创建的socket client对象
必须传入,不允许为空或者nil

@return1 result bool
表示处理结果,成功为true,失败为false

@usage
tcp_ssl_sender.proc("tcp_client_main", socket_client)
]]
function tcp_ssl_sender.proc(task_name, socket_client)
    local send_item
    local result, buff_full

    -- 遍历数据发送队列send_queue
    while #send_queue>0 do
        -- 取出来第一条数据赋值给send_item
        -- 同时从队列send_queue中删除这一条数据
        send_item = table.remove(send_queue,1)

        -- 发送这条数据,超时时间15秒钟
        result, buff_full = libnet.tx(task_name, 15000, socket_client, send_item.data)

        -- 发送失败
        if not result then
            log.error("tcp_ssl_sender.proc", "libnet.tx error")

            -- 如果当前发送的数据有用户回调函数,则执行用户回调函数
            if send_item.cb and send_item.cb.func then
                send_item.cb.func(false, send_item.cb.para)
            end

            return false
        end

        -- 如果内核固件中缓冲区满了,则将send_item再次插入到send_queue的队首位置,等待下次尝试发送
        if buff_full then
            log.error("tcp_ssl_sender.proc", "buffer is full, wait for the next time")
            table.insert(send_queue, 1, send_item)
            return true
        end

        log.info("tcp_ssl_sender.proc", "send success")
        -- 发送成功,如果当前发送的数据有用户回调函数,则执行用户回调函数
        if send_item.cb and send_item.cb.func then
            send_item.cb.func(true, send_item.cb.para)
        end

        -- 发送成功,通知网络环境检测看门狗功能模块进行喂狗
        sys.publish("FEED_NETWORK_WATCHDOG")
    end

    return true
end

--[[
socket client连接出现异常时,清空等待发送的数据,并且执行发送方的回调函数

@api tcp_ssl_sender.exception_proc()

@usage
tcp_ssl_sender.exception_proc()
]]
function tcp_ssl_sender.exception_proc()
    -- 遍历数据发送队列send_queue
    while #send_queue>0 do
        local send_item = table.remove(send_queue,1)
        -- 发送失败,如果当前发送的数据有用户回调函数,则执行用户回调函数
        if send_item.cb and send_item.cb.func then
            send_item.cb.func(false, send_item.cb.para)
        end
    end
end

-- 订阅"SEND_DATA_REQ"消息;
-- 其他应用模块如果需要发送数据,直接sys.publish这个消息即可,将需要发送的数据以及回调函数和毁掉参数一起publish出去;
-- 本demo项目中uart_app.lua和timer_app.lua中publish了这个消息;
sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)

return tcp_ssl_sender

4.3.4 TCP_SSL_CA 服务器数据的收发

注意:本示例在连接 TCP_SSL 时 client 仅单向校验 server 的证书,server 不校验 client 的证书和密钥文件;

tcp_ssl_ca_main.lua

本文件为 tcp_ssl_ca client socket 主应用功能模块,核心业务逻辑为:

1、创建一个 tcp_ssl_ca client socket,连接 server;

2、处理连接异常,出现异常后执行重连动作;

3、调用 tcp_ssl_ca_receiver 和 tcp_ssl_ca_sender 中的外部接口,进行数据收发处理;

local libnet = require "libnet"

-- 加载sntp时间同步应用功能模块(ca证书校验的ssl socket需要时间同步功能)
require "sntp_app"

-- 加载tcp_ssl_ca client socket数据接收功能模块
local tcp_ssl_ca_receiver = require "tcp_ssl_ca_receiver"
-- 加载tcp_ssl_ca client socket数据发送功能模块
local tcp_ssl_ca_sender = require "tcp_ssl_ca_sender"

-- https://www.baidu.com网站服务器,地址为"www.baidu.com",端口为443
local SERVER_ADDR = "www.baidu.com"
local SERVER_PORT = 443

-- tcp_ssl_ca_main的任务名
local TASK_NAME = tcp_ssl_ca_sender.TASK_NAME


-- 处理未识别的消息
local function tcp_ssl_ca_main_cbfunc(msg)
        log.info("tcp_ssl_ca_main_cbfunc", msg[1], msg[2], msg[3], msg[4])
end

-- tcp_ssl_ca client socket的任务处理函数
local function tcp_ssl_ca_main_task_func() 

    local socket_client
    local result, para1, para2

    -- 用来验证server证书是否合法的ca证书文件为baidu_parent_ca.crt
    -- 此ca证书的有效期截止到2028年11月21日
    -- 将这个ca证书文件的内容读取出来,赋值给server_ca_cert
    -- 注意:此处的ca证书文件仅用来验证baidu网站的server证书
    -- baidu网站的server证书有效期截止到2026年8月10日
    -- 在有效期之前,baidu会更换server证书,如果server证书更换后,此处验证使用的baidu_parent_ca.crt也可能需要更换
    -- 使用电脑上的网页浏览器访问https://www.baidu.com,可以实时看到baidu的server证书以及baidu_parent_ca.crt
    -- 如果你使用的是自己的server,要替换为自己server证书对应的ca证书文件
    local server_ca_cert = io.readFile("/luadb/baidu_parent_ca.crt")

    while true do
        -- 如果当前时间点设置的网卡还没有连接成功,一直在这里循环等待
        while not socket.adapter(socket.dft()) do
            log.warn("tcp_ssl_ca_main_task_func", "wait IP_READY", socket.dft())
            -- 在此处阻塞等待网卡连接成功的消息"IP_READY"
            -- 或者等待1秒超时退出阻塞等待状态;
            -- 注意:此处的1000毫秒超时不要修改的更长;
            -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改当前使用的网卡
            -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
            -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
            sys.waitUntil("IP_READY", 1000)
        end

        -- 检测到了IP_READY消息
        log.info("tcp_ssl_ca_main_task_func", "recv IP_READY", socket.dft())

        -- 创建socket client对象
        socket_client = socket.create(nil, TASK_NAME)
        -- 如果创建socket client对象失败
        if not socket_client then
            log.error("tcp_ssl_ca_main_task_func", "socket.create error")
            goto EXCEPTION_PROC
        end

        -- 配置socket client对象为tcp_ssl_ca client
        -- client仅单向校验server的证书,server不校验client的证书和密钥文件
        -- 如果做证书校验,需要特别注意以下几点:
        -- 1、证书校验前,设备端必须同步为正确的时间,因为校验过程中会检查ca证书以及server证书中的有效期是否合法;本demo中的sntp_app.lua会同步时间;
        -- 2、任何证书都有有效期,无论是ca证书还是server证书,必须在有效期截止之前,及时更换证书,延长有效期,否则证书校验会失败;
        -- 3、如果要更换ca证书,需要在设备端远程升级,必须保证ca证书失效之前升级成功,否则校验失败,就无法连接server;
        -- 综上所述,证书校验虽然安全,可以验证身份,但是后续维护成本比较高;除非有需要,否则可以不配置证书校验功能;
        -- 另外,如果使用https://netlab.luatos.com/创建的TCP SSL Server,使用的server证书有可能过了有效期;
        -- 如果过了有效期,使用本文件无法连接成功tcp ssl ca server,遇到这种问题,可以在main.lua中打开socket.sslLog(3),观察Luatools的日志,如果出现类似于下面的日志
        -- expires on        : 2020-12-27 15:46:55
        -- 表示证书有效期截止到2020-12-27 15:46:55,明显就是证书已经过了有效期
        -- 遇到这种情况,可以反馈给合宙的技术人员;或者不再使用netlab server测试,使用你自己的tcp ssl server来测试,只要保证你的server证书合法就行;
        result = socket.config(socket_client, nil, nil, true, nil, nil, nil, server_ca_cert)
        -- 如果配置失败
        if not result then
            log.error("tcp_ssl_ca_main_task_func", "socket.config error")
            goto EXCEPTION_PROC
        end

        -- 连接server
        result = libnet.connect(TASK_NAME, 15000, socket_client, SERVER_ADDR, SERVER_PORT)
        -- 如果连接server失败
        if not result then
            log.error("tcp_ssl_ca_main_task_func", "libnet.connect error")
            goto EXCEPTION_PROC
        end

        log.info("tcp_ssl_ca_main_task_func", "libnet.connect success")

        -- 数据收发以及网络连接异常事件总处理逻辑
        while true do
            -- 数据接收处理(接收处理必须写在libnet.wait之前,因为老版本的内核固件要求必须这样,新版本的内核固件没这个要求,为了不出问题,写在libnet.wait之前就行了)
            -- 如果处理失败,则退出循环
            if not tcp_ssl_ca_receiver.proc(socket_client) then
                log.error("tcp_ssl_ca_main_task_func", "tcp_ssl_ca_receiver.proc error")
                break
            end

            -- 数据发送处理
            -- 如果处理失败,则退出循环
            if not tcp_ssl_ca_sender.proc(TASK_NAME, socket_client) then
                log.error("tcp_ssl_ca_main_task_func", "tcp_ssl_ca_sender.proc error")
                break
            end

            -- 阻塞等待socket.EVENT事件或者15秒钟超时
            -- 以下三种业务逻辑会发布事件:
            -- 1、socket client和server之间的连接出现异常(例如server主动断开,网络环境出现异常等),此时在内核固件中会发布事件socket.EVENT
            -- 2、socket client接收到server发送过来的数据,此时在内核固件中会发布事件socket.EVENT
            -- 3、socket client需要发送数据到server, 在tcp_ssl_ca_sender.lua中会发布事件socket.EVENT
                        result, para1, para2 = libnet.wait(TASK_NAME, 15000, socket_client)
            log.info("tcp_ssl_ca_main_task_func", "libnet.wait", result, para1, para2)

                        -- 如果连接异常,则退出循环
                        if not result then
                                log.warn("tcp_ssl_ca_main_task_func", "connection exception")
                                break
            end
        end


        -- 出现异常    
        ::EXCEPTION_PROC::

        -- 数据发送应用模块对来不及发送的数据做清空和通知失败处理
        tcp_ssl_ca_sender.exception_proc()

        -- 如果存在socket client对象
        if socket_client then
            -- 关闭socket client连接
            libnet.close(TASK_NAME, 5000, socket_client)

            -- 释放socket client对象
            socket.release(socket_client)
            socket_client = nil
        end

        -- 5秒后跳转到循环体开始位置,自动发起重连
        sys.wait(5000)
    end
end

--创建并且启动一个task
--运行这个task的主函数tcp_ssl_ca_main_task_func
sysplus.taskInitEx(tcp_ssl_ca_main_task_func, TASK_NAME, tcp_ssl_ca_main_cbfunc)
tcp_ssl_ca_receiver.lua

本文件为 tcp_ssl_ca client socket 数据接收应用功能模块,核心业务逻辑为:

从内核读取接收到的数据,然后将数据发送给其他应用功能模块做进一步处理;

local tcp_ssl_ca_receiver = {}

-- socket数据接收缓冲区
local recv_buff = nil

--[[
检查socket client是否收到数据,如果收到数据,读取并且处理完所有数据

@api tcp_ssl_ca_receiver.proc(socket_client)

@param1 socket_client userdata
表示由socket.create接口创建的socket client对象
必须传入,不允许为空或者nil

@return1 result bool
表示处理结果,成功为true,失败为false

@usage
-- 
tcp_ssl_ca_receiver.proc(socket_client)
]]
function tcp_ssl_ca_receiver.proc(socket_client)
    -- 如果socket数据接收缓冲区还没有申请过空间,则先申请内存空间
    if recv_buff==nil then
        recv_buff = zbuff.create(1024)
        -- 当recv_buff不再使用时,不需要主动调用recv_buff:free()去释放
        -- 因为Lua的垃圾处理器会自动释放recv_buff所申请的内存空间
        -- 如果等不及垃圾处理器自动处理,在确定以后不会再使用recv_buff时,则可以主动调用recv_buff:free()释放内存空间
    end

    -- 循环从内核的缓冲区读取接收到的数据
    -- 如果读取失败,返回false,退出
    -- 如果读取成功,处理数据,并且继续循环读取
    -- 如果读取成功,并且读出来的数据为空,表示已经没有数据可读,返回true,退出
    while true do
        -- 从内核的缓冲区中读取数据到recv_buff中
        -- 如果recv_buff的存储空间不足,会自动扩容
        -- 如果使用netlab.luatos.com创建的tcl ssl server来配合测试,注意server存在一个问题,后续可能会解决,也可能没解决,问题如下:
        -- netlab创建的tcp ssl server,第一次下发数据给client时,会截取输入的完整数据的一半数据做为一包发送,剩余一半做为第二包发送;
        -- 以后server再次发送其他数据,一包的最大长度一直是第一次下发使用的长度;
        -- 假设第一次在编辑框输入了12字节的数据,则会拆分成2包数据进行发送,每包6字节;
        -- 假设第一次在编辑框输入了120字节的数据,则会拆分成20包数据进行发送,每包仍然6字节;
        -- 如果出现了这个问题,不用担心,和client无关,最终你使用自己的server时只要保证自己的server没问题就行;
        local result = socket.rx(socket_client, recv_buff)

        -- 读取数据失败
        -- 有两种情况:
        -- 1、recv_buff扩容失败
        -- 2、socket client和server之间的连接断开
        if not result then
            log.error("tcp_ssl_ca_receiver.proc", "socket.rx error")
            return false
        end

        -- 如果读取到了数据, used()就必然大于0, 进行处理
        if recv_buff:used() > 0 then
            log.info("tcp_ssl_ca_receiver.proc", "recv data len", recv_buff:used())

            -- 读取socket数据接收缓冲区中的数据,赋值给data
            local data = recv_buff:query()

            -- 将数据data通过"RECV_DATA_FROM_SERVER"消息publish出去,给其他应用模块处理
            sys.publish("RECV_DATA_FROM_SERVER", "recv from tcp_ssl_ca server: ", data)

            -- 接收到数据,通知网络环境检测看门狗功能模块进行喂狗
            sys.publish("FEED_NETWORK_WATCHDOG")

            -- 清空socket数据接收缓冲区中的数据
            recv_buff:del()
            -- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
        else
            break
        end
    end

    return true
end

return tcp_ssl_ca_receiver
tcp_ssl_ca_sender.lua

本文件为 tcp_ssl_ca client socket 数据发送应用功能模块,核心业务逻辑为:

1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)订阅"SEND_DATA_REQ"消息,将其他应用模块需要发送的数据存储到队列 send_queue 中;

2、tcp_ssl_ca_main 主任务调用 tcp_ssl_ca_sender.proc 接口,遍历队列 send_queue,逐条发送数据到 server;

3、tcp_ssl_ca client socket 和 server 之间的连接如果出现异常,tcp_ssl_ca_main 主任务调用 tcp_ssl_ca_sender.exception_proc 接口,丢弃掉队列 send_queue 中未发送的数据;

4、任何一条数据无论发送成功还是失败,只要这条数据有回调函数,都会通过回调函数通知数据发送方;

local tcp_ssl_ca_sender = {}

local libnet = require "libnet"

--[[
数据发送队列,数据结构为:
{
    [1] = {data="data1", cb={func=callback_function1, para=callback_para1}},
    [2] = {data="data2", cb={func=callback_function2, para=callback_para2}},
}
data的内容为真正要发送的数据,必须存在;
func的内容为数据发送结果的用户回调函数,可以不存在
para的内容为数据发送结果的用户回调函数的回调参数,可以不存在;
]]
local send_queue = {}

-- tcp_ssl_ca_main的任务名
tcp_ssl_ca_sender.TASK_NAME = "tcp_ssl_ca_main"

-- "SEND_DATA_REQ"消息的处理函数
local function send_data_req_proc_func(tag, data, cb)
    -- 将原始数据增加前缀,然后插入到发送队列send_queue中
    table.insert(send_queue, {data="send from "..tag..": "..data, cb=cb})
    -- 通知tcp_ssl_ca_main主任务有数据需要发送
    -- tcp_ssl_ca_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
    sysplus.sendMsg(tcp_ssl_ca_sender.TASK_NAME, socket.EVENT, 0)
end

--[[
检查socket client是否需要发送数据,如果需要发送数据,读取并且发送完发送队列中的所有数据

@api tcp_ssl_ca_sender.proc(task_name, socket_client)

@param1 task_name string
表示socket.create接口创建socket client对象时所处的task的name
必须传入,不允许为空或者nil

@param2 socket_client userdata
表示由socket.create接口创建的socket client对象
必须传入,不允许为空或者nil

@return1 result bool
表示处理结果,成功为true,失败为false

@usage
tcp_ssl_ca_sender.proc("tcp_client_main", socket_client)
]]
function tcp_ssl_ca_sender.proc(task_name, socket_client)
    local send_item
    local result, buff_full

    -- 遍历数据发送队列send_queue
    while #send_queue>0 do
        -- 取出来第一条数据赋值给send_item
        -- 同时从队列send_queue中删除这一条数据
        send_item = table.remove(send_queue,1)

        -- 发送这条数据,超时时间15秒钟
        result, buff_full = libnet.tx(task_name, 15000, socket_client, send_item.data)

        -- 发送失败
        if not result then
            log.error("tcp_ssl_ca_sender.proc", "libnet.tx error")

            -- 如果当前发送的数据有用户回调函数,则执行用户回调函数
            if send_item.cb and send_item.cb.func then
                send_item.cb.func(false, send_item.cb.para)
            end

            return false
        end

        -- 如果内核固件中缓冲区满了,则将send_item再次插入到send_queue的队首位置,等待下次尝试发送
        if buff_full then
            log.error("tcp_ssl_ca_sender.proc", "buffer is full, wait for the next time")
            table.insert(send_queue, 1, send_item)
            return true
        end

        log.info("tcp_ssl_ca_sender.proc", "send success")
        -- 发送成功,如果当前发送的数据有用户回调函数,则执行用户回调函数
        if send_item.cb and send_item.cb.func then
            send_item.cb.func(true, send_item.cb.para)
        end

        -- 发送成功,通知网络环境检测看门狗功能模块进行喂狗
        sys.publish("FEED_NETWORK_WATCHDOG")
    end

    return true
end

--[[
socket client连接出现异常时,清空等待发送的数据,并且执行发送方的回调函数

@api tcp_ssl_ca_sender.exception_proc()

@usage
tcp_ssl_ca_sender.exception_proc()
]]
function tcp_ssl_ca_sender.exception_proc()
    -- 遍历数据发送队列send_queue
    while #send_queue>0 do
        local send_item = table.remove(send_queue,1)
        -- 发送失败,如果当前发送的数据有用户回调函数,则执行用户回调函数
        if send_item.cb and send_item.cb.func then
            send_item.cb.func(false, send_item.cb.para)
        end
    end
end

-- 订阅"SEND_DATA_REQ"消息;
-- 其他应用模块如果需要发送数据,直接sys.publish这个消息即可,将需要发送的数据以及回调函数和毁掉参数一起publish出去;
-- 本demo项目中uart_app.lua和timer_app.lua中publish了这个消息;
sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)

return tcp_ssl_ca_sender
sntp_app.lua

本文件为 sntp 时间同步应用功能模块,核心业务逻辑为:

1、连接 ntp 服务器进行时间同步;

2、如果同步成功,1 小时之后重新发起同步动作;

3、如果同步失败,10 秒钟之后重新发起同步动作;

-- sntp时间同步的任务处理函数
local function sntp_task_func() 

    while true do
        -- 如果当前时间点设置的网卡还没有连接成功,一直在这里循环等待
        while not socket.adapter(socket.dft()) do
            log.warn("sntp_task_func", "wait IP_READY", socket.dft())
            -- 在此处阻塞等待网卡连接成功的消息"IP_READY"
            -- 或者等待1秒超时退出阻塞等待状态;
            -- 注意:此处的1000毫秒超时不要修改的更长;
            -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改当前使用的网卡
            -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
            -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
            sys.waitUntil("IP_READY", 1000)
        end

        -- 检测到了IP_READY消息
        log.warn("sntp_task_func", "recv IP_READY")

        -- 发起ntp时间同步动作
        socket.sntp()

        -- 等待ntp时间同步结果,30秒超时失败,通常只需要几百毫秒就能成功
        local ret = sys.waitUntil("NTP_UPDATE", 30000)

        --同步成功
        if ret then
            -- 以下是获取/打印时间的演示,注意时区问题
            log.info("sntp_task_func", "时间同步成功", "本地时间", os.date())
            log.info("sntp_task_func", "时间同步成功", "UTC时间", os.date("!%c"))
            log.info("sntp_task_func", "时间同步成功", "RTC时钟(UTC时间)", json.encode(rtc.get()))
            log.info("sntp_task_func", "时间同步成功", "本地时间戳", os.time())
            local t = os.date("*t")
            log.info("sntp_task_func", "时间同步成功", "本地时间os.date() json格式", json.encode(t))
            log.info("sntp_task_func", "时间同步成功", "本地时间os.date(os.time())", os.time(t))

            -- 正常使用, 一小时一次, 已经足够了, 甚至1天一次也可以
            sys.wait(3600000) 
        --同步失败
        else
            log.info("sntp_task_func", "时间同步失败")
            -- 10秒后重新发起同步动作
            sys.wait(10000) 
        end
    end
end

--创建并且启动一个task
--运行这个task的主函数sntp_task_func
sys.taskInit(sntp_task_func)

4.3.5 网络环境看门狗

本 demo 设计的网络环境检测看门狗功能模块,可以检测以下两种种的任意一种网络环境异常:

(1) 网络环境连续超过 3 分钟没有准备就绪

(2) tcp、tcp ssl、tcp ssl 单向校验证书 3 路连接中,连续 3 分钟没有成功发送数据到服务器;并且 4 路连接中,连续 3 分钟没有收到服务器下发的数据;

-- 网络环境检测看门狗task处理函数
**local** **function** **network_watchdog_task_func**()
    **while** **true** **do**
        --如果等待180秒没有等到"FEED_NETWORK_WATCHDOG"消息,则看门狗超时
        **if** not sys.waitUntil("FEED_NETWORK_WATCHDOG", 180000) **then**            
            log.error("network_watchdog_task_func timeout")
            -- 等待3秒钟,然后软件重启
            sys.wait(3000)
            rtos.reboot()
        **end**
    **end**
**end**

--创建并且启动一个task
--运行这个task的处理函数network_watchdog_task_func
sys.taskInit(network_watchdog_task_func)

4.3.6 定时发送数据给服务器

本文件为定时器应用功能模块,核心业务逻辑为:

创建一个 5 秒的循环定时器,每次产生一段数据,通知四个 socket client 进行处理;

local data = 1

-- 数据发送结果回调函数
-- result:发送结果,true为发送成功,false为发送失败
-- para:回调参数,sys.publish("SEND_DATA_REQ", "timer", data, {func=send_data_cbfunc, para="timer"..data})中携带的para
local function send_data_cbfunc(result, para)
    log.info("send_data_cbfunc", result, para)
    -- 无论上一次发送成功还是失败,启动一个5秒的定时器,5秒后发送下次数据
    sys.timerStart(send_data_req_timer_cbfunc, 5000)
end

-- 定时器回调函数
function send_data_req_timer_cbfunc()
    -- 发布消息"SEND_DATA_REQ"
    -- 携带的第一个参数"timer"表示是定时器应用模块发布的消息
    -- 携带的第二个参数data为要发送的原始数据
    -- 携带的第三个参数cb为发送结果回调(可以为空,如果为空,表示不关心socket client发送数据成功还是失败),其中:
    --       cb.func为回调函数(可以为空,如果为空,表示不关心socket client发送数据成功还是失败)
    --       cb.para为回调函数的第二个参数(可以为空),回调函数的第一个参数为发送结果(true表示成功,false表示失败)
    sys.publish("SEND_DATA_REQ", "timer", data, {func=send_data_cbfunc, para="timer"..data})
    data = data+1
end

-- 启动一个5秒的单次定时器
-- 时间到达后,执行一次send_data_req_timer_cbfunc函数
sys.timerStart(send_data_req_timer_cbfunc, 5000)

4.3.7 串口和服务器之间透传数据

本文件为串口应用功能模块,核心业务逻辑为:

1、打开 uart1,波特率 115200,数据位 8,停止位 1,无奇偶校验位;

2、uart1 和 pc 端的串口工具相连;

3、从 uart1 接收到 pc 端串口工具发送的数据后,通知四个 socket client 进行处理;

4、收到四个 socket client 从 socket server 接收到的数据后,将数据通过 uart1 发送到 pc 端串口工具;

-- 使用UART1
local UART_ID = 1
-- 串口接收数据缓冲区
local read_buf = ""

-- 将前缀prefix和数据data拼接
-- 然后末尾增加回车换行两个字符,通过uart发送出去,方便在PC端换行显示查看
local function recv_data_from_server_proc(prefix, data)
    uart.write(UART_ID, prefix..data.."\r\n")
end


local function concat_timeout_func()
    -- 如果存在尚未处理的串口缓冲区数据;
    -- 将数据通过publish通知其他应用功能模块处理;
    -- 然后清空本文件的串口缓冲区数据
    if read_buf:len() > 0 then
        sys.publish("SEND_DATA_REQ", "uart", read_buf)
        read_buf = ""
    end
end


-- UART1的数据接收中断处理函数,UART1接收到数据时,会执行此函数
local function read()
    local s
    while true do
        -- 非阻塞读取UART1接收到的数据,最长读取1024字节
        s = uart.read(UART_ID, 1024)

        -- 如果从串口没有读到数据
        if not s or s:len() == 0 then
            -- 启动50毫秒的定时器,如果50毫秒内没收到新的数据,则处理当前收到的所有数据
            -- 这样处理是为了防止将一大包数据拆分成多个小包来处理
            -- 例如pc端串口工具下发1100字节的数据,可能会产生将近20次的中断进入到read函数,才能读取完整
            -- 此处的50毫秒可以根据自己项目的需求做适当修改,在满足整包拼接完整的前提下,时间越短,处理越及时
            sys.timerStart(concat_timeout_func, 50)
            -- 跳出循环,退出本函数
            break
        end

        log.info("uart_app.read len", s:len())
        -- log.info("uart_app.read", s)

        -- 将本次从串口读到的数据拼接到串口缓冲区read_buf中
        read_buf = read_buf..s
    end
end



-- 初始化UART1,波特率115200,数据位8,停止位1
uart.setup(UART_ID, 115200, 8, 1)

-- 注册UART1的数据接收中断处理函数,UART1接收到数据时,会执行read函数
uart.on(UART_ID, "receive", read)

-- 订阅"RECV_DATA_FROM_SERVER"消息的处理函数recv_data_from_server_proc
-- 收到"RECV_DATA_FROM_SERVER"消息后,会执行函数recv_data_from_server_proc
sys.subscribe("RECV_DATA_FROM_SERVER", recv_data_from_server_proc)

4.3.8 网卡切换

4.3.8.1 netdrv_4g.lua

本文件为 4G 网卡驱动模块,核心业务逻辑为:

1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;

local function ip_ready_func()
    log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_GP))
end

local function ip_lose_func()
    log.warn("netdrv_4g.ip_lose_func", "IP_LOSE")
end



--此处订阅"IP_READY"和"IP_LOSE"两种消息
--在消息的处理函数中,仅仅打印了一些信息,便于实时观察4G网络的连接状态
--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)

-- 设置默认网卡为socket.LWIP_GP
-- 在Air8000上,内核固件运行起来之后,默认网卡就是socket.LWIP_GP
-- 在单4G网卡使用场景下,下面这一行代码加不加都没有影响,为了和其他网卡驱动模块的代码风格保持一致,所以加上了
socket.dft(socket.LWIP_GP)
4.3.8.2 netdrv_eth_spi.lua

本文件为“通过 SPI 外挂 CH390H 芯片的以太网卡”驱动模块 ,核心业务逻辑为:

1、打开 CH390H 芯片供电开关;

2、初始化 spi1,初始化以太网卡,并且在以太网卡上开启 DHCP(动态主机配置协议);

3、以太网卡的连接状态发生变化时,在日志中进行打印;

local function ip_ready_func()
    log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
end

local function ip_lose_func()
    log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
end



--此处订阅"IP_READY"和"IP_LOSE"两种消息
--在消息的处理函数中,仅仅打印了一些信息,便于实时观察“通过SPI外挂CH390H芯片的以太网卡”的连接状态
--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)


-- 设置默认网卡为socket.LWIP_ETH
socket.dft(socket.LWIP_ETH)


--本demo测试使用的是Air8000开发板
--GPIO140为CH390H以太网芯片的供电使能控制引脚
gpio.setup(140, 1, gpio.PULLUP)

--这个task的核心业务逻辑是:初始化SPI,初始化以太网卡,并在以太网卡上开启动态主机配置协议
local function netdrv_eth_spi_task_func()
    -- 初始化SPI1
    local result = spi.setup(
        1,--spi_id
        nil,
        0,--CPHA
        0,--CPOL
        8,--数据宽度
        25600000--,--频率
        -- spi.MSB,--高低位顺序    可选,默认高位在前
        -- spi.master,--主模式     可选,默认主
        -- spi.full--全双工       可选,默认全双工
    )
    log.info("netdrv_eth_spi", "spi open result", result)
    --返回值为0,表示打开成功
    if result ~= 0 then
        log.error("netdrv_eth_spi", "spi open error",result)
        return
    end

    --初始化以太网卡

    --以太网联网成功(成功连接路由器,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
    --各个功能模块可以订阅"IP_READY"消息实时处理以太网联网成功的事件
    --也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功

    --以太网断网后,内核固件会产生一个"IP_LOSE"消息
    --各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
    --也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功

    -- socket.LWIP_ETH 指定网络适配器编号
    -- netdrv.CH390外挂CH390
    -- SPI ID 1, 片选 GPIO12
    netdrv.setup(socket.LWIP_ETH, netdrv.CH390, {spi=1, cs=12})

    --在以太上开启动态主机配置协议
    netdrv.dhcp(socket.LWIP_ETH, true)
end

--创建并且启动一个task
--task的处理函数为netdrv_eth_spi_task_func
sys.taskInit(netdrv_eth_spi_task_func)
4.3.8.3 netdrv_wifi.lua

本文件为 WIFI STA 网卡驱动模块,核心业务逻辑为:

1、初始化 WIFI 网络;

2、连接 WIFI 路由器;

3、和 WIFI 路由器之间的连接状态发生变化时,在日志中进行打印;

local function ip_ready_func()
    log.info("netdrv_wifi.ip_ready_func", "IP_READY", json.encode(wlan.getInfo()))
end

local function ip_lose_func()
    log.warn("netdrv_wifi.ip_lose_func", "IP_LOSE")
end



--此处订阅"IP_READY"和"IP_LOSE"两种消息
--在消息的处理函数中,仅仅打印了一些信息,便于实时观察WIFI的连接状态
--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)


-- 设置默认网卡为socket.LWIP_STA
socket.dft(socket.LWIP_STA)


wlan.init()
--连接WIFI热点,连接结果会通过"IP_READY"或者"IP_LOSE"消息通知
--Air8000仅支持2.4G的WIFI,不支持5G的WIFI
--此处前两个参数表示WIFI热点名称以及密码,更换为自己测试时的真实参数即可
--第三个参数1表示WIFI连接异常时,内核固件会自动重连
wlan.connect("茶室-降功耗,找合宙!", "Air123456", 1)

--WIFI联网成功(做为STATION成功连接AP,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
--各个功能模块可以订阅"IP_READY"消息实时处理WIFI联网成功的事件
--也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功

--WIFI断网后,内核固件会产生一个"IP_LOSE"消息
--各个功能模块可以订阅"IP_LOSE"消息实时处理WIFI断网的事件
--也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功
4.3.8.4 netdrv_multiple.lua

本文件为多网卡驱动模块 ,核心业务逻辑为:

1、调用 libnetif.set_priority_order 配置多网卡的控制参数以及优先级;

local libnetif = require "libnetif"

-- 网卡状态变化通知回调函数
-- 当libnetif中检测到网卡切换或者所有网卡都断网时,会触发调用此回调函数
-- 当网卡切换切换时:
--     net_type:string类型,表示当前使用的网卡字符串
--     adapter:number类型,表示当前使用的网卡id
-- 当所有网卡断网时:
--     net_type:为nil
--     adapter:number类型,为-1
local function netdrv_multiple_notify_cbfunc(net_type,adapter)
    if type(net_type)=="string" then
        log.info("netdrv_multiple_notify_cbfunc", "use new adapter", net_type, adapter)
    elseif type(net_type)=="nil" then
        log.warn("netdrv_multiple_notify_cbfunc", "no available adapter", net_type, adapter)
    else
        log.warn("netdrv_multiple_notify_cbfunc", "unknown status", net_type, adapter)
    end
end

local function netdrv_multiple_task_func()
    --设置网卡优先级
    libnetif.set_priority_order(
        {
            -- “通过SPI外挂CH390H芯片”的以太网卡,使用Air8000开发板验证
            {
                ETHERNET = {
                    -- 供电使能GPIO
                    pwrpin = 140,
                    -- 设置的多个“已经IP READY,但是还没有ping通”网卡,循环执行ping动作的间隔(单位毫秒,可选)
                    -- 如果没有传入此参数,libnetif会使用默认值10秒
                    ping_time = 3000,

                    -- 连通性检测ip(选填参数);
                    -- 如果没有传入ip地址,libnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",     

                    -- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
                    tp = netdrv.CH390, 
                    opts = {spi=1, cs=12}
                }
            },

            -- WIFI STA网卡
            {
                WIFI = {
                    -- 要连接的WIFI路由器名称
                    ssid = "茶室-降功耗,找合宙!",
                    -- 要连接的WIFI路由器密码
                    password = "Air123456", 

                    -- 连通性检测ip(选填参数);
                    -- 如果没有传入ip地址,libnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
                }
            },

            -- 4G网卡
            {
                LWIP_GP = true
            }
        }
    )    
end

-- 设置网卡状态变化通知回调函数netdrv_multiple_notify_cbfunc
libnetif.notify_status(netdrv_multiple_notify_cbfunc)

-- 如果存在udp网络应用,并且udp网络应用中,根据应用层的心跳能够判断出来udp数据通信出现了异常;
-- 可以在判断出现异常的位置,调用一次libnetif.check_network_status()接口,强制对当前正式使用的网卡进行一次连通性检测;
-- 如果存在tcp网络应用,不需要用户调用libnetif.check_network_status()接口去控制,libnetif会在tcp网络应用通信异常时自动对当前使用的网卡进行连通性检测。


-- 启动一个task,task的处理函数为netdrv_multiple_task_func
-- 在处理函数中调用libnetif.set_priority_order设置网卡优先级
-- 因为libnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
sys.taskInit(netdrv_multiple_task_func)

五、Socket 长连接功能验证

注意:

(1)第四路连接,连接的是 baidu 的 https 网站,连接成功后,Air8000 每隔一段时间发数据给服务器,因为发送的不是 http 合法格式的数据,所以每隔一段时间服务器都会主动断开连接,断开连接后,Air8000 会自动重连,如此循环,属于正常现象。

(2)若串口工具收到 TCP_SSL 的数据会出现分一条或几条接收的现象是正常的,是因为 netlab ssl 收发的时候,总会以第一次发的数据最大长度的一半来分包。

5.1、测试前准备工作:

5.1.1、TCP 服务器建立:

合宙测试服务器链接:https://netlab.luatos.com/

(1)创建一个 TCP 服务器

(2)复制 TCP 服务器端口号

(3)修改 tcp_client_main.lua 里面的端口号

5.1.2、UDP 服务器建立:

(1)创建一个 UDP 服务器

(2)复制 UDP 服务器端口号

(3)修改 udp_client_main.lua 里面的端口号

5.1.3、TCP SSL 服务器建立:

(1)创建一个 TCP SSL 服务器

(2)复制 TCP SSL 服务器端口号

(3)修改 tcp_ssl_main.lua 里面的端口号

5.2、通过 4G 网卡实现 socket 长连接

注意:如果需要单 4G 网卡,程序部分打开 require "netdrv_4g",其余注释掉。

切换网卡为 4G 网卡:

在"netdrv_device.lua"文件中打开“4G 网卡”驱动模块

netdrv_4g.lua 文件代码如下:

luatools 日志打印:

如出现类似 netdnv 4g.ip ready func IP READY 10.74.255.92 255.255.255.255 0.0.0.0 nil 的日志,则表示 4g 网卡连接成功

TCP 服务器的数据发送与接收:

TCP 端收发数据日志:

串口端收发数据日志:

UDP 服务器的数据发送与接收:

UDP 端收发数据日志:

串口端收发数据日志:

TCP_SSL 服务器的数据发送与接收:

TCP_SSL 端收发数据日志:

串口端收发数据日志:

TCP_SSL_CA 服务器的数据发送与接收:

注意:第四路连接 baidu,每隔一段时间服务器都会主动断开连接,断开连接后,Air8000 会自动重连,如此循环,属于正常现象。

luatools 日志:

串口端收发数据日志:

定时器定时 5s 发送一次数据:

luatools 日志:

TCP 服务器日志:

5.3、通过 WIFI STA 网卡实现长连接

注意:如果需要单 WIFI STA 网卡,打开 require "netdrv_wifi",其余注释掉;同时 netdrv_wifi.lua 中的 wlan.connect("茶室-降功耗,找合宙!", "Air123456", 1),前两个参数,修改为自己测试时 wifi 热点的名称和密码;注意:仅支持 2.4G 的 wifi,不支持 5G 的 wifi

切换网卡为 WIFI STA 网卡:

在"netdrv_device.lua"文件中打开“WIFI STA 网卡”驱动模块

netdrv_wifi.lua 文件代码如下:

luatools 日志打印:

如出现类似 netdrv wifiip ready func IP READY ("gW": 172.20.10.1","rssi:-67,"bssid": 2AD08A1BA2B3"} 的日志,则表示 WIFI STA 网卡联网成功

TCP 服务器的数据发送与接收:

TCP 端收发数据日志:

串口端收发数据日志:

UDP 服务器的数据发送与接收:

UDP 端收发数据日志:

串口端收发数据日志:

TCP_SSL 服务器的数据发送与接收:

TCP_SSL 端收发数据日志:

串口端收发数据日志:

TCP_SSl_CA 服务器的数据发送与接收:

注意:第四路连接 baidu,每隔一段时间服务器都会主动断开连接,断开连接后,Air8000 会自动重连,如此循环,属于正常现象。

luatools 日志:

串口端收发数据日志:

定时器定时 5s 发送一次数据:

luatools 日志:

TCP 服务器日志:

5.4、通过以太网实现长连接

如果需要以太网卡,打开 require "netdrv_eth_spi",其余注释掉

切换网卡为以太网卡:

在"netdrv_device.lua"文件中打开“以太网卡”驱动模块

netdrv_eth.lua 文件代码如下:

luatools 日志打印:

如出现类似 netdrv eth spiip ready func IP READY 192.168.0.168 255.255.255.0 192.168.0.1 nil 则说明连接成功

TCP 服务器的数据发送与接收:

TCP 端收发数据日志:

串口端收发数据日志:

UDP 服务器的数据发送与接收:

UDP 端收发数据日志:

串口端收发数据日志:

TCP_SSl 的数据发送与接收:

TCP_SSL 端收发数据日志:

串口端收发数据日志:

TCP_SSl_CA 的数据发送与接收:

注意:第四路连接 baidu,每隔一段时间服务器都会主动断开连接,断开连接后,Air8000 会自动重连,如此循环,属于正常现象。

luatools 日志:

串口端收发数据日志:

定时器定时 5s 发送一次数据:

luatools 日志:

TCP 服务器日志:

5.5、通过多网卡实现长连接

如果需要多网卡,打开 require "netdrv_multiple",其余注释掉;同时 netdrv_multiple.lua 中的 ssid = "茶室-降功耗,找合宙!", password = "Air123456", 修改为自己测试时 wifi 热点的名称和密码;注意:仅支持 2.4G 的 wifi,不支持 5G 的 wifi。

可根据自己的需求调整网卡的优先级,以下示例设置为以太网卡是最高优先级。

多网卡切换:

首先在"netdrv_device.lua"文件中打开“可以配置优先级的多种网卡”驱动模块

netdrv_multiple.lua 文件代码:

默认以太网卡进行连接

拔掉网线后,网络切换为 wifi 网卡

TCP 服务器的数据发送与接收:

TCP 端收发数据日志:

串口端收发数据日志:

UDP 服务器的数据发送与接收:

UDP 端收发数据日志:

串口端收发数据日志:

TCP_SSl 服务器的数据发送与接收:

TCP_SSL 端收发数据日志:

串口端收发数据日志:

TCP_SSl_CA 服务器的数据发送与接收:

注意:第四路连接 baidu,每隔一段时间服务器都会主动断开连接,断开连接后,Air8000 会自动重连,如此循环,属于正常现象。

luatools 日志:

串口端收发数据日志:

定时器定时 5s 发送一次数据:

luatools 日志:

TCP 服务器日志:

六、总结

至此,我们演示了使用Air8000开发板在不同网卡模式下进行 Socket 长连接的全过程,相信聪明的你已经完全领悟 Socket 长连接的逻辑了,快来实际操作一下吧!