11、UDP通信基础:UDP服务端与客户端实现,sendto()与recvfrom()

聊到UDP,很多刚入门的朋友第一反应就是“不可靠”。没错,UDP确实不保证数据一定到达,也不保证顺序。但我想说——不可靠不代表没用

我在做音视频传输项目时,底层几乎全是UDP。为什么?因为实时性要求高,丢一两个包无所谓,但延迟绝对不能大。你想想看,视频通话如果卡一下,体验就很糟糕了。所以UDP在流媒体、游戏、DNS查询这些场景里,反而是主力。

这一章,我们就来手写一个UDP服务端和客户端。核心就两个函数:sendto()recvfrom()。说白了,UDP比TCP简单太多了——没有三次握手,没有连接维护,直接发就完事了。

UDP的核心特点

先快速过一下UDP的几个关键点,后面写代码时你会更有感觉:

  • 无连接:不需要像TCP那样调用 connect(),直接发数据。
  • 不可靠:数据可能丢、可能乱序、可能重复到达。
  • 面向报文:每次 sendto() 发送一个完整报文,接收方 recvfrom() 一次读一个报文。不会像TCP那样粘包。
  • 支持广播和多播:这是TCP做不到的。

核心区别一句话:TCP像打电话——先接通,再说话,挂断结束。UDP像寄快递——写好地址扔出去,不管对方收没收到。

UDP通信流程图

先看一张图,把整体流程刻在脑子里。我个人习惯是先画图再写代码,这样不容易漏步骤。

UDP 服务端 socket() 创建套接字 bind() 绑定端口 recvfrom() 接收数据 sendto() 发送响应 close() 关闭套接字 UDP 客户端 socket() 创建套接字 sendto() 发送数据 recvfrom() 接收响应 close() 关闭套接字 数据报文 响应报文

看到没?服务端和客户端都只需要 socket() 创建套接字,然后直接收发数据。服务端多一个 bind() 绑定端口,客户端甚至可以不绑定——让系统随机分配一个端口就行。

sendto() 和 recvfrom() 函数详解

这两个函数是UDP编程的核心。我当年第一次用的时候,被它们的参数搞得有点晕。这里拆开讲清楚。

sendto() 函数原型

#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
参数说明
sockfd套接字描述符
buf要发送的数据缓冲区
len数据长度(字节)
flags一般填0,特殊场景用MSG_DONTWAIT等
dest_addr目标地址结构体(IP+端口)
addrlen地址结构体长度

小技巧flags 参数平时填0就行。但如果你做高并发服务器,可以用 MSG_DONTWAIT 实现非阻塞发送,避免卡住。

recvfrom() 函数原型

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
参数说明
sockfd套接字描述符
buf接收数据缓冲区
len缓冲区最大长度
flags一般填0
src_addr发送方的地址结构体(函数返回时填充)
addrlen传入地址结构体大小,返回实际大小

注意recvfrom()addrlen 是指针!传入时是 sizeof(struct sockaddr_in),函数返回后变成实际地址长度。我见过有人传了 NULL,结果不知道数据是谁发的——这在多客户端场景下就尴尬了。

完整代码示例:UDP回声服务端

下面是一个完整的UDP回声服务端。客户端发什么,服务端就原样返回。这个例子我在培训新人时经常用,简单但能说明所有关键点。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8888
#define BUFFER_SIZE 1024

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

    // 1. 创建UDP套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(1);
    }

    // 2. 绑定地址和端口
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 监听所有网卡
    server_addr.sin_port = htons(PORT);

    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind");
        close(sockfd);
        exit(1);
    }

    printf("UDP 服务端已启动,监听端口 %d...\n", PORT);

    // 3. 循环接收数据并回声
    while (1) {
        recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,
                            (struct sockaddr *)&client_addr, &addr_len);
        if (recv_len < 0) {
            perror("recvfrom");
            continue;
        }

        buffer[recv_len] = '\0';
        printf("收到来自 %s:%d 的消息: %s\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port),
               buffer);

        // 原样发送回去
        sendto(sockfd, buffer, recv_len, 0,
               (struct sockaddr *)&client_addr, addr_len);
    }

    close(sockfd);
    return 0;
}

完整代码示例:UDP客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    socklen_t addr_len = sizeof(server_addr);
    char send_buf[BUFFER_SIZE];
    char recv_buf[BUFFER_SIZE];
    int recv_len;

    // 1. 创建UDP套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(1);
    }

    // 2. 设置服务端地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    server_addr.sin_port = htons(SERVER_PORT);

    // 3. 发送数据
    printf("请输入要发送的消息: ");
    fgets(send_buf, BUFFER_SIZE, stdin);
    send_buf[strcspn(send_buf, "\n")] = '\0';  // 去掉换行符

    sendto(sockfd, send_buf, strlen(send_buf), 0,
           (struct sockaddr *)&server_addr, addr_len);
    printf("已发送: %s\n", send_buf);

    // 4. 接收回声
    recv_len = recvfrom(sockfd, recv_buf, BUFFER_SIZE, 0,
                        (struct sockaddr *)&server_addr, &addr_len);
    if (recv_len > 0) {
        recv_buf[recv_len] = '\0';
        printf("收到服务端回声: %s\n", recv_buf);
    }

    close(sockfd);
    return 0;
}

运行效果

先启动服务端,再启动客户端。客户端输入 "Hello UDP",服务端会打印收到的消息,客户端会收到同样的内容。

# 服务端输出
UDP 服务端已启动,监听端口 8888...
收到来自 127.0.0.1:54321 的消息: Hello UDP

# 客户端输出
请输入要发送的消息: Hello UDP
已发送: Hello UDP
收到服务端回声: Hello UDP

避坑指南

这里总结几个我踩过的坑,你写代码时注意一下:

  • 字节序问题:IP和端口一定要用 htonl()/htons() 转换。我曾经在x86机器上写死端口,换到ARM板子上就收不到数据——因为字节序反了。
  • 缓冲区大小:UDP报文最大65507字节(理论值),但实际建议控制在1472字节以内(以太网MTU 1500 - IP头20 - UDP头8)。超过这个值可能会分片,分片后只要丢一片,整个报文就废了。
  • recvfrom 的 addrlen 必须初始化:传入前一定要赋值为 sizeof(struct sockaddr_in),否则函数可能返回错误。
  • 客户端不 bind 也可以:系统会自动分配一个临时端口。但如果你需要固定端口(比如做P2P打洞),那就手动 bind。

经验之谈:我在做游戏服务器时,UDP包大小严格控制在1400字节以内。超过这个值,某些路由器会丢弃分片包,导致玩家掉线。后来我们干脆在应用层自己做分包和重组,虽然麻烦点,但稳定多了。

UDP vs TCP 快速对比

特性UDPTCP
连接无连接面向连接
可靠性不可靠可靠
顺序不保证保证
速度相对慢(有握手和确认)
边界保留消息边界字节流,无边界
适用场景音视频、游戏、DNS文件传输、网页、邮件

嗯,到这里UDP的基础通信就讲完了。代码不多,但核心思想都在里面。你把这个回声例子跑通了,后面做广播、多播、甚至P2P通信,都是在这个基础上加东西而已。

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