UDP (User Datagram Protocol,用户数据报协议) 是一种无连接的、不可靠的、基于数据报的传输层协议,相比于 TCP,它更轻量级,开销小,传输速度快,但不保证数据包的顺序、不保证不丢失、不保证不重复,它常用于对实时性要求高、能容忍少量丢包的场景,如视频会议、在线游戏、DNS 查询等。

(图片来源网络,侵删)
下面我将分步讲解 UDP 编程的核心概念、完整的服务端和客户端代码,以及代码的详细解析。
核心概念与函数
UDP 编程主要使用以下几个关键的 Socket API 函数:
socket(): 创建一个套接字,用于网络通信。bind(): 将套接字与一个特定的 IP 地址和端口号绑定,对于服务器端是必须的,客户端通常不需要显式绑定(由操作系统自动分配)。sendto(): 向指定的 IP 地址和端口发送数据,这是 UDP 发送数据的核心函数。recvfrom(): 从网络中接收数据,并可以获取到数据发送方的 IP 地址和端口,这是 UDP 接收数据的核心函数。close(): (Linux/Unix) 或closesocket(): (Windows) 关闭套接字,释放资源。inet_addr(): 将点分十进制的 IP 地址字符串(如 "127.0.0.1")转换为网络字节序的整数格式。htons(): 将主机字节序的端口号转换为网络字节序,网络协议规定端口号和 IP 地址都必须使用网络字节序(大端序)。
编程步骤
UDP 服务器端 编程步骤
- 创建套接字: 使用
socket()函数创建一个 UDP 类型的套接字。 - 绑定地址和端口: 使用
bind()函数将套接字与服务器本机的 IP 地址和监听端口号绑定。 - 接收数据: 在一个循环中,使用
recvfrom()函数等待并接收客户端发送的数据。 - 处理数据 (可选): 对接收到的数据进行处理。
- 发送响应 (可选): 使用
sendto()函数向客户端发送响应数据。 - 关闭套接字: 通信结束后,使用
close()关闭套接字。
UDP 客户端 编程步骤
- 创建套接字: 使用
socket()函数创建一个 UDP 类型的套接字。 - 发送数据: 使用
sendto()函数向服务器的 IP 地址和端口发送数据,客户端不需要bind()(操作系统会自动分配一个临时端口)。 - 接收响应: 使用
recvfrom()函数接收服务器返回的响应数据。 - 关闭套接字: 通信结束后,使用
close()关闭套接字。
完整代码示例
以下是一个经典的“回显服务器”(Echo Server)的例子,客户端发送任何消息,服务器都会将同样的消息返回给客户端。
服务器端代码 (udp_server.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // 用于 close()
#include <sys/socket.h> // 用于 socket(), bind(), sendto(), recvfrom()
#include <netinet/in.h> // 用于 struct sockaddr_in 和 htons()
#include <arpa/inet.h> // 用于 inet_addr()
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_fd, client_len;
char buffer[BUFFER_SIZE];
struct sockaddr_in server_addr, client_addr;
// 1. 创建套接字
// AF_INET: IPv4
// SOCK_DGRAM: 数据报套接字 (UDP)
// 0: 默认协议 (对于 SOCK_DGRAM, 默认就是 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; // IPv4
server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定所有可用的网络接口
server_addr.sin_port = htons(PORT); // 绑定端口,htons将主机字节序转为网络字节序
// 强制绑定端口,避免 "Address already in use" 错误
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
if (bind(server_fd, (const struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 3. 循环接收和发送数据
while (1) {
client_len = sizeof(client_addr); // 必须先初始化这个长度
// 4. 接收客户端数据
// recvfrom 会将客户端的地址信息存入 client_addr
int n = recvfrom(server_fd, (char *)buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&client_addr, (socklen_t *)&client_len);
buffer[n] = '\0'; // 确保字符串以 null
printf("Received from %s:%d -> %s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buffer);
// 5. 将收到的数据回显给客户端
sendto(server_fd, (const char *)buffer, n, 0,
(const struct sockaddr *)&client_addr, client_len);
printf("Echoed back to client.\n");
}
// 6. 关闭套接字 (虽然上面的循环是永久的,但理论上应该在这里执行)
close(server_fd);
return 0;
}
客户端代码 (udp_client.c)
#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_PORT 8080
#define SERVER_IP "127.0.0.1" // 本地回环地址,用于测试
#define BUFFER_SIZE 1024
int main() {
int client_fd;
char buffer[BUFFER_SIZE];
struct sockaddr_in server_addr;
// 1. 创建套接字
if ((client_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&server_addr, 0, sizeof(server_addr)); // 清空结构体
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_port = htons(SERVER_PORT); // 服务器端口
// 将 IP 地址字符串转换为网络字节序的整数
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("invalid address/ Address not supported");
exit(EXIT_FAILURE);
}
// 从标准输入读取用户输入
printf("Enter a message to send to the server (or 'exit' to quit): ");
while (fgets(buffer, BUFFER_SIZE, stdin) != NULL) {
// 去掉换行符
buffer[strcspn(buffer, "\n")] = 0;
if (strcmp(buffer, "exit") == 0) {
break;
}
// 2. 发送数据到服务器
sendto(client_fd, (const char *)buffer, strlen(buffer), 0,
(const struct sockaddr *)&server_addr, sizeof(server_addr));
printf("Message sent.\n");
// 3. 接收服务器响应
int n = recvfrom(client_fd, (char *)buffer, BUFFER_SIZE, 0,
NULL, NULL); // 客户端不需要关心是谁发来的,所以可以传NULL
buffer[n] = '\0';
printf("Server echoed back: %s\n", buffer);
printf("\nEnter another message (or 'exit' to quit): ");
}
// 4. 关闭套接字
close(client_fd);
return 0;
}
代码详解
服务器端 (udp_server.c)
struct sockaddr_in server_addr, client_addr;- 这是用于存储 IPv4 地址信息的结构体。
server_addr用于服务器自己的信息,client_addr用于存储连接进来的客户端的信息。
- 这是用于存储 IPv4 地址信息的结构体。
socket(AF_INET, SOCK_DGRAM, 0)AF_INET: 指定使用 IPv4 地址族。SOCK_DGRAM: 指定创建数据报套接字,也就是 UDP。
INADDR_ANY- 这是一个特殊的 IP 地址,表示“所有可用的网络接口”,服务器通常用它来绑定,这样无论客户端是通过
0.0.1(本机)、168.1.100(局域网IP)还是公网IP访问,服务器都能收到。
- 这是一个特殊的 IP 地址,表示“所有可用的网络接口”,服务器通常用它来绑定,这样无论客户端是通过
htons(PORT)- host to network short,计算机内存中存储数据的方式(字节序)可能与网络标准(大端序)不同。
htons确保端口号在网络传输时是正确的格式,IP 地址的转换函数是inet_addr。
- host to network short,计算机内存中存储数据的方式(字节序)可能与网络标准(大端序)不同。
recvfrom(server_fd, buffer, ...)- 这是 UDP 接收数据的函数,它会阻塞程序,直到收到一个数据包。
buffer: 存放接收到的数据。&client_addr: 一个指针,用于存放发送方的地址信息。&client_len: 一个指针,指向client_addr结构体的大小。recvfrom会填充这个地址信息。
sendto(..., &client_addr, ...)sendto不仅可以发送数据,还能指定要发送给谁,这里我们将收到的数据原封不动地发回给刚刚发消息的客户端。
客户端 (udp_client.c)
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr)- presentation to network,这是将人类可读的 IP 地址字符串(如 "127.0.0.1")转换为
struct in_addr所需的二进制格式,这是现代推荐的做法,比旧的inet_addr更安全,能处理 IPv6。
- presentation to network,这是将人类可读的 IP 地址字符串(如 "127.0.0.1")转换为
sendto(..., &server_addr, ...)客户端在发送数据时,必须明确指定服务器的地址和端口,这样数据包才能被正确路由。
(图片来源网络,侵删)recvfrom(..., NULL, NULL)- 在客户端,我们通常只关心从服务器收到的响应,而不关心这个响应来自哪个地址(因为我们只发给了服务器一个地址),可以将后两个参数设为
NULL,表示我们不关心这些信息。
- 在客户端,我们通常只关心从服务器收到的响应,而不关心这个响应来自哪个地址(因为我们只发给了服务器一个地址),可以将后两个参数设为
如何编译和运行
你需要使用 GCC 编译器来编译这些 C 代码。
-
编译代码 打开终端,运行以下命令:
# 编译服务器 gcc udp_server.c -o udp_server # 编译客户端 gcc udp_client.c -o udp_client
-
运行程序
-
启动服务器:
(图片来源网络,侵删)./udp_server
你会看到输出:
Server listening on port 8080... -
在另一个新的终端窗口中启动客户端:
./udp_client
你会看到提示:
Enter a message to send to the server (or 'exit' to quit): -
进行交互: 在客户端输入一些文字,
Hello, UDP Server!,然后按回车。-
客户端终端 会显示:
Message sent. Server echoed back: Hello, UDP Server! Enter another message (or 'exit' to quit): -
服务器端终端 会显示:
Received from 127.0.0.1:54321 -> Hello, UDP Server! Echoed back to client.(注意:客户端的端口号
54321是每次运行时由系统随机分配的,所以你的可能会不同)
-
-
输入
exit即可退出客户端程序,服务器会继续运行,等待下一个客户端连接,你可以按Ctrl+C来终止服务器。
-
UDP 与 TCP 的关键区别总结
| 特性 | UDP (User Datagram Protocol) | TCP (Transmission Control Protocol) |
|---|---|---|
| 连接性 | 无连接 | 面向连接 |
| 可靠性 | 不可靠 (不保证顺序、不保证不丢包、不保证不重复) | 可靠 (通过确认、重传、排序机制保证数据完整有序) |
| 速度 | 快 (开销小,没有建立连接和复杂的控制过程) | 慢 (开销大,需要三次握手,有拥塞控制等机制) |
| 头部开销 | 小 (8字节) | 大 (20字节以上,可选项会增大) |
| 应用场景 | 实时视频/音频、在线游戏、DNS查询、广播 | 文件传输、网页浏览、电子邮件等要求可靠性的场景 |
| 编程模型 | sendto / recvfrom |
connect / send / recv / read / write |
