第4章:UDP通信实战:UDP Socket API详解、UDP客户端/服务器实现、UDP广播与多播
好,咱们今天聊聊UDP。说实话,在Zephyr的TCP/IP协议栈里,UDP是我用得最多的协议。为什么?因为它简单、轻量、没有连接维护的负担。你想想看,在嵌入式设备上,很多时候我们只需要发个状态、传个心跳,没必要搞TCP那套三次握手的排场。
我个人习惯把UDP比作「寄明信片」——你写好内容,贴上邮票,扔进邮筒,至于对方收没收到,你管不了。这种「尽力而为」的传输方式,在实时性要求高、能容忍少量丢包的场景下,简直是神器。
4.1 UDP Socket API详解
Zephyr的UDP Socket API其实和POSIX标准非常接近。如果你写过Linux下的UDP程序,上手Zephyr基本没有障碍。但有几个细节,我在项目中踩过坑,得跟你说道说道。
4.1.1 创建Socket
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock < 0) {
printk("socket创建失败: %d\n", errno);
return -1;
}
这里注意,SOCK_DGRAM 就是数据报套接字,对应UDP。第三个参数 IPPROTO_UDP 其实可以填0,让系统自动选择。但我建议你显式指定,代码可读性更好。
net_if_up(),结果socket创建死活失败,折腾了半小时。
4.1.2 绑定地址
服务器端必须绑定,客户端通常不需要。但如果你想让客户端从特定端口发数据,也可以绑。
struct sockaddr_in local_addr = {
.sin_family = AF_INET,
.sin_port = htons(8888),
.sin_addr.s_addr = INADDR_ANY
};
int ret = bind(sock, (struct sockaddr *)&local_addr, sizeof(local_addr));
if (ret < 0) {
printk("bind失败: %d\n", errno);
}
嗯,这里要注意 htons() 这个函数。网络字节序是大端,而我们的CPU可能是小端。不转换的话,端口号会变成另一个值。我见过有人把端口设成1234,结果bind到了0x04D2上,排查了半天。
4.1.3 发送与接收
UDP的收发函数是 sendto() 和 recvfrom()。它们比TCP的 send()/recv() 多了目标地址参数。
// 发送
struct sockaddr_in dest_addr = {
.sin_family = AF_INET,
.sin_port = htons(8888),
.sin_addr.s_addr = inet_addr("192.168.1.100")
};
char *msg = "Hello from Zephyr!";
int sent = sendto(sock, msg, strlen(msg), 0,
(struct sockaddr *)&dest_addr, sizeof(dest_addr));
// 接收
char buf[256];
struct sockaddr_in src_addr;
socklen_t addr_len = sizeof(src_addr);
int received = recvfrom(sock, buf, sizeof(buf), 0,
(struct sockaddr *)&src_addr, &addr_len);
说白了,sendto 就是「把数据扔给指定地址」,recvfrom 是「谁给我数据我就收,顺便记下是谁」。这个「顺便记下」很重要,因为UDP是无连接的,你必须知道数据从哪来,才能回复。
4.2 UDP客户端/服务器实现
咱们直接上代码。一个最简单的UDP回显服务器,客户端发什么,服务器就原样返回。
4.2.1 UDP服务器
void udp_server(void)
{
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock < 0) {
printk("socket error\n");
return;
}
struct sockaddr_in server_addr = {
.sin_family = AF_INET,
.sin_port = htons(8888),
.sin_addr.s_addr = INADDR_ANY
};
if (bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
printk("bind error\n");
return;
}
printk("UDP服务器已启动,端口8888\n");
while (1) {
char buf[128];
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int len = recvfrom(sock, buf, sizeof(buf) - 1, 0,
(struct sockaddr *)&client_addr, &addr_len);
if (len < 0) {
printk("recvfrom error\n");
continue;
}
buf[len] = '\0';
printk("收到来自 %s:%d 的消息: %s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buf);
// 原样返回
sendto(sock, buf, len, 0,
(struct sockaddr *)&client_addr, addr_len);
}
}
你看,服务器就是一个死循环,不停地收、回。没有连接、没有状态管理,清爽得很。
4.2.2 UDP客户端
void udp_client(void)
{
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock < 0) {
printk("socket error\n");
return;
}
struct sockaddr_in server_addr = {
.sin_family = AF_INET,
.sin_port = htons(8888),
.sin_addr.s_addr = inet_addr("192.168.1.100")
};
char *msg = "Hello, UDP Server!";
sendto(sock, msg, strlen(msg), 0,
(struct sockaddr *)&server_addr, sizeof(server_addr));
char buf[128];
struct sockaddr_in from_addr;
socklen_t addr_len = sizeof(from_addr);
int len = recvfrom(sock, buf, sizeof(buf) - 1, 0,
(struct sockaddr *)&from_addr, &addr_len);
if (len > 0) {
buf[len] = '\0';
printk("服务器回复: %s\n", buf);
}
close(sock);
}
4.3 UDP广播与多播
这部分是UDP的杀手锏。TCP做不到一对多通信,但UDP可以。我在做智能家居项目时,就用广播来发现设备,用多播来分组控制。
4.3.1 UDP广播
广播就是「吼一嗓子,所有人都能听见」。但有个前提——你得先告诉系统「我要发广播」。
int broadcast_enable = 1;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &broadcast_enable, sizeof(broadcast_enable));
struct sockaddr_in broadcast_addr = {
.sin_family = AF_INET,
.sin_port = htons(9999),
.sin_addr.s_addr = INADDR_BROADCAST // 255.255.255.255
};
sendto(sock, "discover", 8, 0,
(struct sockaddr *)&broadcast_addr, sizeof(broadcast_addr));
这里有个坑:SO_BROADCAST 选项必须设置,否则 sendto() 会返回权限错误。我曾经在调试时忘了这行代码,结果广播包死活发不出去,还以为是网卡驱动有问题。
接收广播很简单,服务器端只要bind到 INADDR_ANY,就能收到发往本网段广播地址的数据包。
4.3.2 UDP多播
多播比广播更优雅。广播是「所有人」,多播是「订阅了某个频道的人」。多播地址范围是224.0.0.0到239.255.255.255。
发送多播和发送普通UDP没区别,只是目标地址换成多播地址:
struct sockaddr_in mcast_addr = {
.sin_family = AF_INET,
.sin_port = htons(7777),
.sin_addr.s_addr = inet_addr("224.0.0.88")
};
sendto(sock, "mcast data", 10, 0,
(struct sockaddr *)&mcast_addr, sizeof(mcast_addr));
接收多播需要加入多播组:
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.88");
mreq.imr_interface.s_addr = INADDR_ANY;
setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
加入多播组后,这个socket就能收到发往224.0.0.88的数据了。注意,一个socket可以加入多个多播组。
4.4 避坑指南
最后,分享几个我踩过的坑:
- 缓冲区大小: UDP数据报最大65507字节(减去IP和UDP头部)。但Zephyr的默认接收缓冲区可能只有几KB。发大包时记得调大
SO_RCVBUF。 - 丢包处理: UDP不保证送达。我在项目中会在应用层加一个简单的确认重传机制——发完数据后等ACK,超时没收到就重发。
- 端口冲突: 同一个端口只能被一个socket绑定。如果程序崩溃后立即重启,可能会报
EADDRINUSE。设置SO_REUSEADDR可以解决。 - 多线程安全: Zephyr的socket API不是线程安全的。多个线程同时操作同一个socket,需要加锁或用消息队列串行化。
好了,UDP这块就聊这么多。说白了,UDP就是「快、简单、不可靠」。在嵌入式场景下,它的优势远大于劣势。下一章咱们聊TCP,那个就复杂多了,要做好心理准备。