02 TCP&UDP server
作者:王世豪 | 最后修改:2026-04-13
一、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 和端口发送数据包。
二、演示功能概述
本文档所演示的功能如下:
-
创建 TCP/UDP 服务器:在项目目录中对应两个文件夹。
-
TCP 文件夹:功能为创建一个 TCP 服务器,等待 TCP 客户端连接。
-
UDP 文件夹:功能为创建一个 UDP 服务器,等待 UDP 客户端连接。
-
服务器数据发送逻辑:当 TCP/UDP 服务器与客户端成功建立连接后,服务器将按照以下逻辑向客户端发送数据:
-
通过串口应用功能模块
uart_app.lua,从 UART1 接收串口数据,并为数据增加send from uart:前缀后发送给客户端。 -
通过定时器应用功能模块
timer_app.lua,定时产生数据,并为数据增加send from timer:前缀后发送给客户端。 -
网络驱动配置:通过
netdrv_device模块配置 TCP/UDP 局域网通信所使用的网卡。目前支持以下三种选择(三选一): -
netdrv_wifi_sta:WIFI STA 网卡 -
netdrv_wifi_ap:WIFI AP 网卡 -
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 软件环境
在开始实践本示例之前,先筹备一下软件环境:
-
烧录工具:Luatools 工具;
-
本demo开发测试时使用的固件为LuatOS-SoC_V2018_Air8000,本demo对固件版本没有什么特殊要求,所以你如果要测试本demo时,可以直接使用最新版本的内核固件;如果发现最新版本的内核固件测试有问题,可以使用我们开发本demo时使用的内核固件版本来对比测试;
-
脚本文件:点击此处查看;
-
网络调试工具:SSCOM (可以模拟 TCP/UDP 客户端);
-
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 主应用功能模块,核心业务逻辑为:
-
创建一个 tcp server ,等待 client 连接;
-
处理连接异常,出现异常后,关闭当前连接,等待下一个 client 连接;
-
调用 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 数据发送应用功能模块,核心业务逻辑为:
-
sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)订阅"SEND_DATA_REQ"消息,将其他应用模块需要发送的数据存储到队列 send_queue 中;
-
tcp_server_main 主任务调用 tcp_server_sender.proc 接口,遍历队列 send_queue,逐条发送数据到 server;
-
tcp server socket 和 client 之间的连接如果出现异常,tcp_server_main 主任务调用tcp_server_sender.exception_proc 接口,丢弃掉队列 send_queue 中未发送的数据;
-
任何一条数据无论发送成功还是失败,只要这条数据有回调函数,都会通过回调函数通知数据发送方;
本文件的对外接口有 3 个:
-
sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func):订阅"SEND_DATA_REQ"消息;其他应用模块如果需要发送数据,直接 sys.publish 这个消息即可,将需要发送的数据以及回调函数和回调参数一起 publish 出去;本 demo 项目中 uart_app.lua 和 timer_app.lua 中 publish 了这个消息;
-
tcp_server_sender.proc:数据发送应用逻辑处理入口,在 tcp_server_main.lua 中调用;
-
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 个:
-
tcp_server_receiver.proc(netc):数据接收应用逻辑处理入口,在 tcp_server_main.lua 中调用;
-
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 主应用功能模块,核心业务逻辑为:
-
创建一个 udp server,监听指定端口;
-
处理通信异常,出现异常后,重新初始化 UDP 服务以恢复正常数据接收;
-
调用 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 个:
-
udp_server_receiver.proc(socket_server):数据接收应用逻辑处理入口,在 udp_server_main.lua 中调用;
-
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 数据发送应用功能模块,核心业务逻辑为:
-
sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)订阅"SEND_DATA_REQ"消息,将其他应用模块需要发送的数据存储到队列 send_queue 中;
-
udp_server_main 主任务调用 udp_server_sender.proc 接口,遍历队列 send_queue,逐条发送数据到 client;
-
udp server socket 如果出现异常,udp_server_main 主任务调用 udp_server_sender.exception_proc 接口,丢弃掉队列 send_queue 中未发送的数据;
-
任何一条数据无论发送成功还是失败,只要这条数据有回调函数,都会通过回调函数通知数据发送方;
本文件的对外接口有 3 个:
-
sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func):订阅"SEND_DATA_REQ"消息;其他应用模块如果需要发送数据,直接 sys.publish 这个消息即可,将需要发送的数据、目标 IP、目标端口以及回调函数和回调参数一起 publish 出去;
-
udp_server_sender.proc:数据发送应用逻辑处理入口,在 udp_server_main.lua 中调用;
-
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)
本文件为串口应用功能模块,核心业务逻辑为:
-
打开 uart1,波特率 115200,数据位 8,停止位 1,无奇偶校验位;
-
uart1 和 pc 端的串口工具相连;
-
从 uart1 接收到 pc 端串口工具发送的数据后,通知 TCP 或 UDP server 进行处理;
-
收到 TCP 或 UDP server 从 client 接收到的数据后,将数据通过 uart1 发送到 pc 端串口工具;
本文件的对外接口有两个:
-
sys.publish("SEND_DATA_REQ", "uart", read_buf, client_ip, port),通过 publish 通知 TCP 或 UDP server 数据发送功能模块发送 read_buf 数据,不关心数据发送成功还是失败;
-
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 通信
注意:
如果需要单 WIFI STA 网卡,打开 require "netdrv_wifi_sta",其余注释掉;
同时 netdrv_wifi_sta.lua 中的 wlan.connect("茶室-降功耗,找合宙!", "Air123456", 1),前两个参数,修改为自己测试时 wifi 热点的名称和密码;注意:仅支持 2.4G 的 wifi,不支持 5G 的 wifi;
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 通信
注意:
如果需要单 WIFI AP 网卡,打开 require "netdrv_wifi_ap,其余注释掉;
同时 netdrv_wifi_ap.lua 中的 wlan.createAP("LuatOS" .. mobile.imei(), "12345678"),表示创建 wifi 的名称和密码,根据自己需求改动即可;
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 嵌入式应用的时候,务必注意。