UDP 是一种无连接的、不可靠的、基于数据报的传输协议,相比于 TCP,它的开销更小,速度更快,但数据包可能丢失、重复或乱序,它适用于对实时性要求高、能容忍少量丢包的场景,如视频会议、在线游戏、DNS 查询等。
下面我将分为三个部分:
- 核心 API 函数介绍
- 完整的服务端和客户端示例
- 代码详解与常见问题
核心 API 函数
在 C 语言中,Socket 编程主要使用 <sys/socket.h> 和 <netinet/in.h> 等头文件提供的函数。
| 函数 | 功能 | 参数 |
|---|---|---|
socket() |
创建一个套接字,返回一个文件描述符。 | domain (地址族, 如 AF_INET), type (套接字类型, 如 SOCK_DGRAM), protocol (协议, 如 IPPROTO_UDP) |
bind() |
将套接字与一个特定的 IP 地址和端口号绑定。 | sockfd (套接字描述符), addr (指向 sockaddr 结构的指针), addrlen (地址结构体长度) |
sendto() |
通过 UDP 套接字发送数据,数据包的目标地址和端口在函数中指定。 | sockfd, buf (数据缓冲区), len (数据长度), flags, dest_addr (目标地址), addrlen |
recvfrom() |
通过 UDP 套接字接收数据,可以获取到数据包的来源地址和端口。 | sockfd, buf (接收缓冲区), len (缓冲区长度), flags, src_addr (来源地址), addrlen (地址结构体长度) |
close() |
关闭套接字,释放资源。 | sockfd |
关键结构体:
struct sockaddr: 通用地址结构体,作为bind,sendto,recvfrom的参数类型。struct sockaddr_in: 针对 IPv4 的地址结构体,我们通常用它来填充地址信息,然后通过强制类型转换传递给通用地址结构。
struct sockaddr_in {
short sin_family; // 地址族 (Address Family), AF_INET
unsigned short sin_port; // 端口号 (Network Byte Order)
struct in_addr sin_addr; // IP 地址
char sin_zero[8]; // 填充字节,保持与 struct sockaddr 大小一致
};
struct in_addr {
long s_addr; // IPv4 地址,以网络字节序存储
};
字节序转换:
网络数据流采用大端字节序(Big-Endian),而 x86 架构的计算机使用小端字节序(Little-Endian),在设置端口号和 IP 地址时,必须进行转换。
htons(): Host to Network Short (16位)htonl(): Host to Network Long (32位)ntohs(): Network to Host Short (16位)ntohl(): Network to Host Long (32位)
完整示例:回显服务器
这是一个经典的例子:客户端发送一条消息给服务器,服务器收到后将原消息发回给客户端。
服务端代码 (udp_server.c)
服务端需要绑定一个固定的 IP 和端口,等待客户端的请求。
#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 sockfd;
char buffer[BUFFER_SIZE];
struct sockaddr_in servaddr, cliaddr;
// 1. 创建 UDP socket
// AF_INET: IPv4
// SOCK_DGRAM: UDP
// 0: 自动选择协议 (IPPROTO_UDP)
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 2. 设置服务器地址信息
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET; // IPv4
servaddr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
servaddr.sin_port = htons(PORT); // 端口号,转换为网络字节序
// 3. 将 socket 与服务器地址绑定
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
int len, n;
len = sizeof(cliaddr); // cliaddr 的大小
// 4. 循环接收客户端数据
while (1) {
// recvfrom 会阻塞,直到收到数据
// buffer: 存储接收到的数据
// len: buffer 的大小
// cliaddr: 存储客户端的地址信息
n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&cliaddr, &len);
// 将接收到的数据以字符串形式结束
buffer[n] = '\0';
printf("Message from client: %s\n", buffer);
printf("Client IP: %s, Port: %d\n",
inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
// 5. 将收到的数据原样发回给客户端
sendto(sockfd, (const char *)buffer, n, 0,
(const struct sockaddr *)&cliaddr, len);
printf("Echo message sent.\n");
}
// 6. 关闭 socket (理论上上面的 while(1) 不会退出)
close(sockfd);
return 0;
}
客户端代码 (udp_client.c)
客户端不需要绑定固定端口,操作系统会自动分配一个临时端口,它只需要知道服务器的 IP 和端口即可发送数据。
#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 sockfd;
char buffer[BUFFER_SIZE];
struct sockaddr_in servaddr;
// 1. 创建 UDP socket
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 2. 设置服务器地址信息
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
// 将点分十进制 IP 地址转换为网络字节序的 32 位整数
if (inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr) <= 0) {
perror("invalid address/ Address not supported");
close(sockfd);
exit(EXIT_FAILURE);
}
// 3. 获取用户输入并发送
while (1) {
printf("Enter message to send (or 'exit' to quit): ");
fgets(buffer, BUFFER_SIZE, stdin);
// 去掉 fgets 读取的换行符
buffer[strcspn(buffer, "\n")] = 0;
if (strcmp(buffer, "exit") == 0) {
break;
}
// 发送数据到服务器
sendto(sockfd, (const char *)buffer, strlen(buffer), 0,
(const struct sockaddr *)&servaddr, sizeof(servaddr));
printf("Message sent.\n");
// 接收服务器的回显
int n;
socklen_t len = sizeof(servaddr);
n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&servaddr, &len);
buffer[n] = '\0';
printf("Echo from server: %s\n", buffer);
}
// 4. 关闭 socket
close(sockfd);
return 0;
}
代码详解与常见问题
如何编译和运行?
-
将上述两段代码分别保存为
udp_server.c和udp_client.c。 -
打开终端,使用 GCC 进行编译:
# 编译服务端 gcc udp_server.c -o server # 编译客户端 gcc udp_client.c -o client
-
先运行服务端,它会开始监听端口并等待连接:
./server # 输出: Server listening on port 8080...
-
再运行客户端,它会连接到服务器并发送消息:
./client # 输出: Enter message to send (or 'exit' to quit):
在客户端输入
hello world,然后按回车,你会看到:客户端终端:
Enter message to send (or 'exit' to quit): hello world Message sent. Echo from server: hello world Enter message to send (or 'exit' to quit):服务端终端:
Server listening on port 8080... Message from client: hello world Client IP: 127.0.0.1, Port: 54321 // 端口号是动态分配的 Echo message sent.
常见问题与注意事项
-
为什么服务端用
INADDR_ANY?INADDR_ANY(值为 0) 是一个通配符地址,表示服务器接受发送到其所有网络接口(如0.0.1,168.1.100等)上的指定端口的 UDP 数据包,这使得服务器无论通过哪个 IP 地址都能被访问,非常灵活。 -
recvfrom的最后一个参数addrlen这个参数比较特殊,在调用recvfrom之前,你需要将cliaddr结构体的大小赋给它,在调用之后,它会被修改为实际写入的地址结构体的大小,这是一个“值-结果”(value-result)参数,通常的做法是定义一个变量len = sizeof(cliaddr),然后将&len传递给函数。 -
UDP 是无连接的 UDP 的
sendto和recvfrom是成对出现的,你不需要像 TCP 那样先connect,每次sendto都可以指定不同的目标地址,每次recvfrom也都可以从不同的来源接收数据。 -
缓冲区大小
BUFFER_SIZE定义了能接收的最大数据包大小,如果客户端发送的数据超过了这个大小,超出部分的数据将被丢弃,可能会导致数据不完整,对于 UDP,需要应用层自己处理分包和组包的逻辑。 -
错误处理 示例中的
perror是一个很好的错误处理方式,它会打印出函数名和系统返回的错误信息(如 "Address already in use"),在实际生产环境中,健壮的错误处理至关重要。 -
防火墙 如果程序无法连接,请检查系统的防火墙设置,防火墙可能会阻止特定端口(如 8080)的流量,你可能需要配置防火墙规则来允许 UDP 流量通过该端口。
