第四节:UDP通讯——无连接的艺术

说到UDP,很多刚入行的朋友第一反应就是“不可靠”。嗯,这话没错,但也不全对。我做了十几年网络通讯,UDP其实是个很有意思的协议——它把选择权交给你,让你自己决定“可靠性”的尺度。

核心要点:UDP是面向无连接的传输层协议。它不建立连接,不保证顺序,不重传丢包。说白了,就是“发了就不管了”。

4.1 UDP的特点——为什么它还在被广泛使用?

你想想看,TCP那么可靠,为什么还要用UDP?我在项目中遇到过很多场景,比如视频监控、游戏同步、传感器数据采集——这些场景里,实时性比可靠性更重要。你愿意等一帧画面重传,还是愿意看到画面稍微花一下但保持流畅?

特性 UDP TCP
连接建立 无连接 三次握手
可靠性 不保证 保证
传输效率 较低
数据边界 保留消息边界 流式传输
适用场景 实时音视频、广播 文件传输、网页

我个人习惯把UDP比作“寄明信片”——你写好地址扔进邮筒,至于对方收不收得到,邮局不负责。但正因为没有那么多确认和重传的流程,UDP的延迟可以做到非常低。

实战经验:我曾经在一个工业控制项目中,用UDP传输传感器数据。每秒2000个数据包,每个包只有几十字节。如果用TCP,光握手和确认就能把CPU吃满。换成UDP后,CPU占用率从85%降到了12%。

4.2 UDP客户端实现——最简单的网络通讯

UDP客户端其实就三步:创建套接字、发送数据、接收数据。没有connect,没有listen,干净利落。

// UDP客户端示例(C语言)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char buffer[1024] = "Hello, UDP Server!";
    
    // 1. 创建套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    
    // 2. 设置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8888);
    server_addr.sin_addr.s_addr = inet_addr("192.168.1.100");
    
    // 3. 发送数据
    sendto(sockfd, buffer, strlen(buffer), 0,
           (struct sockaddr*)&server_addr, sizeof(server_addr));
    
    // 4. 接收响应(可选)
    recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL, NULL);
    
    close(sockfd);
    return 0;
}

嗯,这里要注意:sendto函数每次都要指定目标地址。为什么?因为UDP没有连接,内核不知道你要发给谁。我刚开始学的时候也犯过这个错——以为发过一次就不用再指定地址了。

4.3 UDP服务端实现——被动接收的艺术

服务端更简单:绑定端口,然后收数据。不需要accept,因为UDP没有连接的概念。

// UDP服务端示例(C语言)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

int main() {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    char buffer[1024];
    
    // 1. 创建套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    
    // 2. 绑定地址和端口
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8888);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    
    bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    
    // 3. 循环接收数据
    while(1) {
        memset(buffer, 0, sizeof(buffer));
        recvfrom(sockfd, buffer, sizeof(buffer), 0,
                 (struct sockaddr*)&client_addr, &addr_len);
        printf("收到来自 %s:%d 的消息: %s\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port),
               buffer);
    }
    
    close(sockfd);
    return 0;
}

避坑指南:我曾经在一个项目中,服务端用UDP接收数据,客户端发得快了,服务端来不及处理,结果缓冲区溢出丢包。解决方案有两个:要么加大接收缓冲区(setsockopt设置SO_RCVBUF),要么在应用层做流控。我个人建议后者——缓冲区再大也有上限。

4.4 UDP广播与组播——一对多的通讯模式

这是UDP最强大的特性之一。TCP做不到广播,因为TCP是一对一的连接。而UDP天生支持一对多。

4.4.1 广播——发给所有人

广播地址通常是xxx.xxx.xxx.255,或者255.255.255.255。你想想看,在一个局域网里,你发一个广播包,所有设备都能收到。这在设备发现、服务宣告等场景非常有用。

// 广播发送示例
int broadcast = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));

// 目标地址设为广播地址
server_addr.sin_addr.s_addr = inet_addr("192.168.1.255");
server_addr.sin_port = htons(9999);

sendto(sockfd, buffer, len, 0, 
       (struct sockaddr*)&server_addr, sizeof(server_addr));

实战经验:我在做智能家居项目时,用UDP广播做设备发现。新设备上电后,发一个广播包“我是谁,我在哪”,网关收到后回复配置信息。整个过程不到100毫秒,用户体验非常好。

4.4.2 组播——发给特定群体

广播有个问题:它会打扰到所有设备。组播就优雅多了——只有加入特定组播组的设备才会收到数据。组播地址范围是224.0.0.0到239.255.255.255。

// 加入组播组
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.1");
mreq.imr_interface.s_addr = INADDR_ANY;
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));

// 发送组播数据
server_addr.sin_addr.s_addr = inet_addr("224.0.0.1");
sendto(sockfd, buffer, len, 0,
       (struct sockaddr*)&server_addr, sizeof(server_addr));

我记得有一次做视频会议系统,用组播实现了多路视频分发。每个参会者加入一个组播组,摄像头数据通过组播发送,接收端只解码自己需要的流。相比TCP的点对点转发,网络带宽节省了70%以上。

UDP通讯模式对比 单播 A B 一对一通讯 目标明确 最常用模式 广播 A 一对所有 局域网内全发 可能打扰无关设备 组播 A 一对特定群体 需加入组播组 高效、精准

总结一下:UDP虽然“不可靠”,但它的低延迟、无连接、支持广播组播的特性,让它在实时通讯领域无可替代。记住一句话:用UDP,你要自己负责可靠性——比如在应用层加序列号、超时重传、去重等机制。我见过太多人直接拿UDP当TCP用,结果丢包了还一脸懵。

专注资料整理