UDP(用户数据报协议)是一种无连接的传输层协议,与TCP不同,它不保证数据包的顺序和可靠性,但其简单性和低延迟特性使其在实时应用中非常有用。

一、UDP协议核心特性

UDP作为传输层协议,与TCP的“可靠连接”不同,它具有以下特点:

  • 无连接:通信前无需建立三次握手,直接发送数据报
  • 不可靠:不保证数据有序到达,也不重传丢失的数据包
  • 面向数据报:数据以固定大小的“数据报”为单位传输
  • 低延迟:省去连接建立和确认机制,适合实时场景(如聊天、直播)

UDP适合用于一次传送少量数据、对可靠性要求不高的应用环境。例如流媒体、在线游戏、DNS查询等场景。

二、UDP编程模型

UDP是无连接的,先启动哪一端都不会报错。UDP编程与TCP的核心差异在于“无连接”特性——无需listenacceptconnect流程,直接通过sendtorecvfrom完成数据收发。

服务器端流程:

  1. 创建套接字:使用socket(AF_INET, SOCK_DGRAM, 0)创建UDP套接字
  2. 绑定地址:使用bind()将套接字与IP地址和端口号关联
  3. 收发数据:使用recvfrom()/sendto()进行数据交互
  4. 关闭套接字:使用close()释放资源

客户端流程:

  1. 创建套接字:使用socket(AF_INET, SOCK_DGRAM, 0)创建UDP套接字
  2. 收发数据:使用sendto()/recvfrom()进行数据交互
  3. 关闭套接字:使用close()释放资源

三、核心API函数详解

1. socket() - 创建套接字

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  • AF_INET:IPv4协议族
  • SOCK_DGRAM:数据报套接字,对应UDP协议,区别于TCP的SOCK_STREAM
  • 第三个参数通常设为0,表示由内核自动选择协议

2. bind() - 绑定地址(服务器端必须)

UDP服务器端必须调用bind函数将套接字绑定到固定的本地端口——客户端需要通过该端口定位服务器,而UDP客户端通常无需绑定(系统自动分配临时端口)。

struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(12345);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));

3. sendto() - 发送数据

UDP无连接特性决定了其发送数据时需明确指定目标地址——sendto函数正是为此设计,它将数据报发送到指定的服务器端地址,无需提前建立连接(区别于TCP的send)。

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

重要说明sendto返回成功仅表示数据已交给内核发送缓冲区,不保证对方能接收(UDP无确认机制),这与TCP的send有本质区别。

4. recvfrom() - 接收数据

UDP服务器端通过recvfrom接收客户端发送的数据报,同时获取发送方(客户端)的地址信息,以便后续回复。

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

阻塞特性:默认情况下,若套接字无数据可接收,recvfrom会一直阻塞,直到有数据报到达或被信号中断。

5. close() - 关闭套接字

close(sockfd);

四、完整代码示例

UDP服务器端代码

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

#define SERVER_PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int server_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];

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

    // 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(SERVER_PORT);

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

    printf("UDP Server listening on port %d...\n", SERVER_PORT);

    // 3. 循环收发数据
    while (1) {
        memset(buffer, 0, BUFFER_SIZE);
        
        // 接收客户端数据
        ssize_t recv_len = recvfrom(server_fd, buffer, BUFFER_SIZE - 1, 0,
                                    (struct sockaddr *)&client_addr, &client_addr_len);
        if (recv_len < 0) {
            perror("recvfrom failed");
            continue;
        }
        buffer[recv_len] = '\0';

        printf("Received from %s:%d: %s\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port),
               buffer);

        // 回显数据到客户端
        sendto(server_fd, buffer, strlen(buffer), 0,
               (struct sockaddr *)&client_addr, client_addr_len);
    }

    close(server_fd);
    return 0;
}

UDP客户端代码

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

#define SERVER_PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int client_fd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];

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

    // 2. 配置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

    // 3. 发送数据
    const char *message = "Hello, UDP Server!";
    sendto(client_fd, message, strlen(message), 0,
           (struct sockaddr *)&server_addr, sizeof(server_addr));
    printf("Sent: %s\n", message);

    // 4. 接收回显数据
    socklen_t addr_len = sizeof(server_addr);
    ssize_t recv_len = recvfrom(client_fd, buffer, BUFFER_SIZE - 1, 0,
                                (struct sockaddr *)&server_addr, &addr_len);
    if (recv_len > 0) {
        buffer[recv_len] = '\0';
        printf("Received: %s\n", buffer);
    }

    close(client_fd);
    return 0;
}

五、UDP与TCP编程的关键差异

维度 UDP TCP
连接方式 无连接,直接发送数据报 面向连接,需三次握手建立连接
可靠性 不可靠,不保证数据到达 可靠,有确认重传机制
数据边界 保留消息边界 流式协议,无消息边界(需自行处理粘包)
服务端流程 socket→bind→recvfrom/sendto socket→bind→listen→accept→recv/send
客户端流程 socket→sendto/recvfrom socket→connect→send/recv
适用场景 实时音视频、在线游戏、DNS查询 文件传输、网页浏览、邮件服务

六、错误处理

所有系统调用都可能出错,合适的错误处理能提高程序的健壮性。使用errno来检查错误代码,并利用perror()strerror()来输出或处理错误信息。

常见错误处理示例:

if (sendto(sockfd, message, strlen(message), 0, 
           (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) {
    perror("sendto failed");
    // 根据errno进行相应处理
}

七、UDP编程的注意事项

  1. 数据报大小限制:UDP数据报最大64KB(包括头部),建议发送数据控制在1472字节以内(以太网MTU 1500 - IP头部20 - UDP头部8)以避免分片。

  2. UDP套接字没有粘包问题,但是不能替代TCP套接字,因为UDP协议有一个缺陷:如果数据发送的途中丢失,则数据就丢失了,而TCP则不会有这种缺陷。

  3. 空数据的处理:UDP协议是数据报协议,传入的数据可为空,在传输过程中UDP会对数据进行内部的拼接和处理。

  4. 断开链接的影响:UDP协议是通过解析对方数据中的IP和端口来返回数据的,所以一方发生问题并不会影响到另一方。

八、进阶应用场景

基于UDP可以构建多种实际应用,例如:

  • 回显服务器:接收客户端消息并原样返回,是UDP编程的“Hello World”
  • 英译汉字典服务:客户端发送英文单词,服务器返回中文释义
  • 多线程聊天室:使用多线程处理多个客户端,实现实时群聊功能

这些应用展示了UDP在实际开发中的灵活性和实用性,掌握基础后可以进一步探索更复杂的网络应用开发。处理大量并发客户端时,可能需要使用多线程(pthread库)或多进程(fork系统调用)来实现。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐