UDP通信:无连接的数据报传输

UDP,全称是用户数据报协议。说实话,我第一次接触它的时候,觉得这协议也太「随意」了——发数据前连个招呼都不打。但后来在项目中用多了,才发现这种「随意」恰恰是它的魅力所在。

UDP的特点:简单粗暴,但快

UDP和TCP最大的区别,就是它不建立连接。你想发数据?直接扔出去就行。对方收没收到?不知道。数据顺序乱没乱?也不管。

听起来很不靠谱对吧?但正是这种「不靠谱」,让它拥有了极低的延迟。

UDP的核心特点:

  • 无连接:发送前不需要三次握手,直接发
  • 不可靠:不保证送达,不保证顺序
  • 面向数据报:保留消息边界,一次sendto对应一次recvfrom
  • 无拥塞控制:网络堵了?照发不误
  • 轻量级:头部只有8字节,TCP是20字节

我在项目中遇到过一个问题:用TCP传实时视频流,结果网络一抖动,画面卡得不行。后来换成UDP,虽然偶尔花屏,但整体流畅度提升了一大截。说白了,有些场景下,「快」比「稳」更重要。

UDP客户端/服务器模型

UDP的编程模型比TCP简单得多。不需要listen,不需要accept,甚至连connect都可以省略。

来看一个最基础的UDP服务器:

// UDP服务器端 - echo server
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main() {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    char buffer[1024];
    socklen_t addr_len = sizeof(client_addr);

    // 1. 创建socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    // 2. 绑定地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8888);
    bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));

    // 3. 直接收数据,不需要listen
    while(1) {
        recvfrom(sockfd, buffer, sizeof(buffer), 0,
                 (struct sockaddr*)&client_addr, &addr_len);
        printf("收到: %s\n", buffer);
        sendto(sockfd, buffer, sizeof(buffer), 0,
               (struct sockaddr*)&client_addr, addr_len);
    }
    close(sockfd);
    return 0;
}

客户端更简单:

// UDP客户端
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(8888);

sendto(sockfd, "Hello", 5, 0,
       (struct sockaddr*)&server_addr, sizeof(server_addr));

recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL, NULL);
printf("服务器回复: %s\n", buffer);

嗯,这里要注意:UDP的recvfrom和sendto每次都要指定对方地址。为什么?因为没有连接啊,内核不知道你要跟谁说话。

UDP的广播与组播

这是UDP独有的能力。TCP是点对点的,但UDP可以「一对多」。

广播:吼一嗓子,所有人都能听见

广播地址通常是xxx.xxx.xxx.255。比如局域网是192.168.1.x,广播地址就是192.168.1.255。

我曾经在一个智能家居项目里用过广播。设备上线时发个广播包,网关收到后就知道有新设备了。省去了手动配置IP的麻烦。

// 开启广播选项
int broadcast = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));

// 发送到广播地址
struct sockaddr_in broadcast_addr;
broadcast_addr.sin_family = AF_INET;
broadcast_addr.sin_addr.s_addr = inet_addr("192.168.1.255");
broadcast_addr.sin_port = htons(9999);

sendto(sockfd, "广播消息", 12, 0,
       (struct sockaddr*)&broadcast_addr, sizeof(broadcast_addr));

注意:广播会消耗网络带宽。所有设备都会收到广播包,即使它们不关心。生产环境中别滥用,否则网络管理员会来找你喝茶。

组播:只发给感兴趣的人

组播比广播更优雅。它使用D类IP地址(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));

组播的好处是:精确投递。只有订阅了该组的设备才会收到数据,不会像广播那样打扰所有人。

UDP与TCP的选择:什么时候用哪个?

这个问题没有标准答案。我个人的经验是:

场景 推荐协议 原因
文件传输、网页浏览 TCP 数据必须完整、有序
实时音视频 UDP 延迟敏感,丢几帧没关系
DNS查询 UDP 一次请求一次响应,简单快速
游戏状态同步 UDP 需要低延迟,丢包了直接覆盖
远程登录(SSH) TCP 每个按键都不能丢

我的建议:如果你不确定用哪个,先用TCP。TCP是「万金油」,大多数场景都能胜任。只有当你明确感受到TCP的延迟或吞吐量成为瓶颈时,再考虑UDP。

你想想看,为什么HTTP/3要用QUIC(基于UDP)?就是因为TCP的队头阻塞问题在弱网环境下太严重了。但QUIC在UDP之上自己实现了可靠传输和拥塞控制——说白了,是在UDP的「地基」上盖TCP的「房子」。

UDP通信知识体系

下面这张图总结了UDP通信的核心脉络:

UDP通信 核心特点:无连接 · 不可靠 · 面向数据报 客户端/服务器模型 广播 组播 socket → bind → recvfrom/sendto 无需listen/accept SO_BROADCAST选项 发送到255.255.255.255 IP_ADD_MEMBERSHIP D类IP地址(224.0.0.0/4) 与TCP的选择:延迟 vs 可靠性 的权衡

这张图把UDP的知识点串起来了。从中心出发,左边是编程模型,中间是广播,右边是组播。底部是最终的选择问题——说白了,就是你要在「快」和「稳」之间做个取舍。

我记得有一次做视频会议系统,一开始用的TCP,结果网络一卡,整个画面都卡住了。后来换成UDP,虽然偶尔有马赛克,但至少能看。用户反馈说:「画面有点糊,但至少不卡了。」——这就是UDP的价值所在。


公众号:蓝海资料掘金营,微信deep3321