- 核心概念与函数:介绍 UDP 编程需要用到的主要数据结构和函数。
- 完整服务端/客户端示例:提供一个可运行的、带详细注释的代码示例。
- 代码编译与运行:说明如何编译和运行这些程序。
- 常见问题与注意事项:总结一些开发中需要注意的点。
核心概念与函数
1 头文件
你需要包含以下头文件:

(图片来源网络,侵删)
#include <stdio.h> // 标准输入输出 #include <stdlib.h> // 标准库函数 #include <string.h> // 字符串操作 #include <unistd.h> // POSIX 系统调用 (如 close) #include <sys/socket.h> // 套接字相关的系统调用和结构体 #include <netinet/in.h> // IP 地址和端口的结构体定义 #include <arpa/inet.h> // IP 地址转换函数 (如 inet_pton)
2 关键数据结构
struct sockaddr: 通用套接字地址结构,作为参数传递给函数时,会被强制转换。struct sockaddr_in: 用于 IPv4 的套接字地址结构,包含端口号和 IP 地址。struct sockaddr_in { short sin_family; // 地址族 (AF_INET) unsigned short sin_port; // 16位端口号,需要用 htons() 转换 struct in_addr sin_addr; // 32位 IP 地址 unsigned char sin_zero[8]; // 填充字段,不使用 };struct in_addr: IPv4 地址结构。// in_addr 是一个联合体,通常用 s_addr 成员 typedef uint32_t in_addr_t; struct in_addr { in_addr_t s_addr; };
3 核心函数
socket(): 创建一个套接字。int socket(int domain, int type, int protocol); // domain: 地址族 (AF_INET for IPv4) // type: 套接字类型 (SOCK_DGRAM for UDP) // protocol: 协议 (对于 SOCK_DGRAM 和 AF_INET,通常为 0) // 返回值: 成功返回套接字描述符(一个整数),失败返回 -1
bind(): 将套接字与一个特定的 IP 地址和端口号绑定。int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // sockfd: socket() 返回的套接字描述符 // addr: 指向 sockaddr_in 结构体的指针,包含要绑定的 IP 和端口 // addrlen: addr 结构体的长度 // 返回值: 成功返回 0,失败返回 -1
sendto(): 通过 UDP 发送数据,它需要指定数据要发送到的目标地址。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: 指向目标 sockaddr_in 结构体的指针 // addrlen: 目标地址结构体的长度 // 返回值: 成功返回发送的字节数,失败返回 -1recvfrom(): 通过 UDP 接收数据,它会返回数据以及发送方的地址信息。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: 用于存储发送方地址的 sockaddr_in 结构体指针 // addrlen: 指向 src_addr 结构体长度的指针(传入时是 sizeof(struct sockaddr_in),返回时会被系统填入实际长度) // 返回值: 成功返回接收到的字节数,失败返回 -1,连接对端关闭时返回 0 (TCP 特性,UDP 不适用)- `htons() / htonl()**: 主机字节序到网络字节序的转换。
htons(): host to network short (16位)htonl(): host to network long (32位) 网络字节序是大端序,而不同主机的字节序可能不同,在设置端口号和 IP 地址时必须使用这些函数。
inet_pton() / inet_ntop(): IP 地址的点分十进制字符串与网络字节序的二进制格式之间的转换。inet_pton(): presentation to numeric (字符串 -> 二进制)inet_ntop(): numeric to presentation (二进制 -> 字符串)
完整服务端/客户端示例
这个例子实现了一个简单的回显服务器:客户端发送一条消息,服务器收到后,再将原消息发送回客户端。
1 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. 创建套接字
// AF_INET: IPv4
// SOCK_DGRAM: UDP
// 0: 自动选择协议
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. 绑定地址和端口
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_port = htons(PORT); // 端口号,转换为网络字节序
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
// 绑定套接字
if (bind(server_fd, (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) {
// 接收客户端数据
// recvfrom 会阻塞,直到收到数据
int n = recvfrom(server_fd, 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, buffer, strlen(buffer), 0,
(struct sockaddr *)&client_addr, client_addr_len);
printf("Echoed back to client.\n");
}
// 4. 关闭套接字 (虽然上面的 while(1) 永远不会退出)
close(server_fd);
return 0;
}
2 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. 创建套接字
if ((sock_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 设置服务器地址
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);
}
// 2. 循环发送和接收数据
while (1) {
printf("Enter a message to send (or 'exit' to quit): ");
fgets(buffer, BUFFER_SIZE, stdin);
// 如果输入 "exit",则退出程序
if (strncmp(buffer, "exit", 4) == 0) {
break;
}
// 发送数据到服务器
sendto(sock_fd, buffer, strlen(buffer), 0,
(struct sockaddr *)&server_addr, sizeof(server_addr));
printf("Message sent.\n");
// 接收服务器的回显
int n = recvfrom(sock_fd, buffer, BUFFER_SIZE, 0, NULL, NULL);
// 注意:这里我们不需要知道是谁发来的,因为我们只和一个服务器通信
// 所以可以传入 NULL 和 NULL
if (n < 0) {
perror("recvfrom failed");
continue;
}
buffer[n] = '\0';
printf("Received from server -> %s\n", buffer);
}
// 3. 关闭套接字
close(sock_fd);
return 0;
}
代码编译与运行
-
保存文件:将服务端代码保存为
server.c,客户端代码保存为client.c。 -
编译:打开终端,使用
gcc进行编译,需要链接pthread库(虽然这个例子没用,但通常网络程序会用到,养成好习惯)。# 编译服务端 gcc server.c -o server # 编译客户端 gcc client.c -o client
-
运行:
(图片来源网络,侵删)-
首先启动服务端:
./server
你会看到输出:
Server listening on port 8080... -
然后启动客户端(可以在新的终端窗口中启动):
./client
客户端会提示你输入消息。
(图片来源网络,侵删) -
交互示例:
# 客户端终端 Enter a message to send (or 'exit' to quit): Hello, UDP Server! Message sent. Received from server -> Hello, UDP Server! Enter a message to send (or 'exit' to quit): This is a test. Message sent. Received from server -> This is a test. Enter a message to send (or 'exit' to quit): exit -
服务端终端会显示:
Server listening on port 8080... Received from 127.0.0.1:54321 -> Hello, UDP Server! Echoed back to client. Received from 127.0.0.1:54321 -> This is a test. Echoed back to client.
-
常见问题与注意事项
-
无连接性:
- UDP 是无连接的。
sendto的第一次调用并不会建立连接,它只是把数据包发出去。recvfrom也可以接收来自任何地址的数据包,这使得 UDP 编程比 TCP 简单,但也需要开发者自己处理地址管理和数据包的来源验证。
- UDP 是无连接的。
-
不可靠性:
UDP 不保证数据包的顺序,也不保证数据包一定能到达,如果应用层需要可靠传输,必须自己实现序列号、确认、重传等机制(在 UDP 之上实现一个简单的可靠协议)。
-
数据包大小限制:
UDP 数据包有理论上的最大大小(约 65,507 字节,包括 8 字节的 UDP 头和 20 字节的 IP 头),在以太网等链路上,实际传输的有效载荷通常限制在 1472 字节左右,以避免分片,发送大数据时需要进行分片和重组。
-
阻塞 vs. 非阻塞:
recvfrom默认是阻塞的,即如果没有数据到达,程序会一直等待,可以通过fcntl或ioctl等函数将套接字设置为非阻塞模式,这样recvfrom会立即返回,如果没有数据则返回错误码EAGAIN或EWOULDBLOCK。
-
广播与多播:
- UDP 支持广播和多播,通过设置
sin_addr.s_addr为INADDR_BROADCAST(广播地址) 或加入多播组,一个数据包可以发送到网络中的多个主机。
- UDP 支持广播和多播,通过设置
-
错误处理:
- 网络编程中,系统调用(如
sendto,recvfrom)可能会因为各种原因失败(网络中断、目标不可达等),必须检查它们的返回值,并根据错误码(通过perror或strerror打印)进行适当的错误处理。
- 网络编程中,系统调用(如
