第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,让系统自动选择。但我建议你显式指定,代码可读性更好。

小提示: 在Zephyr中,socket()返回的是非负整数。如果返回负数,别急着查errno,先看看你的网络接口是不是初始化了。我曾经有一次忘了调用 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);
}
注意: 客户端不调用bind()时,系统会自动分配一个临时端口。这个端口每次运行都可能不同。如果你需要固定端口,记得显式bind。

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可以加入多个多播组。

实战经验: 我在做设备发现协议时,先用广播发「谁在?」的探测包,所有设备回复自己的IP和功能。然后根据回复,用多播对特定类型的设备下发指令。这样既避免了广播风暴,又实现了灵活分组。

4.4 避坑指南

最后,分享几个我踩过的坑:

  • 缓冲区大小: UDP数据报最大65507字节(减去IP和UDP头部)。但Zephyr的默认接收缓冲区可能只有几KB。发大包时记得调大 SO_RCVBUF
  • 丢包处理: UDP不保证送达。我在项目中会在应用层加一个简单的确认重传机制——发完数据后等ACK,超时没收到就重发。
  • 端口冲突: 同一个端口只能被一个socket绑定。如果程序崩溃后立即重启,可能会报 EADDRINUSE。设置 SO_REUSEADDR 可以解决。
  • 多线程安全: Zephyr的socket API不是线程安全的。多个线程同时操作同一个socket,需要加锁或用消息队列串行化。

好了,UDP这块就聊这么多。说白了,UDP就是「快、简单、不可靠」。在嵌入式场景下,它的优势远大于劣势。下一章咱们聊TCP,那个就复杂多了,要做好心理准备。