UDP (User Datagram Protocol) 是一种无连接的、不可靠的、但传输效率较高的传输层协议,它不像 TCP 那样需要建立连接(三次握手),直接发送数据包,因此开销小,速度快,适用于对实时性要求高、能容忍少量丢包的场景,如视频会议、在线游戏、DNS 查询等。

(图片来源网络,侵删)
下面我将分步介绍如何使用 C 语言创建 UDP 服务器和客户端。
核心概念与步骤
无论是服务器还是客户端,使用 UDP Socket 的基本步骤都如下:
- 创建套接字:使用
socket()函数创建一个通信端点。 - 绑定地址 (服务器):服务器需要将套接字绑定到一个特定的 IP 地址和端口号,以便客户端知道往哪里发送数据。
- 发送/接收数据:使用
sendto()和recvfrom()函数进行数据传输,这两个函数是 UDP 编程的核心,因为它们需要指定数据要发送到的地址或从哪个地址接收的数据。 - 关闭套接字:通信结束后,使用
close()函数关闭套接字,释放资源。
所需头文件
#include <stdio.h> // 标准输入输出 #include <stdlib.h> // 标准库函数 #include <string.h> // 字符串操作 #include <unistd.h> // POSIX 系统调用 (如 close) #include <sys/socket.h> // Socket 相关函数和结构体 #include <netinet/in.h> // Internet 地址族 (sockaddr_in) #include <arpa/inet.h> // IP 地址转换函数 (inet_pton) #include <errno.h> // 错误码
UDP 服务器示例
服务器负责监听一个端口,接收来自客户端的数据,并可能将响应数据发回。
// udp_server.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>
#include <errno.h>
#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, 会自动是 IPPROTO_UDP)
if ((server_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 设置套接字选项,允许地址重用
// 防止服务器快速重启时 "Address already in use" 错误
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt");
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); // 将端口号从主机字节序转换为网络字节序
// 绑定套接字
if (bind(server_fd, (const 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", PORT);
// 3. 循环接收和发送数据
while (1) {
// 清空缓冲区
memset(buffer, 0, BUFFER_SIZE);
// 4. 接收来自客户端的数据
// recvfrom 会将客户端的地址信息也填充到 client_addr 结构体中
client_len = sizeof(client_addr);
int n = recvfrom(server_fd, (char *)buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&client_addr, &client_len);
if (n < 0) {
perror("recvfrom failed");
continue; // 继续下一次循环,而不是退出
}
// 打印接收到的信息和客户端地址
printf("Received from %s:%d -> %s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buffer);
// 5. 将接收到的数据原样发回 (Echo Server)
sendto(server_fd, (const char *)buffer, n, 0,
(const struct sockaddr *)&client_addr, client_len);
printf("Echoed back to the client.\n");
}
// 6. 关闭套接字 (理论上上面的 while(1) 不会退出)
close(server_fd);
return 0;
}
代码解释:
socket(): 创建了一个 UDP 套接字。setsockopt(): 设置SO_REUSEADDR选项,这是一个好习惯,尤其是在开发调试阶段。bind(): 将套接字绑定到本机的所有网络接口 (INADDR_ANY) 和8080端口。htons()函数将主机字节序的端口号转换为网络字节序(大端序)。recvfrom(): 这是 UDP 接收数据的关键函数。- 它会阻塞,直到有数据到达。
buffer用于存储接收到的数据。client_addr会填充发送方的 IP 地址和端口号。- 返回值是接收到的字节数。
sendto(): 这是 UDP 发送数据的关键函数。- 它需要明确指定要发送到的地址 (
client_addr)。 n是要发送的字节数,我们这里使用接收到的字节数,实现回显功能。
- 它需要明确指定要发送到的地址 (
inet_ntoa(): 将网络地址结构体in_addr中的 IP 地址转换为点分十进制字符串(如 "192.168.1.100")。ntohs(): 将网络字节序的端口号转换回主机字节序,方便打印。
UDP 客户端示例
客户端主动向服务器发送数据,并接收服务器的响应。
// 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>
#include <errno.h>
#define SERVER_PORT 8080
#define SERVER_IP "127.0.0.1" // 本地回环地址,用于测试
#define BUFFER_SIZE 1024
int main() {
int sock_fd;
char buffer[BUFFER_SIZE];
struct sockaddr_in server_addr;
// 1. 创建套接字
if ((sock_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_port = htons(SERVER_PORT); // 服务器端口
// 将点分十进制 IP 地址转换为网络字节序
// inet_pton 是更安全、更现代的函数,比 inet_aton 更好
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("invalid address/ Address not supported");
close(sock_fd);
exit(EXIT_FAILURE);
}
// 3. 与服务器交互
while (1) {
printf("Enter a message to send to the server (or 'exit' to quit): ");
fgets(buffer, BUFFER_SIZE, stdin);
// 去掉末尾的换行符
buffer[strcspn(buffer, "\n")] = 0;
if (strcmp(buffer, "exit") == 0) {
break;
}
// 4. 发送数据到服务器
sendto(sock_fd, (const char *)buffer, strlen(buffer), 0,
(const struct sockaddr *)&server_addr, sizeof(server_addr));
printf("Message sent.\n");
// 5. 接收服务器的响应
memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
int n = recvfrom(sock_fd, (char *)buffer, BUFFER_SIZE, 0,
NULL, NULL); // 客户端不需要关心是谁发回的,所以可以传NULL
if (n < 0) {
perror("recvfrom failed");
continue;
}
printf("Received from server: %s\n", buffer);
}
// 6. 关闭套接字
close(sock_fd);
return 0;
}
代码解释:
socket(): 客户端同样需要创建一个套接字。inet_pton(): 这是将 IP 地址字符串(如 "127.0.0.1")转换为struct in_addr结构体的标准方法,比旧的inet_aton函数支持更广泛的地址格式(如 IPv6)。sendto(): 客户端使用sendto()将数据发送到预先设置好的服务器地址 (server_addr)。recvfrom(): 客户端调用recvfrom()来等待服务器的响应,我们不需要关心响应来自哪个地址(因为我们只连接了一个服务器),所以后两个参数可以传NULL,在实际应用中,你可能需要处理来自不同服务器的响应,这时就需要填充一个sockaddr结构体。
如何编译和运行
-
保存代码:将服务器代码保存为
udp_server.c,客户端代码保存为udp_client.c。 -
编译:打开终端,使用
gcc进行编译。# 编译服务器 gcc udp_server.c -o udp_server # 编译客户端 gcc udp_client.c -o udp_client
-
运行:
-
首先启动服务器:
./udp_server
你会看到输出:
UDP Server listening on port 8080... -
然后在一个新的终端窗口启动客户端:
./udp_client
客户端会提示你输入消息。
-
-
测试:
- 在客户端输入
Hello, Server!并按回车。 - 你会在服务器终端看到接收到的信息,并看到它回显的消息。
- 在客户端终端,你会看到服务器回显的
Hello, Server!。 - 在客户端输入
exit并按回车,客户端程序会退出。
- 在客户端输入
总结与关键点
| 特性 | TCP (流式套接字) | UDP (数据报套接字) |
|---|---|---|
| 连接 | 面向连接,需 connect() |
无连接,直接 sendto() |
| 可靠性 | 可靠,保证数据不丢失、不重复、有序 | 不可靠,可能丢包、重复、乱序 |
| 速度 | 较慢,有连接建立和拥塞控制开销 | 较快,无额外开销 |
| 边界 | 无消息边界,数据流式传输 | 有消息边界,recvfrom() 一次读取一个 sendto() 发送的数据包 |
| 函数 | socket(), bind(), listen(), accept(), read(), write(), close() |
socket(), bind(), sendto(), recvfrom(), close() |
| 适用场景 | 文件传输、网页浏览、邮件等需要高可靠性的场景 | 视频会议、在线游戏、DNS、VoIP等对实时性要求高、能容忍少量丢包的场景 |
对于 UDP 编程,最重要的就是理解并熟练使用 sendto() 和 recvfrom() 这两个函数,它们是 UDP 通信的核心,不要忘记处理网络字节序(htons, htonl, ntohs, ntohl)和错误处理。
