目录
[一、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 编程的关键要点:
- 无连接特性:不需要建立连接,直接发送数据报
- 数据报边界 :每次
sendto()对应一次recvfrom(),保留消息边界 - 不可靠性:应用层需要自行处理丢包、乱序、重复等问题
- 高效性:头部开销小,适合实时性要求高的场景
- 广播/多播:支持一对多的通信方式
- MTU 注意:注意数据报大小,避免 IP 分片
- 缓冲区管理:合理设置接收缓冲区,防止丢包
UDP 适用于:视频流、在线游戏、DNS、实时通信等对延迟敏感但可以容忍少量丢包的场景。