一、TCP 和 UDP 服务器概述

TCP 服务器是基于传输控制协议(TCP)构建的网络服务端程序。它作为被动的通信端点,持续监听特定网络端口,等待客户端连接请求,并为已建立连接的客户端提供可靠的数据通信服务。

UDP 服务器则是基于用户数据报协议(UDP)构建的网络服务端程序。它作为无连接的通信端点,绑定特定网络端口,直接接收客户端发送的数据报文,并提供高效、低延迟的数据传输服务。

补充

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

3>LuatOS 的 socket server 的连接能力

(1) 目前 Tcp server 仅支持一对一连接。

(2) UDP 协议本身是无连接的,这意味着任何在同一局域网下的客户端都可以向 udp server 的 IP 和端口发送数据包。

二、演示功能概述

本文档所演示的功能如下:

  1. 创建 TCP/UDP 服务器:在项目目录中对应两个文件夹。

  2. TCP 文件夹:功能为创建一个 TCP 服务器,等待 TCP 客户端连接。

  3. UDP 文件夹:功能为创建一个 UDP 服务器,等待 UDP 客户端连接。

  4. 服务器数据发送逻辑:当 TCP/UDP 服务器与客户端成功建立连接后,服务器将按照以下逻辑向客户端发送数据:

  5. 通过串口应用功能模块 uart_app.lua,从 UART1 接收串口数据,并为数据增加 send from uart: 前缀后发送给客户端。

  6. 通过定时器应用功能模块 timer_app.lua,定时产生数据,并为数据增加 send from timer: 前缀后发送给客户端。

  7. 网络驱动配置:通过 netdrv_device 模块配置 TCP/UDP 局域网通信所使用的网卡。目前支持以下三种选择(三选一):

  8. netdrv_wifi_sta:WIFI STA 网卡

  9. netdrv_wifi_ap:WIFI AP 网卡

  10. netdrv_eth_spi:通过 SPI 接口外挂 CH390H 芯片的以太网卡

三、准备硬件环境

在这里插入图片描述

Air8000 开发板一块 +wifi 天线一根 + 网线一根:

  • 天线装到开发板上

  • 网线一端插入开发板网口,另外一端连接可以上外网的路由器网口

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 口;

四、准备软件环境

4.1 软件环境

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

  1. 烧录工具:Luatools 工具

  2. 本demo开发测试时使用的固件为LuatOS-SoC_V2018_Air8000,本demo对固件版本没有什么特殊要求,所以你如果要测试本demo时,可以直接使用最新版本的内核固件;如果发现最新版本的内核固件测试有问题,可以使用我们开发本demo时使用的内核固件版本来对比测试;

  3. 脚本文件:点击此处查看;

  4. PC 端串口工具:例如 SSCOMLLCOM 等都可以;

  5. 网络调试工具:SSCOM (可以模拟 TCP/UDP 客户端);

  6. LuatOS 运行所需要的 lib 文件:使用 Luatools 烧录时,勾选 添加默认 lib 选项,使用默认 lib 脚本文件。

准备好软件环境之后,接下来查看如何烧录项目文件到 Air8000 开发板中,将本篇文章中演示使用的项目文件烧录到 Air8000 开发板中。

4.2 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/

五、程序结构

server/
    main.lua
    netdrv_device.lua
    readme.md
    timer_app.lua
    uart_app.lua
    netdrv/
    ├── netdrv_eth_spi.lua
    ├── netdrv_wifi_ap.lua
    └── netdrv_wifi_sta.lua
    tcp/
    ├── tcp_server_main.lua
    ├── tcp_server_receiver.lua
    └── tcp_server_sender.lua
    udp/
    ├── udp_server_main.lua
    ├── udp_server_receiver.lua
    └── udp_server_sender.lua

5.1 文件说明

  • main.lua:主程序入口文件。

  • netdrv_device.lua - 网络驱动设备选择配置

  • readme.md - 项目说明文档

  • timer_app.lua - 定时器应用模块

  • uart_app.lua - 串口应用模块

  • netdrv/ - 网络驱动层

  • netdrv_eth_spi.lua - SPI 以太网驱动(CH390H 芯片)

  • netdrv_wifi_ap.lua - WiFi AP 模式驱动

  • netdrv_wifi_sta.lua - WiFi STA 模式驱动

  • tcp/ - TCP 服务器模块

  • tcp_server_main.lua - TCP 服务器主程序

  • tcp_server_receiver.lua - TCP 数据接收处理

  • tcp_server_sender.lua - TCP 数据发送处理

  • udp/ - UDP 服务器模块

  • udp_server_main.lua - UDP 服务器主程序

  • udp_server_receiver.lua - UDP 数据接收处理

  • udp_server_sender.lua - UDP 数据发送处理

六、核心模块讲解

6.1 主程序(main.lua)

主程序文件 main.lua 是整个项目的入口点。它负责初始化系统环境。

6.2 网络驱动(netdrv/)

网络驱动模块负责初始化和管理不同类型的网络接口,为上层应用提供统一的网络通信能力。

6.2.1 WIFI_AP 网络驱动(netdrv_wifi_ap.lua)
  • 初始化 WiFi AP 功能,配置热点名称、密码等参数,启动接入点供其他设备连接;

  • 设置默认网卡为 socket.LWIP_AP

6.2.2 WIFI_STA 网络驱动(netdrv_wifi_sta.lua)
  • 初始化 WIFI 模块,连接指定的热点(需要修改成需要连接的 WiFi 热点名称和密码,并且是 2.4G,不支持 5G WiFi)。

  • 设置默认网卡为 socket.LWIP_STA

6.2.3 以太网网络驱动(netdrv_eth_spi.lua)
  • 通过 SPI 接口外挂 CH390H 芯片实现以太网。

  • 配置 SPI1 接口参数,用于与 CH390H 芯片通信。

  • 通过 netdrv.setup 函数配置以太网卡,并开启 DHCP 动态获取 IP 地址。

  • 设置默认网卡为 socket.LWIP_ETH

6.3 TCP 服务器模块(tcp/)

TCP 服务器模块实现了基于 TCP 协议的网络通信功能,支持客户端连接管理、数据收发等核心功能。

注意:当前仅支持一对一连接。

6.3.1 tcp_server_main.lua

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

  1. 创建一个 tcp server ,等待 client 连接;

  2. 处理连接异常,出现异常后,关闭当前连接,等待下一个 client 连接;

  3. 调用 tcp_server_receiver 和 tcp_server_sender 中的外部接口,进行数据收发处理;

local libnet = require "libnet"

-- 加载TCP服务器数据接收功能模块
local tcp_server_receiver = require "tcp_server_receiver"
-- 加载TCP服务器数据发送功能模块
local tcp_server_sender = require "tcp_server_sender"

-- tcp_server_main的任务名
local TASK_NAME = tcp_server_sender.TASK_NAME

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

-- tcp server socket的任务处理函数
local function tcp_server_main_task_func()
    local netc = nil
    local result, param
    local listen_port = 50003 -- tcp server监听的端口号

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

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

        netc = socket.create(socket.dft(), TASK_NAME)
        if not netc then
            log.error("tcp_server_task_func", "socket.create失败")
            goto EXCEPTION_PROC
        end

        socket.debug(netc, true)
        -- 配置socker server 对象为tcp server
        result = socket.config(netc, listen_port)
        -- 如果配置失败
        if not result then
            log.error("tcp_server_task_func", "socket.config失败")
            goto EXCEPTION_PROC
        end

        -- 监听tcp server端口
        result = libnet.listen(TASK_NAME, 0, netc)
        -- 如果监听失败
        if not result then
            log.error("tcp_server_task_func", "监听失败")
            goto EXCEPTION_PROC
        end

        -- 客户端连上了, 发一条数据给客户端
        libnet.tx(TASK_NAME, 0, netc, "hello world")

        -- 数据收发以及网络连接异常事件总处理逻辑
        while true do
            -- 数据接收处理
            if not tcp_server_receiver.proc(netc) then
                log.info("tcp_server_task_func", "tcp_server_receiver.proc error")
                break
            end

            -- 数据发送处理
            if not tcp_server_sender.proc(TASK_NAME, netc) then
                log.info("tcp_server_task_func", "tcp_server_sender.proc error")
                break
            end

            -- 阻塞等待socket.EVENT事件或者15秒钟超时
            result, param = libnet.wait(TASK_NAME, 15000, netc)
            log.info("tcp_server_task_func", "wait result", result, param)

            -- 如果连接异常,则退出循环
            if not result then
                log.info("tcp_server_task_func", "客户端断开")
                break
            end
        end

        -- 出现异常    
        ::EXCEPTION_PROC::

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

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

            -- 释放socket server对象
            socket.release(netc)
            netc = nil  
        end

        -- 等待5秒后,再次尝试创建新的连接
        sys.wait(5000)
    end
end

--创建并且启动一个task
--运行这个task的主函数tcp_server_main_task_func
sys.taskInitEx(tcp_server_main_task_func, TASK_NAME, tcp_server_main_cbfunc)
6.3.2 tcp_server_sender.lua

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

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

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

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

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

本文件的对外接口有 3 个:

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

  2. tcp_server_sender.proc:数据发送应用逻辑处理入口,在 tcp_server_main.lua 中调用;

  3. tcp_server_sender.exception_proc:数据发送应用逻辑异常处理入口,在 tcp_server_main.lua 中调用;

local tcp_server_sender = {}

local libnet = require "libnet"
--[[
数据发送队列,数据结构为:
{
    [1] = {data="send from tag: data1", ip=ip_address1, port=port1, cb=callback_struct1},
    [2] = {data="send from tag: data2", ip=ip_address2, port=port2, cb=callback_struct2},
}
data的内容为带发送方标识前缀的实际数据,必须存在;
ip为目标IP地址,可以不存在;
port为目标端口号,可以不存在;
cb为用户回调函数结构,可以不存在;
]]
local send_queue = {}

-- tcp_server_main的任务名
tcp_server_sender.TASK_NAME = "tcp_server_main"

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

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

@api tcp_server_sender.proc(task_name, socket_server)

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

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

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

@usage
tcp_server_sender.proc("tcp_server_main", socket_server)
]]
function tcp_server_sender.proc(task_name, netc)
    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, netc, send_item.data)

        -- 检查发送结果
        if not result then
            log.error("tcp_server_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_server_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 server连接出现异常时,清空等待发送的数据,并且执行发送方的回调函数
@api tcp_server_sender.exception_proc()

@usage
tcp_server_sender.exception_proc()
]]
function tcp_server_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_server_sender
6.3.3 tcp_server_receiver.lua

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

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

本文件的对外接口有 2 个:

  1. tcp_server_receiver.proc(netc):数据接收应用逻辑处理入口,在 tcp_server_main.lua 中调用;

  2. sys.publish(“RECV_DATA_FROM_CLIENT”, data):

(1) 将接收到的数据通过消息"RECV_DATA_FROM_CLIENT"发布出去;

(2) 需要处理数据的应用功能模块订阅处理此消息即可,本 demo 项目中 uart_app.lua 中订阅处理了本消息;

local tcp_server_receiver = {}

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

--[[
检查socket server是否收到数据,如果收到数据,读取并且处理完所有数据
@api tcp_server_receiver.proc(netc)

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

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

@usage
-- 示例:处理tcp server接收数据
tcp_server_receiver.proc(netc)
]]
function tcp_server_receiver.proc(netc)
    -- 如果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中
        local succ, param = socket.rx(netc, recv_buff)

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

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

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

            log.info("tcp_server_receiver.proc", "recv data", data)

            -- 将数据通过"RECV_DATA_FROM_CLIENT"消息publish出去,给其他应用模块处理
            sys.publish("RECV_DATA_FROM_CLIENT", data)

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

    return true
end

return tcp_server_receiver

6.4 UDP 服务器模块(udp/)

UDP 服务器模块实现了基于 UDP 协议的网络通信功能,支持无连接数据传输、广播通信等核心功能。

6.4.1 udp_server_main.lua

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

  1. 创建一个 udp server,监听指定端口;

  2. 处理通信异常,出现异常后,重新初始化 UDP 服务以恢复正常数据接收;

  3. 调用 udp_server_receiver 和 udp_server_sender 中的外部接口,进行数据收发处理;

local udpsrv = require "udpsrv"

-- 加载UDP服务器数据接收功能模块
local udp_server_receiver = require "udp_server_receiver"
-- 加载UDP服务器数据发送功能模块
local udp_server_sender = require "udp_server_sender"

-- 服务器监听端口
local SERVER_PORT = 50003
-- 服务器主题(用于接收消息)
SERVER_TOPIC = "udp_server"

-- udp server socket的任务处理函数
local function udp_server_main_task_func() 
    local udp_server
    local ret, data, remote_ip, remote_port

    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毫秒超时不要修改的更长;
            -- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
            -- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
            -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
            sys.waitUntil("IP_READY", 1000)
        end

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

        -- 创建UDP服务器对象
        -- 注意:udpsrv.create有3个参数,最后一个参数是网络适配器编号
        udp_server = udpsrv.create(SERVER_PORT, SERVER_TOPIC, socket.dft())

        if not udp_server then
            log.error("udp_server_main_task_func", "udpsrv.create error")
            goto EXCEPTION_PROC
        end

        log.info("udp_server_main_task_func", "UDP server started on port", SERVER_PORT)

        -- 发送一条广播消息,通知端口号为50000的客户端,UDP服务器已启动
        udp_server:send("UDP Server is UP", "255.255.255.255", 50000)

        -- 数据收发以及网络连接异常事件总处理逻辑
        while true do
            -- 数据发送处理
            if not udp_server_sender.proc(udp_server) then
                log.error("udp_server_main_task_func", "udp_server_sender.proc error")
            end

            -- 等待接收数据事件
            ret, data, remote_ip, remote_port = sys.waitUntil(SERVER_TOPIC, 15000)

            if ret then
                -- 判断是否是发送就绪事件(通过 data 内容或 remote_ip 是否为 nil)
                if data == "SEND_READY" and remote_ip == nil then
                    -- 这是发送就绪事件,无需处理接收数据,直接继续循环以发送数据
                    log.info("udp_server_main_task_func", "send ready event received")
                -- 网络异常事件
                elseif data == "SOCKET_CLOSED" then
                    goto EXCEPTION_PROC
                else
                    -- 真实接收到的数据
                    if not udp_server_receiver.proc(data, remote_ip, remote_port) then
                        log.error("udp_server_main_task_func", "udp_server_receiver.proc error")
                    end
                end
            else
                -- 超时,发送一条心跳广播
                log.info("udp_server_main_task_func", "No data received, sending broadcast heartbeat")
                udp_server:send("UDP Server Heartbeat", "255.255.255.255", 50000)
            end
        end

        ::EXCEPTION_PROC::

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

        -- 关闭UDP服务器
        if udp_server then
            udp_server:close()
            udp_server = nil
        end

        -- 5秒后跳转到循环体开始位置,重建udp server
        sys.wait(5000)
    end
end

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

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

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

本文件的对外接口有 2 个:

  1. udp_server_receiver.proc(socket_server):数据接收应用逻辑处理入口,在 udp_server_main.lua 中调用;

  2. sys.publish(“RECV_DATA_FROM_CLIENT”, data, remote_ip, remote_port):

(1) 将接收到的数据通过消息"RECV_DATA_FROM_CLIENT"发布出去;

(2) 需要处理数据的应用功能模块订阅处理此消息即可;

--[[
@module  udp_server_receiver
@summary udp server socket数据接收应用功能模块 
@version 1.0
@date    2025.09.16
@author  王世豪
@usage
本文件为udp server socket数据接收应用功能模块,核心业务逻辑为:
从内核读取接收到的数据,然后将数据发送给其他应用功能模块做进一步处理;

本文件的对外接口有2个:
1、udp_server_receiver.proc(socket_server):数据接收应用逻辑处理入口,在udp_server_main.lua中调用;
2、sys.publish("RECV_DATA_FROM_CLIENT", data, remote_ip, remote_port):
    将接收到的数据通过消息"RECV_DATA_FROM_CLIENT"发布出去;
    需要处理数据的应用功能模块订阅处理此消息即可;
]]

local udp_server_receiver = {}

-- 客户端信息
local client_info = {}

-- 获取客户端信息
function udp_server_receiver.get_client_info()
    return client_info
end

-- 重置客户端信息
function udp_server_receiver.reset_client_info()
    client_info.ip = nil
    client_info.port = nil
end

-- 初始化客户端信息
udp_server_receiver.reset_client_info()

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

@api udp_server_receiver.proc(data, remote_ip, remote_port)

@param1 data string
表示接收到的数据;

@param2 remote_ip string
表示发送数据的client的IP地址;

@param3 remote_port number
表示发送数据的client的端口号;

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

@usage
udp_server_receiver.proc(data, remote_ip, remote_port)
]]
function udp_server_receiver.proc(data, remote_ip, remote_port)
    log.info("udp_server_receiver.proc", "收到数据", data, "来自", remote_ip, remote_port)

    client_info.ip = remote_ip
    client_info.port = remote_port

    log.info("client_info", client_info.ip, client_info.port)

    -- 将接收到的数据通过消息发布出去
    sys.publish("RECV_DATA_FROM_CLIENT", data, remote_ip, remote_port)

    return true
end

return udp_server_receiver
6.4.3 udp_server_sender.lua

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

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

  2. udp_server_main 主任务调用 udp_server_sender.proc 接口,遍历队列 send_queue,逐条发送数据到 client;

  3. udp server socket 如果出现异常,udp_server_main 主任务调用 udp_server_sender.exception_proc 接口,丢弃掉队列 send_queue 中未发送的数据;

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

本文件的对外接口有 3 个:

  1. sys.subscribe(“SEND_DATA_REQ”, send_data_req_proc_func):订阅"SEND_DATA_REQ"消息;其他应用模块如果需要发送数据,直接 sys.publish 这个消息即可,将需要发送的数据、目标 IP、目标端口以及回调函数和回调参数一起 publish 出去;

  2. udp_server_sender.proc:数据发送应用逻辑处理入口,在 udp_server_main.lua 中调用;

  3. udp_server_sender.exception_proc:数据发送应用逻辑异常处理入口,在 udp_server_main.lua 中调用;

local udp_server_sender = {}

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

-- "SEND_DATA_REQ"消息的处理函数
local function send_data_req_proc_func(tag, data, ip, port, cb)
    -- 将原始数据增加前缀,然后插入到发送队列send_queue中
    table.insert(send_queue, {data="send from "..tag..": "..data, ip=ip, port=port, cb=cb}) 
    log.info("send_queue", #send_queue)
    -- 通知主任务:有数据待发送,唤醒阻塞
    sys.publish("udp_server", "SEND_READY", nil, nil)  -- 后两个参数为 remote_ip 和 remote_port,这里置为 nil
end

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

@api udp_server_sender.proc(udp_server)

@param 
表示由udpsrv.create接口创建的udp_server对象;
必须传入,不允许为空或者nil;

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

@usage
udp_server_sender.proc(udp_server)
]]
function udp_server_sender.proc(udp_server)
    local send_item
    local result

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

        result = udp_server:send(send_item.data, send_item.ip, send_item.port)

        -- 发送失败
        if not result then
            log.error("udp_server_sender.proc", "udp_server:send error")

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

            return false
        end

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

    return true
end

-- UDP服务器出现异常时,清空等待发送的数据,并且执行发送方的回调函数
function udp_server_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出去;
-- 参数格式: sys.publish("SEND_DATA_REQ", tag, data, ip, port, cb)
-- tag: 发送方标识, data: 要发送的数据, ip: 目标IP, port: 目标端口, cb: 回调函数
-- 例如: sys.publish("SEND_DATA_REQ", "app1", "hello client", "192.168.1.100", 50000)
-- 本demo项目中uart_app.lua和timer_app.lua中publish了这个消息;
sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)

return udp_server_sender

6.5 应用功能(timer_app.lua, uart_app.lua)

应用功能模块负责生成测试数据和处理串口通信。

6.5.1 定时器应用(timer_app.lua)

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

创建一个 5 秒的循环定时器,每次产生一段数据,通知 TCP 或 UDP server 进行处理;

本文件的对外接口有一个:

sys.publish(“SEND_DATA_REQ”, “timer”, data, ip, port, {func=send_data_cbfunc, para=“timer”…data}),通过 publish 通知 TCP 或 UDP server 数据发送功能模块发送 data 数据,数据发送结果通过执行回调函数 send_data_cbfunc 通知本功能模块;

local config = {
    enable_udp = true,            -- 是否启用UDP发送
    enable_tcp = false             -- 是否启用TCP发送
}

local data = 1

local udp_server_receiver = require "udp_server_receiver"

-- 数据发送结果回调函数
-- result:发送结果,true为发送成功,false为发送失败
-- para:回调参数,sys.publish("SEND_DATA_REQ", "timer", data, ip, port, {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为要发送的原始数据
    -- 携带的第三个参数client_ip为目标IP地址
    -- 携带的第四个参数port为目标端口号
    -- 携带的第五个参数cb为发送结果回调(可以为空,如果为空,表示不关心TCP或UDP server发送数据成功还是失败),其中:
    --       cb.func为回调函数(可以为空,如果为空,表示不关心TCP或UDP server发送数据成功还是失败)
    --       cb.para为回调函数的第二个参数(可以为空),回调函数的第一个参数为发送结果(true表示成功,false表示失败)

    -- UDP发送处理
    if config.enable_udp then
        -- 获取客户端信息
        local client_info = udp_server_receiver.get_client_info()

        -- 检查是否有客户端IP和端口
        if client_info.ip and client_info.port then
            -- 使用记录的客户端信息发送
            sys.publish("SEND_DATA_REQ", "timer", data, client_info.ip, client_info.port, {func=send_data_cbfunc, para="udp_timer"..data})
        else
            -- 未收到过客户端数据,提示错误
            log.error("timer_app", "尚未收到客户端数据, 无法确定目标IP和端口")
            sys.timerStart(send_data_req_timer_cbfunc, 5000)
        end
        -- TCP发送处理
    elseif config.enable_tcp then
        -- 当前TCP server与client是一对一连接,publish的消息可忽略ip和port参数
        sys.publish("SEND_DATA_REQ", "timer", data, {func=send_data_cbfunc, para="tcp_timer"..data})  
    end

    data = data + 1
    log.info("send_data_req_timer_cbfunc", data)
end

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

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

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

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

  3. 从 uart1 接收到 pc 端串口工具发送的数据后,通知 TCP 或 UDP server 进行处理;

  4. 收到 TCP 或 UDP server 从 client 接收到的数据后,将数据通过 uart1 发送到 pc 端串口工具;

本文件的对外接口有两个:

  1. sys.publish(“SEND_DATA_REQ”, “uart”, read_buf, client_ip, port),通过 publish 通知 TCP 或 UDP server 数据发送功能模块发送 read_buf 数据,不关心数据发送成功还是失败;

  2. sys.subscribe(“RECV_DATA_FROM_SERVER”, recv_data_from_server_proc),订阅 RECV_DATA_FROM_SERVER 消息,处理消息携带的数据;

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

local config = {
    enable_udp = true,              -- 是否启用UDP发送
    enable_tcp = false               -- 是否启用TCP发送
}

-- 加载UDP服务器数据接收功能模块
local udp_server_receiver = require "udp_server_receiver"

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

local function concat_timeout_func()
    -- 如果存在尚未处理的串口缓冲区数据;
    -- 将数据通过publish通知其他应用功能模块处理;
    -- 然后清空本文件的串口缓冲区数据
    if read_buf:len() > 0 then
        if config.enable_udp then
            -- 获取客户端信息
            local client_info = udp_server_receiver.get_client_info()
            -- 检查是否有客户端IP和端口
            if client_info.ip and client_info.port then
                -- 使用记录的客户端信息
                sys.publish("SEND_DATA_REQ", "uart", read_buf, client_info.ip, client_info.port)
            else
                -- 未收到过客户端数据,提示错误
                log.error("uart_app", "尚未收到客户端数据,无法确定目标IP和端口")
            end
        elseif config.enable_tcp then
            -- 当前TCP server与client是一对一连接,publish的消息可忽略ip和port参数
            sys.publish("SEND_DATA_REQ", "uart", read_buf)
        end

        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_CLIENT"消息的处理函数recv_data_from_client_proc
-- 收到"RECV_DATA_FROM_CLIENT"消息后,会执行函数recv_data_from_client_proc   
sys.subscribe("RECV_DATA_FROM_CLIENT", recv_data_from_client_proc)

七、演示功能

7.1 通过 WIFI STA 网卡实现 TCP/UDP Server 通信

注意:

  1. 如果需要单 WIFI STA 网卡,打开 require “netdrv_wifi_sta”,其余注释掉;

  2. 同时 netdrv_wifi_sta.lua 中的 wlan.connect(“茶室-降功耗,找合宙!”, “Air123456”, 1),前两个参数,修改为自己测试时 wifi 热点的名称和密码;注意:仅支持 2.4G 的 wifi,不支持 5G 的 wifi;

  3. PC 端通过 SSCOM 启动一个客户端,需要注意 PC 端要和 Air8000 开发板连接同一个 wifi 热点;

切换网卡为 WIFI STA 网卡:

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

luatools 日志打印:

在 Luatools 的日志输出中,可以查找到设备通过 WiFi 连接获取的 IP 地址。

TCP 服务器会在设备的网络接口上监听端口,接受客户端的连接请求;

而 UDP 服务器同样在该网络接口上监听端口接收数据。客户端需要使用服务器的 IP 地址和端口号来建立 TCP 连接或发送 UDP 数据包。

TCP 服务端和客户端的数据发送与接收:

TCP 客户端收发数据日志:

串口端收发数据日志:

UDP 服务端和客户端的数据发送与接收:

注意:

1、当 client 端向 server 端发送数据时,server 端会记录 client 端的 ip 和 port,然后通过定时器应用向 client 端发送数据。

2、如果连接断开或者还不知道 client 的 ip 和 port,timer app 并不确定发将数据发送给谁,所以此时 luatools 日志会打印:“尚未收到客户端数据, 无法确定目标 IP 和端口”。

UDP 客户端收发数据日志:

串口端收发数据日志:

7.2 通过 WIFI AP 网卡实现 TCP/UDP Server 通信

注意:

  1. 如果需要单 WIFI AP 网卡,打开 require "netdrv_wifi_ap,其余注释掉;

  2. 同时 netdrv_wifi_ap.lua 中的 wlan.createAP(“LuatOS” … mobile.imei(), “12345678”),表示创建 wifi 的名称和密码,根据自己需求改动即可;

  3. PC 端通过 SSCOM 启动一个客户端,需要注意 PC 端要连接上 Air8000 开发板生成的 AP 热点;

切换网卡为 WIFI AP 网卡:

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

luatools 日志打印:

在 Luatools 的日志输出中,可以查找到设备的 WiFi AP 网卡信息。WiFi AP 模式下,设备会配置一个固定的 IP 地址(通常为 192.168.4.1)作为接入点。

TCP 服务器会在 WiFi AP 网络接口上监听端口,接受连接到该 WiFi 网络的客户端连接请求;

而 UDP 服务器同样在该网络接口上监听端口接收数据。客户端需要先连接到设备创建的 WiFi 网络,然后使用服务器的固定 IP 地址(192.168.4.1)和端口号来建立 TCP 连接或发送 UDP 数据包。

TCP 服务端和客户端的数据发送与接收:

TCP 客户端收发数据日志:

串口端收发数据日志:

UDP 服务端和客户端的数据发送与接收:

注意:

1、当 client 端向 server 端发送数据时,server 端会记录 client 端的 ip 和 port,然后通过定时器应用向 client 端发送数据。

2、如果连接断开或者还不知道 client 的 ip 和 port,timer app 并不确定发将数据发送给谁,所以此时 luatools 日志会打印:“尚未收到客户端数据, 无法确定目标 IP 和端口”。

UDP 客户端收发数据日志:

串口端收发数据日志:

7.3 通过以太网卡实现 TCP/UDP Server 通信

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

切换网卡为以太网卡:

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

luatools 日志打印:

在 Luatools 的日志输出中,可以查找到设备通过以太网卡获取的 IP 地址。以太网卡(如 CH390H)通常通过 SPI 接口与模块连接,其 IP 地址可配置为静态 IP 或通过 DHCP 自动获取。

TCP 服务器会在以太网卡对应的网络接口上监听端口,接受局域网内客户端的连接请求;

而 UDP 服务器同样在该网络接口上监听端口接收数据。客户端需要使用以太网卡分配的 IP 地址和端口号来建立 TCP 连接或发送 UDP 数据包。

TCP 服务端和客户端的数据发送与接收:

TCP 客户端收发数据日志:

串口端收发数据日志:

UDP 服务端和客户端的数据发送与接收:

注意:

1、当 client 端向 server 端发送数据时,server 端会记录 client 端的 ip 和 port,然后通过定时器应用向 client 端发送数据。

2、如果连接断开或者还不知道 client 的 ip 和 port,timer app 并不确定发将数据发送给谁,所以此时 luatools 日志会打印:“尚未收到客户端数据, 无法确定目标 IP 和端口”。

UDP 客户端收发数据日志:

串口端收发数据日志:

八、总结

LuatOS 支持 socket server,包括 tcp server,udp server,http server,ftp server(虽然 ftp server 还没实现),本文仅仅描述了 tcp server 和 udp server 的实现细节。

LuatOS 的 socket server,支持 4G,WiFi,以太网这三种不同的网卡承载,可以根据实际需要选择合适的网卡做 socket 的承载。

同时,LuatOS 的 tcp server 目前仅支持一对一连接,UDP 协议本身是无连接的,这意味着任何在同一局域网下的客户端都可以向 udp server 的 IP 和端口发送数据包。这点在开发 socket server 嵌入式应用的时候,务必注意。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐