我们将分步介绍,从最基础的 socket 创建,到发送和接收数据,最后是一个完整的客户端/服务器示例。

(图片来源网络,侵删)
核心概念与流程
UDP 编程的核心是 套接字,它是一个通信端点,应用程序通过它发送和接收数据。
服务器端流程
- 创建套接字:使用
socket()函数创建一个 UDP 套接字。 - 绑定地址和端口:使用
bind()函数将套接字与一个特定的 IP 地址和端口号绑定,这样客户端才知道数据该发到哪里。 - 接收数据:使用
recvfrom()函数在一个循环中等待接收来自客户端的数据,这个函数会阻塞程序,直到有数据到达。 - 处理数据并发送响应:对收到的数据进行处理后,可以使用
sendto()函数将响应数据发回给客户端。 - 关闭套接字:通信结束后,使用
close()函数关闭套接字。
客户端流程
- 创建套接字:同样使用
socket()函数创建一个 UDP 套接字。 - 发送数据:使用
sendto()函数将数据发送到服务器的 IP 地址和端口号,客户端不需要绑定,系统会自动分配一个临时端口。 - 接收响应:使用
recvfrom()函数接收服务器的响应。 - 关闭套接字:通信结束后,使用
close()函数关闭套接字。
关键函数详解
socket()
创建一个套接字。
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int socket(int domain, int type, int protocol);
domain: 地址族,对于网络通信,使用AF_INET(IPv4) 或AF_INET6(IPv6)。type: 套接字类型,对于 UDP,使用SOCK_DGRAM(数据报)。protocol: 协议,当 type 为SOCK_DGRAM时,通常设置为IPPROTO_UDP。- 返回值:成功返回一个套接字描述符(一个非负整数),失败返回 -1。
bind()
将套接字与一个本地地址(IP 和端口)绑定。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:socket()返回的套接字描述符。addr: 指向sockaddr结构体的指针,对于 IPv4,我们通常使用struct sockaddr_in,然后将其地址强制转换为struct sockaddr *。addrlen:addr结构体的长度,即sizeof(struct sockaddr_in)。- 返回值:成功返回 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: 指向目标地址(struct sockaddr_in)的指针。addrlen: 目标地址的长度,即sizeof(struct sockaddr_in)。- 返回值:成功返回实际发送的字节数,失败返回 -1。
recvfrom()
通过 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: 指向一个struct sockaddr_in结构体的指针,用于存储发送方的地址信息,如果不需要,可以设为NULL。addrlen: 指向一个socklen_t变量的指针,它应该在调用时设置为sizeof(struct sockaddr_in),调用后会被修改为实际地址的长度。- 返回值:成功返回接收到的字节数,如果对端关闭连接,返回 0(UDP 本身无连接,此情况较少见),失败返回 -1。
sockaddr_in 结构体
这是 IPv4 地址的核心结构体,定义在 <netinet/in.h> 中。
struct sockaddr_in {
short sin_family; // 地址族,必须设置为 AF_INET
unsigned short sin_port; // 端口号,需要用 htons() 转换
struct in_addr sin_addr; // IP 地址,需要用 inet_addr() 或 inet_pton() 转换
char sin_zero[8]; // 填充字节,必须为 0
};
// in_addr 结构体只包含一个成员
struct in_addr {
unsigned long s_addr; // 32 位 IPv4 地址
}
重要函数:
htons(): Host to Network Short (16位),将主机字节序转换为网络字节序(大端序)。htonl(): Host to Network Long (32位)。inet_addr(): 将点分十进制的 IP 地址字符串(如 "127.0.0.1")转换为in_addr结构体所需的格式。inet_pton(): 更现代、更安全的 IP 地址转换函数,支持 IPv4 和 IPv6。
完整代码示例
下面是一个简单的 "Echo" (回声) 服务器和客户端的例子,客户端发送一条消息,服务器接收后原样返回。

(图片来源网络,侵删)
服务器端代码 (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 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_addr.s_addr = INADDR_ANY; // 绑定到所有可用的网络接口
server_addr.sin_port = htons(PORT); // 端口号,转换为网络字节序
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
printf("UDP Server listening on port %d...\n", PORT);
// 3. 循环接收数据
while (1) {
// 清空缓冲区
memset(buffer, 0, BUFFER_SIZE);
// 4. 接收客户端数据
// recvfrom 会阻塞,直到收到数据
int n = recvfrom(server_fd, buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&client_addr, &client_addr_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. 将接收到的数据回发客户端
sendto(server_fd, buffer, strlen(buffer), 0,
(struct sockaddr *)&client_addr, client_addr_len);
}
// 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 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};
char *message = "Hello from UDP Client!";
// 1. 创建套接字
if ((sock_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 设置服务器地址信息
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("inet_pton failed");
exit(EXIT_FAILURE);
}
// 3. 发送数据到服务器
sendto(sock_fd, message, strlen(message), 0,
(struct sockaddr *)&server_addr, sizeof(server_addr));
printf("Message sent to server.\n");
// 4. 接收服务器的响应
int n = recvfrom(sock_fd, buffer, BUFFER_SIZE, 0, NULL, NULL);
// 注意:如果不需要知道是谁发的,可以把 src_addr 和 addrlen 设为 NULL
if (n < 0) {
perror("recvfrom failed");
exit(EXIT_FAILURE);
}
printf("Server replied: %s\n", buffer);
// 5. 关闭套接字
close(sock_fd);
return 0;
}
如何编译和运行
-
保存代码:将上述两段代码分别保存为
udp_server.c和udp_client.c。 -
编译:打开终端,使用
gcc进行编译,需要链接pthread库(虽然这个例子不用,但网络编程常涉及,养成好习惯)。# 编译服务器 gcc udp_server.c -o udp_server # 编译客户端 gcc udp_client.c -o udp_client
-
运行:
- 第一步:在一个终端窗口中启动服务器,服务器会一直运行,等待客户端连接。
./udp_server
你会看到输出:
UDP Server listening on port 8080... - 第二步:在另一个终端窗口中启动客户端。
./udp_client
- 第三步:观察两个终端的输出。
- 客户端终端会显示:
Message sent to server. Server replied: Hello from UDP Client! - 服务器终端会显示:
Received from 127.0.0.1:54321 -> Hello from UDP Client!(注意:客户端的端口号
54321是系统临时分配的,每次运行可能不同)
- 客户端终端会显示:
- 第一步:在一个终端窗口中启动服务器,服务器会一直运行,等待客户端连接。
常见问题与注意事项
- 字节序:在将端口号和 IP 地址填充到
sockaddr_in结构体中时,必须使用htons()和inet_addr()/inet_pton()等函数将主机字节序转换为网络字节序,这是一个非常常见的错误。 - 地址已在使用:如果服务器程序异常退出,端口可能处于
TIME_WAIT状态,导致下次启动时bind失败,解决方法是在bind之前使用setsockopt设置SO_REUSEADDR选项,如示例代码所示。 recvfrom的阻塞:recvfrom是一个阻塞函数,如果没有数据到达,程序会卡在这里,如果需要非阻塞方式,可以使用fcntl或ioctlsocket(Windows) 将套接字设置为非阻塞模式。- 无连接性:UDP 是无连接的。
sendto可以向任何地址发送数据,而无需事先建立连接,同样,recvfrom可以从任何地址接收数据。 - 数据大小限制:UDP 数据报有理论上的最大长度(约 64KB),但实际网络中为了分片和重组的效率,建议限制在 512 字节到 1472 字节之间(对于以太网 MTU 为 1500 字节的情况)。
- 错误处理:网络编程中,函数调用失败是常态,务必检查每个函数的返回值,并根据错误码(通过
perror打印)进行适当的错误处理。
