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

(图片来源网络,侵删)
下面我们将通过一个完整的“回显服务器”(Echo Server)和客户端的例子来学习 UDP 编程的核心步骤。
核心概念
在开始编码前,先了解几个关键的系统调用和数据结构:
socket(): 创建一个套接字,这是进行网络通信的端点。bind(): 将套接字与一个特定的 IP 地址和端口号绑定,对于服务器来说,这是必须的,这样客户端才知道数据该发到哪里。sendto(): 发送数据,UDP 是无连接的,所以每次发送都需要指定目标地址(IP 和端口)。recvfrom(): 接收数据,UDP 也是无连接的,所以接收时不仅能拿到数据,还能知道数据来自哪个地址(谁发的)。struct sockaddr_in: 用于处理 IPv4 地址的结构体,它包含了端口号、IP 地址等信息。inet_addr()/inet_pton(): 将点分十进制的 IP 地址字符串(如 "127.0.0.1")转换为网络字节序的二进制格式。- htons() / ntohs(): 主机字节序和网络字节序之间的转换,网络协议规定多字节数据(如端口号)必须使用网络字节序(大端序),而不同 CPU 的主机字节序可能不同(小端序或大端序),因此必须进行转换。
完整示例:UDP 回显服务器和客户端
这个例子包含两个程序:
udp_server.c: 一个服务器,它接收客户端发来的任何消息,然后将原消息发回给客户端。udp_client.c: 一个客户端,它从命令行读取消息,发送给服务器,并打印服务器返回的消息。
服务器端代码 (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>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
char buffer[BUFFER_SIZE] = {0};
socklen_t client_addr_len = sizeof(client_addr);
// 1. 创建套接字 (socket)
// 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);
}
printf("Socket created successfully.\n");
// 2. 绑定地址和端口 (bind)
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");
exit(EXIT_FAILURE);
}
printf("Server bound to port %d.\n", PORT);
// 3. 循环接收和发送数据 (recvfrom & sendto)
while (1) {
printf("Waiting for a message...\n");
// 从客户端接收数据
// recvfrom 会将客户端的地址信息存入 client_addr
int n = recvfrom(server_fd, (char *)buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&client_addr, &client_addr_len);
if (n < 0) {
perror("recvfrom failed");
continue; // 继续等待下一个消息
}
buffer[n] = '\0'; // 确保字符串正确终止
// 打印接收到的消息和客户端信息
printf("Received from %s:%d -> %s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buffer);
// 将接收到的消息回显给客户端
sendto(server_fd, (const char *)buffer, n, 0,
(const struct sockaddr *)&client_addr, client_addr_len);
printf("Echo message sent back.\n");
}
// 4. 关闭套接字 (close)
// 由于代码在 while(1) 循环中,这里理论上不会执行
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 PORT 8080
#define SERVER_IP "127.0.0.1" // 本地回环地址
#define BUFFER_SIZE 1024
int main() {
int sock_fd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE] = {0};
// 1. 创建套接字 (socket)
if ((sock_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
printf("Socket created successfully.\n");
// 2. 设置服务器地址信息 (不需要 bind,系统会自动分配)
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
// 将 IP 地址字符串转换为网络字节序的二进制格式
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("invalid address/ Address not supported");
exit(EXIT_FAILURE);
}
// 3. 循环读取用户输入并发送/接收
while (1) {
printf("Enter a message to send (or 'exit' to quit): ");
fgets(buffer, BUFFER_SIZE, stdin);
// 去掉末尾的换行符
buffer[strcspn(buffer, "\n")] = 0;
if (strcmp(buffer, "exit") == 0) {
break;
}
// 发送数据到服务器
sendto(sock_fd, (const char *)buffer, strlen(buffer), 0,
(const struct sockaddr *)&server_addr, sizeof(server_addr));
printf("Message sent to server.\n");
// 从服务器接收回显消息
int n = recvfrom(sock_fd, (char *)buffer, BUFFER_SIZE, 0,
NULL, NULL); // 客户端不需要关心返回地址
if (n < 0) {
perror("recvfrom failed");
continue;
}
buffer[n] = '\0';
printf("Received from server -> %s\n", buffer);
}
// 4. 关闭套接字 (close)
close(sock_fd);
return 0;
}
编译和运行
-
保存代码: 将上面的代码分别保存为
udp_server.c和udp_client.c。
(图片来源网络,侵删) -
编译: 使用 GCC 编译器进行编译。
# 编译服务器 gcc udp_server.c -o udp_server # 编译客户端 gcc udp_client.c -o udp_client
-
运行:
-
首先在一个终端窗口启动服务器:
./udp_server
你会看到输出:
(图片来源网络,侵删)Socket created successfully. Server bound to port 8080. Waiting for a message... -
然后在另一个终端窗口启动客户端:
./udp_client
你会看到输出:
Socket created successfully. Enter a message to send (or 'exit' to quit): -
在客户端输入消息并发送:
Enter a message to send (or 'exit' to quit): Hello, UDP Server! Message sent to server. Received from server -> Hello, UDP Server! Enter a message to send (or 'exit' to quit): -
在服务器端观察输出:
Waiting for a message... Received from 127.0.0.1:54321 -> Hello, UDP Server! Echo message sent back. Waiting for a message...(注意:客户端的端口号
54321每次运行可能会不同) -
在客户端输入
exit即可退出程序,服务器会继续运行,等待下一个客户端的连接。
-
UDP 编程流程总结
| 角色 | 步骤 | 函数 | 说明 |
|---|---|---|---|
| 服务器 | 创建套接字 | socket() |
创建通信端点。 |
| 绑定地址和端口 | bind() |
告诉系统这个套接字负责哪个 IP 和端口,这是服务器的关键步骤。 | |
| 接收数据 | recvfrom() |
阻塞等待,直到收到数据包,并获取发送方的地址信息。 | |
| (可选) 发送数据 | sendto() |
将处理后的数据发回给特定的客户端地址。 | |
| 关闭套接字 | close() |
释放资源。 | |
| 客户端 | 创建套接字 | socket() |
创建通信端点。 |
(不需要 bind) |
客户端通常不需要 bind,操作系统会自动为它分配一个临时的、可用的端口。 |
||
| 发送数据 | sendto() |
将数据发送到已知的服务器地址(IP + 端口)。 | |
| 接收数据 | recvfrom() |
阻塞等待,接收服务器的响应。 | |
| 关闭套接字 | close() |
释放资源。 |
关键区别:UDP vs. TCP
| 特性 | UDP | TCP |
|---|---|---|
| 连接性 | 无连接 | 面向连接(需三次握手) |
| 可靠性 | 不可靠(不保证不丢失、不重复、不乱序) | 可靠(通过确认、重传、排序机制保证) |
| 传输效率 | 高(开销小,无连接建立和拆除过程) | 相对较低(有连接开销和流量控制) |
| 数据形式 | 数据报(Datagram),有边界 | 字节流(Stream),无边界 |
| 适用场景 | 实时应用(视频、游戏)、DNS查询、广播 | 文件传输、网页浏览、邮件等要求可靠性的场景 |
希望这个详细的教程能帮助你理解 Linux C 语言下的 UDP 编程!
