RT-Thread 中 lwIP 2.0.3 网络应用(ping 与 TFTP 服务器)的移植与实战指南
RT-Thread 中 lwIP 2.0.3 网络应用(ping 与 TFTP 服务器)的移植与实战指南
本指南围绕 RT-Thread 官方在 lwIP 2.0.3 源码树中维护的 apps 网络应用集合展开,重点剖析其中 RT-Thread 开发团队亲自移植、并可直接在 finsh/msh 命令行下使用的两个核心工具:ping 与 tftp。通过阅读本文,你将理解这两个应用在 RT-Thread 中的编译使能方式、源码级工作流程,并能直接在开发板上复现 "ping 通目标主机" 与 "通过 TFTP 上传/下载文件" 的完整实操方案。
一、src/apps 目录:lwIP 官方应用套件与 RT-Thread 的移植范围
在 RT-Thread 的 lwIP 端口代码树中,components/net/lwip/lwip-2.0.3/src/apps/README.md 明确说明:
Some net apps for current version LwIP. The RT-Thread develop team ported apps. It's easy to use it on finsh/msh.
即:该目录下存放的是与当前 lwIP 2.0.3 版本配套的一组网络应用(net apps),RT-Thread 开发团队已完成移植(ported apps),并重点保证它们可以轻松地在 finsh/msh 命令行中使用。原文档特别点名的两个应用是:
- ping —— 基于 ICMP Echo 的网络连通性测试工具;
- tftp —— 基于 RFC 1350 的简单文件传输协议服务器。
从目录结构(components/net/lwip/lwip-2.0.3/src/apps)可以看到,该目录还包含 lwIP 上游的其他标准应用:httpd(内嵌 Web 服务器)、lwiperf(带宽测试)、mdns、mqtt、netbiosns、snmp(含 SNMPv3)与 sntp(NTP 客户端)。本指南以文档点名的 ping 与 tftp 为主线,其余应用仅作背景提及。
RT-Thread 通过 SConscript 构建脚本控制这些应用的编译。在 components/net/lwip/lwip-2.0.3/SConscript 中可以看到:
snmp_src = Glob("src/apps/snmp/*.c"),且仅在使能RT_LWIP_SNMP时加入编译(第 55、69-71 行);if GetDepend(['RT_LWIP_USING_PING']): src += Glob('src/apps/ping/ping.c'),即 ping 应用只在RT_LWIP_USING_PING被打开时参与构建(第 80-81 行);- 所有应用源码的公共头文件搜索路径已包含
src/include等核心目录,应用自身的头文件(如snmp)在对应条件分支中加入(第 62-71 行)。
也就是说,ping 是唯一被 RT-Thread 显式挂入默认构建系统的 apps 应用;TFTP 则默认不编译,需要用户自行把 tftp 目录下的源文件加入工程(详见下文第四节)。
二、ping:在 RT-Thread 命令行中实现主机连通性测试
2.1 功能使能
ping 应用对应 Kconfig 配置项 RT_LWIP_USING_PING,位于 components/net/lwip/Kconfig:
config RT_LWIP_USING_PING
bool "Enable ping features"
default y
select NETDEV_USING_PING
select RT_LWIP_ICMP
select RT_LWIP_RAW
其默认开启,并会自动连带使能 NETDEV_USING_PING(网络设备层 ping)、RT_LWIP_ICMP(ICMP 协议支持)与 RT_LWIP_RAW(原始套接字)。因此打开该配置后,ping.c 中的 #ifdef RT_LWIP_ICMP 编译条件即满足,ping 源码会被 SConscript 纳入编译。
2.2 命令行用法
源码末尾同时导出两种命令行入口(ping.c):
FINSH_FUNCTION_EXPORT(ping, ping network host)—— 供 finsh 使用;MSH_CMD_EXPORT_ALIAS(cmd_ping, ping, ping network host)—— 供 msh 使用,其中cmd_ping包装函数会打印用法提示并以 4 次、默认报文大小调用ping()。
在 msh 下直接输入:
msh />ping 192.168.1.1
该命令等价于调用 ping("192.168.1.1", 4, 0),即默认发送 4 个 ICMP Echo 请求,载荷大小取默认值 PING_DATA_SIZE = 32 字节。若参数个数不足,会输出提示 Please input: ping <host address>。典型输出如下:
msh />ping 192.168.1.1
60 bytes from 192.168.1.1 icmp_seq=0 ttl=64 time=1 ms
60 bytes from 192.168.1.1 icmp_seq=1 ttl=64 time=1 ms
60 bytes from 192.168.1.1 icmp_seq=2 ttl=64 time=1 ms
60 bytes from 192.168.1.1 icmp_seq=3 ttl=64 time=1 ms
其中 time 单位是毫秒,由 RT-Thread 内核 tick(rt_tick_get())换算而来。
2.3 源码级流程解析
ping 的实现位于 components/net/lwip/lwip-2.0.3/src/apps/ping/ping.c,核心流程如下:
- 域名解析:
ping()使用lwip_getaddrinfo()将主机名(如www.rt-thread.org)或点分十进制 IP 解析为地址,失败时打印ping: unknown host %s并返回-RT_ERROR(第 161-176 行); - 创建原始套接字:
lwip_socket(AF_INET, SOCK_RAW, IP_PROTO_ICMP)创建 ICMP 原始套接字,并设置接收超时SO_RCVTIMEO(第 177-184 行),超时值由PING_RCV_TIMEO (2 * RT_TICK_PER_SECOND)决定,即 2 个系统 tick 周期; - 发送报文:
lwip_ping_send()内部由ping_prepare_echo()构造 ICMP Echo 报文——类型设为ICMP_ECHO、代码为 0、标识符固定为PING_ID 0xAFAF、序号htons(++ping_seq_num)递增,载荷填充 0~31 的字节序列;校验和通过inet_chksum()计算,若使能了硬件校验和(RT_LWIP_USING_HW_CHECKSUM)则交由硬件处理(第 50-106 行); - 接收应答:
lwip_ping_recv()在lwip_recvfrom()循环中校验 IP 头之后的 ICMP Echo Reply 是否与本机PING_ID及当前序号匹配,并从中提取 TTL(iphdr->_ttl)返回(第 108-131 行); - 统计输出:每条回复打印
%d bytes from %s icmp_seq=%d ttl=%d time=%d ms;未收到则打印From %s icmp_seq=%d timeout;发送间隔由PING_DELAY (1 * RT_TICK_PER_SECOND)控制(第 186-217 行)。
该实现依赖 RT-Thread 的 POSIX socket 层(lwip_socket/lwip_sendto/lwip_recvfrom),因此也要求工程使能 SAL(Socket 抽象层)或直接使用 lwIP 原生 socket API。
三、tftp:基于 RFC 1350 的轻量文件传输服务器
3.1 源码构成与架构
tftp 应用由两个文件组成:
- tftp_server.c:lwIP 上游的 TFTP 服务器实现,使用 lwIP raw API(
udp_new_ip_type、udp_bind、udp_recv),完全基于回调驱动,不依赖操作系统线程; - tftp_port.c:RT-Thread 开发团队编写的移植层(首个版本由 armink 于 2017-08-17 提交),将 lwIP 的文件读写回调接到 RT-Thread 的 POSIX 文件系统(
open/read/write/close)上。
协议本身遵循 RFC 1350(Trivial File Transfer Protocol),实现的关键常量(tftp_server.c):
| 常量 | 值 | 含义 |
|---|---|---|
TFTP_MAX_PAYLOAD_SIZE |
512 | 单块数据最大载荷(RFC 1350 规定的数据块上限) |
TFTP_HEADER_LENGTH |
4 | 2 字节操作码 + 2 字节块号 |
TFTP_RRQ/WRQ/DATA/ACK/ERROR |
1/2/3/4/5 | 读请求 / 写请求 / 数据 / 确认 / 错误 |
| 错误码 1~7 | — | 文件未找到、访问违规、磁盘满、非法操作、未知传输 ID、文件已存在、无此用户 |
3.2 与 RT-Thread 文件系统的对接(tftp_port.c)
RT-Thread 的移植层通过 struct tftp_context 回调集合把服务器与具体文件系统解耦(tftp_port.c):
static void* tftp_open(const char* fname, const char* mode, u8_t write)
{
int fd = -1;
if (!rt_strcmp(mode, "octet"))
{
if (write)
{
fd = open(fname, O_WRONLY | O_CREAT, 0);
}
else
{
fd = open(fname, O_RDONLY, 0);
}
}
else
{
rt_kprintf("tftp: No support this mode(%s).", mode);
}
return (void *) fd;
}
static int tftp_write(void* handle, struct pbuf* p)
{
int fd = (int) handle;
return write(fd, p->payload, p->len);
}
要点:
- 传输模式:目前仅支持
octet(二进制)模式,netascii等其他模式会打印tftp: No support this mode(%s).并返回无效句柄; - 读写映射:
write请求时以O_WRONLY | O_CREAT打开文件(覆盖写),读请求时以O_RDONLY打开; - 其余回调:
ctx.close = close、ctx.read = read直接复用 RT-Thread libc/POSIX 的标准函数,ctx.write = tftp_write则将 lwIP 的struct pbuf载荷直接写入文件描述符。
服务器启动函数 tftp_server() 依次填充四个回调、调用 tftp_init(&ctx),成功后打印 TFTP server start successfully.,失败打印 TFTP server start failed.。
3.3 服务器状态机与超时重传
tftp_server.c 用全局 struct tftp_state 维护单一连接状态,并明确限制同一时刻只支持一个连接——当收到来自不同端口/地址的报文时,会回送错误 2(Only one connection at a time is supported)。其状态机(recv() 回调,第 203-358 行):
- RRQ/WRQ(读/写请求):解析以
\0结尾的文件名与模式字符串,调用ctx->open();WRQ 回送ACK(0)作为握手,RRQ 则立即发送第一块数据; - DATA(数据块):仅接受写连接,剥去 4 字节头部后调用
ctx->write(),随后回送ACK(blknum);当数据块小于 512 字节时判定传输结束并关闭句柄; - ACK(确认):仅接受读连接,校验块号后递增
blknum继续发送下一块,若上一块不足 512 字节则结束传输; - 错误处理:文件名过长/未以
\0终止、模式非法、块号不匹配、未知操作码均会回送对应错误码。
可靠性机制由 tftp_tmr() 定时回调实现(第 360-383 行):每 TFTP_TIMER_MSECS 触发一次,若超时(超过 TFTP_TIMEOUT_MSECS)且重传次数未达 TFTP_MAX_RETRIES,则重发 last_data,否则关闭连接。
3.4 启动服务器
与 ping 相同,tftp_server 也同时注册了 finsh 与 msh 命令(tftp_port.c):
FINSH_FUNCTION_EXPORT(tftp_server, start tftp server.);
MSH_CMD_EXPORT(tftp_server, start tftp server.);
在 msh 下输入:
msh />tftp_server
TFTP server start successfully.
服务器随即监听 UDP 69 号端口(TFTP_PORT),等待 PC 端 TFTP 客户端连接。在 PC 上即可执行:
# 将本地文件上传到开发板
tftp 192.168.1.10 -m binary -c put firmware.bin
# 从开发板下载文件
tftp 192.168.1.10 -m binary -c get log.txt
需要注意的是,tftp 应用默认不随 SConscript 自动编译,需将 tftp 目录下的 tftp_server.c 与 tftp_port.c 加入工程,并确保头文件路径包含 lwip/apps/tftp_server.h 所在目录;同时工程必须使能 lwIP 的 UDP(LWIP_UDP)与 RT-Thread DFS 文件系统(提供 open/read/write/close 及 unistd.h、sys/stat.h)。
四、RT-Thread 移植层的定位与边界
从实现可以归纳出 RT-Thread 移植这两个应用的技术策略:
- 应用逻辑零改动:
ping.c、tftp_server.c均为 lwIP 上游代码,RT-Thread 未改动其协议逻辑; - 入口统一到 finsh/msh:通过
FINSH_FUNCTION_EXPORT/MSH_CMD_EXPORT宏把 C 函数注册为命令行工具,实现文档所述 "easy to use it on finsh/msh"; - 系统能力通过标准接口注入:ping 使用 RT-Thread 内核 tick 与 POSIX socket;tftp 通过
tftp_context回调对接 DFS 文件系统,使服务器与具体存储介质无关; - 构建条件与 Kconfig 联动:ping 通过
RT_LWIP_USING_PING联动RT_LWIP_ICMP、RT_LWIP_RAW、NETDEV_USING_PING,保证依赖完整闭合。
五、延伸阅读
- lwIP 应用源码总览:components/net/lwip/lwip-2.0.3/src/apps
- ping 完整实现:components/net/lwip/lwip-2.0.3/src/apps/ping/ping.c
- TFTP 服务器协议实现:components/net/lwip/lwip-2.0.3/src/apps/tftp/tftp_server.c
- TFTP 的 RT-Thread 文件系统移植层:components/net/lwip/lwip-2.0.3/src/apps/tftp/tftp_port.c
- lwIP 网络栈功能开关(含
RT_LWIP_USING_PING):components/net/lwip/Kconfig - lwIP 构建脚本(
RT_LWIP_USING_PING与 SNMP 的条件编译):components/net/lwip/lwip-2.0.3/SConscript