UDP 编程 详解

目录

前言

[一、UDP 协议概述](#一、UDP 协议概述)

[1.1 UDP 特点](#1.1 UDP 特点)

[1.2 UDP 与 TCP 对比](#1.2 UDP 与 TCP 对比)

[1.3 UDP 数据报格式](#1.3 UDP 数据报格式)

[二、核心 API 详解](#二、核心 API 详解)

[2.1 socket() - 创建 UDP 套接字](#2.1 socket() - 创建 UDP 套接字)

[2.2 bind() - 绑定地址](#2.2 bind() - 绑定地址)

[2.3 sendto() - 发送数据报](#2.3 sendto() - 发送数据报)

[2.4 recvfrom() - 接收数据报](#2.4 recvfrom() - 接收数据报)

[2.5 connect() - 连接 UDP 套接字(可选)](#2.5 connect() - 连接 UDP 套接字(可选))

[2.6 send() / recv() - 用于已连接的 UDP](#2.6 send() / recv() - 用于已连接的 UDP)

三、服务器端编程

[3.1 基本流程](#3.1 基本流程)

[3.2 迭代服务器示例](#3.2 迭代服务器示例)

[3.3 多线程服务器示例](#3.3 多线程服务器示例)

四、客户端编程

[4.1 基本流程](#4.1 基本流程)

[4.2 基础客户端示例](#4.2 基础客户端示例)

[4.3 使用 connect() 的客户端](#4.3 使用 connect() 的客户端)

五、高级特性

[5.1 广播(Broadcast)](#5.1 广播(Broadcast))

[5.2 多播(Multicast)](#5.2 多播(Multicast))

[5.3 设置超时](#5.3 设置超时)

[5.4 获取数据报信息](#5.4 获取数据报信息)

[5.5 使用 recvmsg/sendmsg(高级 I/O)](#5.5 使用 recvmsg/sendmsg(高级 I/O))

[5.6 I/O 多路复用](#5.6 I/O 多路复用)

六、完整示例代码

[6.1 可靠的 UDP 实现](#6.1 可靠的 UDP 实现)

[6.2 文件传输服务器](#6.2 文件传输服务器)

七、常见问题与调试

[7.1 常见错误码](#7.1 常见错误码)

[7.2 MTU 与分片问题](#7.2 MTU 与分片问题)

[7.3 缓冲区设置](#7.3 缓冲区设置)

[7.4 调试工具](#7.4 调试工具)

[7.5 编译运行](#7.5 编译运行)

总结


前言

本文详细介绍了UDP协议及其编程实现。

UDP是一种无连接、不可靠但高效的传输层协议,适用于视频流、游戏等实时应用。文章对比了UDP与TCP的特性差异,详细解析了UDP数据报格式和核心API(socket、bind、sendto、recvfrom等),并提供了服务器和客户端的编程示例。

此外,还涵盖了UDP高级特性如广播、多播、超时设置和I/O多路复用,以及可靠UDP实现和文件传输的完整代码示例。最后总结了UDP编程要点和常见问题调试方法,为开发者提供了全面的UDP网络编程指导。


一、UDP 协议概述

1.1 UDP 特点

UDP(User Datagram Protocol,用户数据报协议)是一种无连接的、不可靠的、基于数据报的传输层协议

| 特性 | 说明 |
| 无连接 | 不需要建立连接,直接发送数据 |
| 不可靠 | 不保证数据到达,无重传机制 |
| 面向数据报 | 保留消息边界,一次发送对应一次接收 |
| 低开销 | 头部仅 8 字节,比 TCP 更高效 |
| 无拥塞控制 | 不会根据网络状况调整发送速率 |

支持多播/广播 可以向多个目标同时发送数据

1.2 UDP 与 TCP 对比

| 特性 | TCP | UDP |
| 连接方式 | 面向连接 | 无连接 |
| 可靠性 | 可靠传输 | 不可靠传输 |
| 数据边界 | 字节流,无边界 | 数据报,保留边界 |
| 头部大小 | 20 字节 | 8 字节 |
| 传输效率 | 较低(有控制开销) | 较高 |
| 适用场景 | 文件传输、HTTP、邮件 | 视频流、DNS、游戏 |
| 拥塞控制 | 有 | 无 |
| 流量控制 | 有 | 无 |

多播/广播 不支持 支持

1.3 UDP 数据报格式

复制代码
 0      7 8     15 16    23 24    31
+--------+--------+--------+--------+
|     源端口      |     目的端口     |
+--------+--------+--------+--------+
|     长度        |     校验和       |
+--------+--------+--------+--------+
|              数据(可选)            |
+-----------------------------------+

- 源端口 (16位): 发送方端口号
- 目的端口 (16位): 接收方端口号
- 长度 (16位): UDP 头部 + 数据的总长度
- 校验和 (16位): 用于检测数据错误

二、核心 API 详解

2.1 socket() - 创建 UDP 套接字

cpp 复制代码
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

UDP 套接字创建

cpp 复制代码
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
    perror("socket creation failed");
    exit(EXIT_FAILURE);
}

注意 :UDP 使用 SOCK_DGRAM(Datagram)类型。

2.2 bind() - 绑定地址

cpp 复制代码
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

UDP 服务器通常需要绑定固定端口,客户端可以不绑定(系统自动分配)。

cpp 复制代码
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
serv_addr.sin_addr.s_addr = INADDR_ANY;

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

2.3 sendto() - 发送数据报

cpp 复制代码
#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)
dest_addr 目标地址结构体
addrlen 地址结构体长度

返回值:成功返回发送的字节数,失败返回 -1

示例

cpp 复制代码
struct sockaddr_in dest_addr;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &dest_addr.sin_addr);

const char *msg = "Hello, UDP Server!";
sendto(sockfd, msg, strlen(msg), 0, 
       (struct sockaddr *)&dest_addr, sizeof(dest_addr));

2.4 recvfrom() - 接收数据报

cpp 复制代码
#include <sys/socket.h>

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 输入输出参数,地址长度

返回值

  • > 0:接收到的字节数
  • = 0:UDP 中通常不会发生
  • < 0:发生错误

示例

cpp 复制代码
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
char buffer[1024];

int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
                 (struct sockaddr *)&cli_addr, &cli_len);
if (n > 0) {
    buffer[n] = '\0';
    printf("Received from %s:%d: %s\n",
           inet_ntoa(cli_addr.sin_addr),
           ntohs(cli_addr.sin_port),
           buffer);
}

2.5 connect() - 连接 UDP 套接字(可选)

复制代码
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

UDP 中的 connect() 不会建立真正的连接,只是记录默认的目标地址。

使用 connect() 后的好处

  • 可以使用 send() 代替 sendto()
  • 可以使用 recv() 代替 recvfrom()
  • 只接收来自指定地址的数据报
cpp 复制代码
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);

connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

// 之后可以使用 send/recv
send(sockfd, msg, strlen(msg), 0);
recv(sockfd, buffer, sizeof(buffer), 0);

2.6 send() / recv() - 用于已连接的 UDP

当 UDP 套接字调用 connect() 后,可以使用 send()recv()

cpp 复制代码
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

三、服务器端编程

3.1 基本流程

cpp 复制代码
┌─────────────┐
│   socket()  │  ← 创建 UDP 套接字
└──────┬──────┘
       │
       ▼
┌─────────────┐
│    bind()   │  ← 绑定地址和端口
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  recvfrom() │  ← 循环接收数据报
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  sendto()   │  ← 发送响应
└─────────────┘
       │
       └───────┐
               │
               ▼
         [继续循环]

3.2 迭代服务器示例

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in serv_addr, cli_addr;
    socklen_t cli_len = sizeof(cli_addr);
    char buffer[BUFFER_SIZE];

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

    // 2. 绑定地址
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(PORT);

    if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("Bind failed");
        exit(EXIT_FAILURE);
    }

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

    // 3. 接收和响应数据报
    while (1) {
        memset(buffer, 0, BUFFER_SIZE);
        
        // 接收数据
        int n = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
                        (struct sockaddr *)&cli_addr, &cli_len);
        if (n < 0) {
            perror("recvfrom failed");
            continue;
        }

        buffer[n] = '\0';
        printf("Received from %s:%d: %s\n",
               inet_ntoa(cli_addr.sin_addr),
               ntohs(cli_addr.sin_port),
               buffer);

        // 发送响应
        const char *response = "Message received!";
        sendto(sockfd, response, strlen(response), 0,
               (struct sockaddr *)&cli_addr, cli_len);
    }

    close(sockfd);
    return 0;
}

3.3 多线程服务器示例

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024
#define MAX_THREADS 100

typedef struct {
    int sockfd;
    struct sockaddr_in cli_addr;
    socklen_t cli_len;
    char buffer[BUFFER_SIZE];
    int data_len;
} client_data_t;

void *handle_client(void *arg) {
    client_data_t *data = (client_data_t *)arg;
    
    printf("[Thread] Processing from %s:%d: %s\n",
           inet_ntoa(data->cli_addr.sin_addr),
           ntohs(data->cli_addr.sin_port),
           data->buffer);

    // 模拟处理时间
    sleep(1);

    // 发送响应
    const char *response = "Processed by thread";
    sendto(data->sockfd, response, strlen(response), 0,
           (struct sockaddr *)&data->cli_addr, data->cli_len);

    free(data);
    return NULL;
}

int main() {
    int sockfd;
    struct sockaddr_in serv_addr;
    pthread_t tid;

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(PORT);

    if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("Bind failed");
        exit(EXIT_FAILURE);
    }

    printf("UDP Server (multi-thread) listening on port %d...\n", PORT);

    while (1) {
        client_data_t *data = malloc(sizeof(client_data_t));
        data->sockfd = sockfd;
        data->cli_len = sizeof(data->cli_addr);

        // 接收数据
        int n = recvfrom(sockfd, data->buffer, BUFFER_SIZE - 1, 0,
                        (struct sockaddr *)&data->cli_addr, &data->cli_len);
        if (n < 0) {
            perror("recvfrom failed");
            free(data);
            continue;
        }

        data->data_len = n;
        data->buffer[n] = '\0';

        // 创建线程处理
        if (pthread_create(&tid, NULL, handle_client, data) != 0) {
            perror("Thread creation failed");
            free(data);
        } else {
            pthread_detach(tid);
        }
    }

    close(sockfd);
    return 0;
}

四、客户端编程

4.1 基本流程

cpp 复制代码
┌─────────────┐
│   socket()  │  ← 创建 UDP 套接字
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  sendto()   │  ← 发送数据报
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  recvfrom() │  ← 接收响应
└──────┬──────┘
       │
       ▼
┌─────────────┐
│   close()   │  ← 关闭套接字
└─────────────┘

4.2 基础客户端示例

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

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

int main() {
    int sockfd;
    struct sockaddr_in serv_addr;
    socklen_t serv_len = sizeof(serv_addr);
    char buffer[BUFFER_SIZE];

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

    // 2. 设置服务器地址
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERVER_PORT);
    inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr);

    printf("UDP Client started. Type messages (or 'quit' to exit):\n");

    // 3. 发送和接收数据
    while (1) {
        printf("> ");
        fgets(buffer, BUFFER_SIZE, stdin);
        buffer[strcspn(buffer, "\n")] = 0;

        if (strcmp(buffer, "quit") == 0) {
            break;
        }

        // 发送数据
        sendto(sockfd, buffer, strlen(buffer), 0,
               (struct sockaddr *)&serv_addr, serv_len);

        // 接收响应
        memset(buffer, 0, BUFFER_SIZE);
        int n = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
                        (struct sockaddr *)&serv_addr, &serv_len);
        if (n > 0) {
            buffer[n] = '\0';
            printf("Server response: %s\n", buffer);
        }
    }

    close(sockfd);
    printf("Client exited.\n");

    return 0;
}

4.3 使用 connect() 的客户端

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

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

int main() {
    int sockfd;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE];

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

    // 设置服务器地址
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERVER_PORT);
    inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr);

    // 连接 UDP 套接字(记录默认目标地址)
    if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("Connect failed");
        exit(EXIT_FAILURE);
    }

    printf("Connected UDP Client started. Type messages (or 'quit' to exit):\n");

    while (1) {
        printf("> ");
        fgets(buffer, BUFFER_SIZE, stdin);
        buffer[strcspn(buffer, "\n")] = 0;

        if (strcmp(buffer, "quit") == 0) {
            break;
        }

        // 使用 send() 发送(不需要指定地址)
        send(sockfd, buffer, strlen(buffer), 0);

        // 使用 recv() 接收
        memset(buffer, 0, BUFFER_SIZE);
        int n = recv(sockfd, buffer, BUFFER_SIZE - 1, 0);
        if (n > 0) {
            buffer[n] = '\0';
            printf("Server response: %s\n", buffer);
        }
    }

    close(sockfd);
    return 0;
}

五、高级特性

5.1 广播(Broadcast)

UDP 支持向本地网络中的所有主机发送数据。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define BROADCAST_PORT 9999
#define BUFFER_SIZE 1024

// 广播发送者
void broadcast_sender() {
    int sockfd;
    struct sockaddr_in broadcast_addr;
    int broadcast_enable = 1;

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    
    // 启用广播
    setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, 
               &broadcast_enable, sizeof(broadcast_enable));

    memset(&broadcast_addr, 0, sizeof(broadcast_addr));
    broadcast_addr.sin_family = AF_INET;
    broadcast_addr.sin_port = htons(BROADCAST_PORT);
    broadcast_addr.sin_addr.s_addr = inet_addr("255.255.255.255");

    const char *msg = "Broadcast message!";
    sendto(sockfd, msg, strlen(msg), 0,
           (struct sockaddr *)&broadcast_addr, sizeof(broadcast_addr));

    printf("Broadcast sent: %s\n", msg);
    close(sockfd);
}

// 广播接收者
void broadcast_receiver() {
    int sockfd;
    struct sockaddr_in recv_addr, sender_addr;
    socklen_t sender_len = sizeof(sender_addr);
    char buffer[BUFFER_SIZE];

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    // 允许地址重用
    int reuse = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

    memset(&recv_addr, 0, sizeof(recv_addr));
    recv_addr.sin_family = AF_INET;
    recv_addr.sin_port = htons(BROADCAST_PORT);
    recv_addr.sin_addr.s_addr = INADDR_ANY;

    bind(sockfd, (struct sockaddr *)&recv_addr, sizeof(recv_addr));

    printf("Waiting for broadcast messages...\n");

    while (1) {
        memset(buffer, 0, BUFFER_SIZE);
        int n = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
                        (struct sockaddr *)&sender_addr, &sender_len);
        if (n > 0) {
            buffer[n] = '\0';
            printf("Received broadcast from %s: %s\n",
                   inet_ntoa(sender_addr.sin_addr), buffer);
        }
    }

    close(sockfd);
}

int main(int argc, char *argv[]) {
    if (argc > 1 && strcmp(argv[1], "sender") == 0) {
        broadcast_sender();
    } else {
        broadcast_receiver();
    }
    return 0;
}

5.2 多播(Multicast)

多播允许将数据发送给特定的主机组。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define MULTICAST_GROUP "239.255.0.1"
#define MULTICAST_PORT 8888
#define BUFFER_SIZE 1024

// 多播发送者
void multicast_sender() {
    int sockfd;
    struct sockaddr_in multicast_addr;

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    memset(&multicast_addr, 0, sizeof(multicast_addr));
    multicast_addr.sin_family = AF_INET;
    multicast_addr.sin_port = htons(MULTICAST_PORT);
    inet_pton(AF_INET, MULTICAST_GROUP, &multicast_addr.sin_addr);

    const char *msg = "Multicast message!";
    
    while (1) {
        sendto(sockfd, msg, strlen(msg), 0,
               (struct sockaddr *)&multicast_addr, sizeof(multicast_addr));
        printf("Multicast sent: %s\n", msg);
        sleep(2);
    }

    close(sockfd);
}

// 多播接收者
void multicast_receiver() {
    int sockfd;
    struct sockaddr_in local_addr, sender_addr;
    struct ip_mreq mreq;
    socklen_t sender_len = sizeof(sender_addr);
    char buffer[BUFFER_SIZE];

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    // 允许地址重用
    int reuse = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

    // 绑定到多播端口
    memset(&local_addr, 0, sizeof(local_addr));
    local_addr.sin_family = AF_INET;
    local_addr.sin_port = htons(MULTICAST_PORT);
    local_addr.sin_addr.s_addr = INADDR_ANY;

    bind(sockfd, (struct sockaddr *)&local_addr, sizeof(local_addr));

    // 加入多播组
    mreq.imr_multiaddr.s_addr = inet_addr(MULTICAST_GROUP);
    mreq.imr_interface.s_addr = INADDR_ANY;
    setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));

    printf("Joined multicast group %s:%d\n", MULTICAST_GROUP, MULTICAST_PORT);

    while (1) {
        memset(buffer, 0, BUFFER_SIZE);
        int n = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
                        (struct sockaddr *)&sender_addr, &sender_len);
        if (n > 0) {
            buffer[n] = '\0';
            printf("Received multicast from %s: %s\n",
                   inet_ntoa(sender_addr.sin_addr), buffer);
        }
    }

    // 离开多播组
    setsockopt(sockfd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq));
    close(sockfd);
}

int main(int argc, char *argv[]) {
    if (argc > 1 && strcmp(argv[1], "sender") == 0) {
        multicast_sender();
    } else {
        multicast_receiver();
    }
    return 0;
}

5.3 设置超时

cpp 复制代码
#include <sys/socket.h>
#include <sys/time.h>

// 设置接收超时
struct timeval tv;
tv.tv_sec = 5;  // 5秒
tv.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

// 接收数据(带超时)
int n = recvfrom(sockfd, buffer, sizeof(buffer), 0,
                (struct sockaddr *)&addr, &addr_len);
if (n < 0) {
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
        printf("Receive timeout\n");
    } else {
        perror("recvfrom error");
    }
}

5.4 获取数据报信息

cpp 复制代码
#include <sys/socket.h>

// 获取数据报大小
int recv_size;
socklen_t optlen = sizeof(recv_size);
getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &recv_size, &optlen);

// 获取本地地址
struct sockaddr_in local_addr;
socklen_t local_len = sizeof(local_addr);
getsockname(sockfd, (struct sockaddr *)&local_addr, &local_len);

// 获取对端地址(仅适用于已连接的 UDP)
struct sockaddr_in peer_addr;
socklen_t peer_len = sizeof(peer_addr);
getpeername(sockfd, (struct sockaddr *)&peer_addr, &peer_len);

5.5 使用 recvmsg/sendmsg(高级 I/O)

cpp 复制代码
#include <sys/socket.h>
#include <sys/uio.h>

// 使用 recvmsg 接收数据
struct msghdr msg;
struct iovec iov;
struct sockaddr_in addr;
char buffer[1024];
char control_buffer[256];

iov.iov_base = buffer;
iov.iov_len = sizeof(buffer);

msg.msg_name = &addr;
msg.msg_namelen = sizeof(addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = control_buffer;
msg.msg_controllen = sizeof(control_buffer);
msg.msg_flags = 0;

ssize_t n = recvmsg(sockfd, &msg, 0);
if (n > 0) {
    // 处理接收到的数据
    printf("Received %zd bytes\n", n);
    
    // 遍历控制消息
    struct cmsghdr *cmsg;
    for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; 
         cmsg = CMSG_NXTHDR(&msg, cmsg)) {
        if (cmsg->cmsg_level == IPPROTO_IP && 
            cmsg->cmsg_type == IP_PKTINFO) {
            struct in_pktinfo *pktinfo = (struct in_pktinfo *)CMSG_DATA(cmsg);
            printf("Destination IP: %s\n", 
                   inet_ntoa(pktinfo->ipi_addr));
        }
    }
}

5.6 I/O 多路复用

cpp 复制代码
#include <sys/select.h>
#include <poll.h>
#include <sys/epoll.h>

// select 示例
fd_set readfds;
struct timeval timeout;

FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);

timeout.tv_sec = 5;
timeout.tv_usec = 0;

int ret = select(sockfd + 1, &readfds, NULL, NULL, &timeout);
if (ret > 0 && FD_ISSET(sockfd, &readfds)) {
    // 套接字可读
    recvfrom(sockfd, buffer, sizeof(buffer), 0, ...);
}

// epoll 示例
int epoll_fd = epoll_create1(0);
struct epoll_event ev, events[10];

ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev);

int nfds = epoll_wait(epoll_fd, events, 10, -1);
for (int i = 0; i < nfds; i++) {
    if (events[i].data.fd == sockfd) {
        recvfrom(sockfd, buffer, sizeof(buffer), 0, ...);
    }
}

六、完整示例代码

6.1 可靠的 UDP 实现

虽然 UDP 本身不可靠,但可以在应用层实现简单的可靠传输机制:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <sys/time.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024
#define MAX_RETRIES 5
#define TIMEOUT_MS 1000
#define SEQ_NUM_MOD 1000

typedef struct {
    uint32_t seq_num;
    uint32_t ack_num;
    uint32_t flags;  // 1=ACK, 2=DATA
    uint32_t data_len;
    char data[BUFFER_SIZE];
} packet_t;

// 获取当前时间(毫秒)
uint64_t get_time_ms() {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}

// 发送数据包
int send_packet(int sockfd, packet_t *pkt, struct sockaddr_in *addr) {
    return sendto(sockfd, pkt, sizeof(packet_t), 0,
                  (struct sockaddr *)addr, sizeof(*addr));
}

// 接收数据包(带超时)
int recv_packet(int sockfd, packet_t *pkt, struct sockaddr_in *addr, 
                int timeout_ms) {
    struct timeval tv;
    tv.tv_sec = timeout_ms / 1000;
    tv.tv_usec = (timeout_ms % 1000) * 1000;
    
    setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
    
    socklen_t addr_len = sizeof(*addr);
    int n = recvfrom(sockfd, pkt, sizeof(packet_t), 0,
                    (struct sockaddr *)addr, &addr_len);
    
    // 恢复阻塞模式
    tv.tv_sec = 0;
    tv.tv_usec = 0;
    setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
    
    return n;
}

// 可靠发送
int reliable_send(int sockfd, const char *data, size_t len, 
                  struct sockaddr_in *addr, uint32_t seq_num) {
    packet_t pkt;
    packet_t ack_pkt;
    int retries = 0;
    
    memset(&pkt, 0, sizeof(pkt));
    pkt.seq_num = seq_num;
    pkt.flags = 2;  // DATA
    pkt.data_len = len;
    memcpy(pkt.data, data, len);
    
    while (retries < MAX_RETRIES) {
        // 发送数据
        send_packet(sockfd, &pkt, addr);
        printf("Sent packet seq=%u, retry=%d\n", seq_num, retries);
        
        // 等待 ACK
        if (recv_packet(sockfd, &ack_pkt, addr, TIMEOUT_MS) > 0) {
            if (ack_pkt.flags == 1 && ack_pkt.ack_num == seq_num) {
                printf("Received ACK for seq=%u\n", seq_num);
                return 0;  // 发送成功
            }
        }
        
        retries++;
    }
    
    return -1;  // 发送失败
}

// 可靠接收
int reliable_recv(int sockfd, char *buffer, size_t max_len,
                  struct sockaddr_in *addr, uint32_t expected_seq) {
    packet_t pkt;
    packet_t ack_pkt;
    
    while (1) {
        socklen_t addr_len = sizeof(*addr);
        int n = recvfrom(sockfd, &pkt, sizeof(pkt), 0,
                        (struct sockaddr *)addr, &addr_len);
        
        if (n < 0) continue;
        
        if (pkt.flags == 2) {  // DATA
            printf("Received packet seq=%u\n", pkt.seq_num);
            
            // 发送 ACK
            memset(&ack_pkt, 0, sizeof(ack_pkt));
            ack_pkt.flags = 1;  // ACK
            ack_pkt.ack_num = pkt.seq_num;
            send_packet(sockfd, &ack_pkt, addr);
            
            // 检查序列号
            if (pkt.seq_num == expected_seq) {
                size_t copy_len = pkt.data_len < max_len ? pkt.data_len : max_len;
                memcpy(buffer, pkt.data, copy_len);
                return copy_len;
            }
        }
    }
}

// 服务器
void reliable_server() {
    int sockfd;
    struct sockaddr_in serv_addr, cli_addr;
    char buffer[BUFFER_SIZE];
    uint32_t expected_seq = 0;

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(PORT);

    bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

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

    while (1) {
        int n = reliable_recv(sockfd, buffer, sizeof(buffer), 
                             &cli_addr, expected_seq);
        if (n > 0) {
            buffer[n] = '\0';
            printf("Received: %s\n", buffer);
            
            // 发送响应
            const char *response = "ACK";
            reliable_send(sockfd, response, strlen(response), 
                         &cli_addr, expected_seq);
            
            expected_seq = (expected_seq + 1) % SEQ_NUM_MOD;
        }
    }

    close(sockfd);
}

// 客户端
void reliable_client() {
    int sockfd;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE];
    uint32_t seq_num = 0;

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);

    printf("Reliable UDP Client started.\n");

    while (1) {
        printf("> ");
        fgets(buffer, BUFFER_SIZE, stdin);
        buffer[strcspn(buffer, "\n")] = 0;

        if (strcmp(buffer, "quit") == 0) break;

        // 发送数据
        if (reliable_send(sockfd, buffer, strlen(buffer), 
                         &serv_addr, seq_num) == 0) {
            // 接收响应
            char response[BUFFER_SIZE];
            if (reliable_recv(sockfd, response, sizeof(response),
                            &serv_addr, seq_num) > 0) {
                printf("Server response: %s\n", response);
            }
            seq_num = (seq_num + 1) % SEQ_NUM_MOD;
        } else {
            printf("Failed to send message\n");
        }
    }

    close(sockfd);
}

int main(int argc, char *argv[]) {
    if (argc > 1 && strcmp(argv[1], "server") == 0) {
        reliable_server();
    } else {
        reliable_client();
    }
    return 0;
}

6.2 文件传输服务器

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 4096
#define MAX_FILENAME 256

typedef struct {
    uint32_t type;       // 1=文件名, 2=数据, 3=结束
    uint32_t seq_num;
    uint32_t data_len;
    char data[BUFFER_SIZE];
} file_packet_t;

void file_server() {
    int sockfd, fd;
    struct sockaddr_in serv_addr, cli_addr;
    socklen_t cli_len = sizeof(cli_addr);
    file_packet_t pkt;
    char filename[MAX_FILENAME];

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(PORT);

    bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

    printf("File Server listening on port %d...\n", PORT);

    fd = -1;
    while (1) {
        int n = recvfrom(sockfd, &pkt, sizeof(pkt), 0,
                        (struct sockaddr *)&cli_addr, &cli_len);
        if (n < 0) continue;

        switch (pkt.type) {
            case 1:  // 文件名
                snprintf(filename, MAX_FILENAME, "received_%s", pkt.data);
                fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
                if (fd < 0) {
                    perror("Failed to create file");
                } else {
                    printf("Receiving file: %s\n", filename);
                }
                break;

            case 2:  // 数据
                if (fd >= 0) {
                    write(fd, pkt.data, pkt.data_len);
                    printf("Received chunk %u, size %u\n", 
                           pkt.seq_num, pkt.data_len);
                }
                break;

            case 3:  // 结束
                if (fd >= 0) {
                    close(fd);
                    fd = -1;
                    printf("File received successfully\n");
                    
                    // 发送确认
                    pkt.type = 4;  // ACK
                    sendto(sockfd, &pkt, sizeof(pkt), 0,
                           (struct sockaddr *)&cli_addr, cli_len);
                }
                break;
        }
    }

    close(sockfd);
}

void file_client(const char *filepath) {
    int sockfd, fd;
    struct sockaddr_in serv_addr;
    socklen_t serv_len = sizeof(serv_addr);
    file_packet_t pkt;
    ssize_t n;
    uint32_t seq = 0;

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);

    fd = open(filepath, O_RDONLY);
    if (fd < 0) {
        perror("Failed to open file");
        exit(EXIT_FAILURE);
    }

    // 发送文件名
    const char *filename = strrchr(filepath, '/');
    if (!filename) filename = filepath;
    else filename++;

    memset(&pkt, 0, sizeof(pkt));
    pkt.type = 1;
    strncpy(pkt.data, filename, BUFFER_SIZE);
    sendto(sockfd, &pkt, sizeof(pkt), 0,
           (struct sockaddr *)&serv_addr, serv_len);

    printf("Sending file: %s\n", filename);

    // 发送数据
    while ((n = read(fd, pkt.data, BUFFER_SIZE)) > 0) {
        pkt.type = 2;
        pkt.seq_num = seq++;
        pkt.data_len = n;
        sendto(sockfd, &pkt, sizeof(pkt), 0,
               (struct sockaddr *)&serv_addr, serv_len);
        usleep(1000);  // 防止发送过快
    }

    // 发送结束标记
    memset(&pkt, 0, sizeof(pkt));
    pkt.type = 3;
    sendto(sockfd, &pkt, sizeof(pkt), 0,
           (struct sockaddr *)&serv_addr, serv_len);

    printf("File sent, waiting for ACK...\n");

    // 等待确认
    struct timeval tv;
    tv.tv_sec = 5;
    tv.tv_usec = 0;
    setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

    n = recvfrom(sockfd, &pkt, sizeof(pkt), 0,
                (struct sockaddr *)&serv_addr, &serv_len);
    if (n > 0 && pkt.type == 4) {
        printf("File transfer completed successfully\n");
    } else {
        printf("No ACK received\n");
    }

    close(fd);
    close(sockfd);
}

int main(int argc, char *argv[]) {
    if (argc > 1 && strcmp(argv[1], "server") == 0) {
        file_server();
    } else if (argc > 2 && strcmp(argv[1], "client") == 0) {
        file_client(argv[2]);
    } else {
        printf("Usage: %s server\n", argv[0]);
        printf("       %s client <filepath>\n", argv[0]);
    }
    return 0;
}

七、常见问题与调试

7.1 常见错误码

错误码 含义 常见原因
EADDRINUSE 地址已被使用 端口被占用
EMSGSIZE 消息太长 数据报超过 MTU 或缓冲区大小
ECONNREFUSED 连接被拒绝 ICMP 端口不可达(已连接 UDP)
EAGAIN/EWOULDBLOCK 资源暂时不可用 非阻塞操作无法立即完成
ENETUNREACH 网络不可达 路由问题

7.2 MTU 与分片问题

cpp 复制代码
// 获取 MTU
int mtu;
socklen_t optlen = sizeof(mtu);
getsockopt(sockfd, IPPROTO_IP, IP_MTU, &mtu, &optlen);
printf("MTU: %d\n", mtu);

// 禁止分片
int dont_fragment = 1;
setsockopt(sockfd, IPPROTO_IP, IP_DONTFRAG, 
           &dont_fragment, sizeof(dont_fragment));

// 推荐:应用层分片,控制每个数据报大小
#define MAX_UDP_SIZE 1400  // 小于典型以太网 MTU (1500)

7.3 缓冲区设置

cpp 复制代码
// 设置接收缓冲区
int recv_buf_size = 256 * 1024;  // 256KB
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, 
           &recv_buf_size, sizeof(recv_buf_size));

// 设置发送缓冲区
int send_buf_size = 256 * 1024;
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, 
           &send_buf_size, sizeof(send_buf_size));

// 验证设置
int actual_size;
socklen_t optlen = sizeof(actual_size);
getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &actual_size, &optlen);
printf("Actual receive buffer: %d\n", actual_size);

7.4 调试工具

cpp 复制代码
# 查看 UDP 连接和监听端口
netstat -ulnp
ss -ulnp

# 抓包分析 UDP 流量
tcpdump -i any udp port 8080 -w udp_capture.pcap

# 使用 nc 测试 UDP
nc -u localhost 8080

# 使用 socat 测试
socat - UDP:localhost:8080

# 检查丢包统计
cat /proc/net/snmp | grep Udp

7.5 编译运行

cpp 复制代码
# 编译服务器
gcc -o udp_server udp_server.c -pthread

# 编译客户端
gcc -o udp_client udp_client.c

# 运行服务器
./udp_server

# 运行客户端
./udp_client

# 运行可靠 UDP
./reliable_udp server
./reliable_udp client

# 运行文件传输
./file_transfer server
./file_transfer client /path/to/file

总结

UDP 编程的关键要点:

  1. 无连接特性:不需要建立连接,直接发送数据报
  2. 数据报边界 :每次 sendto() 对应一次 recvfrom(),保留消息边界
  3. 不可靠性:应用层需要自行处理丢包、乱序、重复等问题
  4. 高效性:头部开销小,适合实时性要求高的场景
  5. 广播/多播:支持一对多的通信方式
  6. MTU 注意:注意数据报大小,避免 IP 分片
  7. 缓冲区管理:合理设置接收缓冲区,防止丢包

UDP 适用于:视频流、在线游戏、DNS、实时通信等对延迟敏感但可以容忍少量丢包的场景。

相关推荐
可涵不会debug1 小时前
五种IO模型
运维·服务器·网络
23.1 小时前
【分析】HTTP请求端口错误诊断:404、502与连接拒绝的区别
网络·网络协议·http
Irissgwe1 小时前
Linux进程信号
linux·服务器·开发语言·c++·linux进程信号
水木兰亭1 小时前
多进程编程总结
linux·运维·服务器
梦想是造卫星1 小时前
如何从零开始构建一个ros开发项目?
linux·ros开发
艾莉丝努力练剑1 小时前
【Linux:文件 + 进程】理解IPC通信
linux·运维·服务器·开发语言·网络·c++·ide
sdszoe49221 小时前
OSPF多区域基础实验1
网络·华为·ospf多区域实验
开开心心就好1 小时前
安卓免费证件照制作软件,无广告弹窗
linux·运维·安全·pdf·迭代器模式·依赖倒置原则·1024程序员节
辉视广播对讲1 小时前
私有协议 IP 广播对讲 vs SIP 广播对讲多维度对比分析报告
网络·网络协议·tcp/ip
猿来如此呀1 小时前
Linux 常用命令选项与对应英文单词对照表
linux·运维·服务器