recvfrom 是用于无连接(UDP)套接字编程的核心函数,它不仅可以从网络中接收数据,还能获取发送数据的源地址信息(即“谁”发的)。

(图片来源网络,侵删)
函数原型
recvfrom 函数通常在 <sys/socket.h> 头文件中声明。
#include <sys/socket.h>
#include <unistd.h> // 用于 ssize_t 类型
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
-
返回值:
- 成功时,返回接收到的字节数。
- 如果连接被对端关闭,返回
0。 - 出错时,返回
-1,并设置errno。
-
参数详解:
-
sockfd:
(图片来源网络,侵删)- 接收数据的套接字文件描述符,这个套接字必须是通过
socket()函数创建的,并且已经通过bind()绑定了一个本地端口和地址。
- 接收数据的套接字文件描述符,这个套接字必须是通过
-
buf:- 一个指向缓冲区的指针,接收到的数据将被拷贝到这个缓冲区中,你需要确保这个缓冲区足够大,以存放可能的最大数据包。
-
len:buf指向的缓冲区的长度(以字节为单位)。recvfrom最多读取len个字节的数据。
-
flags:- 控制接收行为的标志位,通常设置为
0,表示使用默认行为。 - 常用标志位:
MSG_DONTWAIT: 使recvfrom变为非阻塞模式,如果没有数据可读,函数会立即返回-1,并设置errno为EAGAIN或EWOULDBLOCK,而不是阻塞等待。MSG_PEEK: 查看数据,但不从套接字接收缓冲区中移除数据,下一次调用recvfrom时会再次读到同样的数据。MSG_WAITALL: 等待直到请求的字节数len全部收到,或者发生错误/连接关闭,但这并不保证一定能读取到len个字节(对端关闭连接)。
- 控制接收行为的标志位,通常设置为
-
src_addr:
(图片来源网络,侵删)- 一个指向
struct sockaddr结构体的指针。recvfrom会将发送方的地址信息(IP地址和端口号)填充到这个结构体中。 - 重要提示:如果你不关心发送方的地址,可以将其设置为
NULL。
- 一个指向
-
addrlen:- 这是一个输入/输出参数(value-result argument)。
- 输入:在调用
recvfrom之前,你需要将src_addr指向的结构体的长度赋值给*addrlen(sizeof(struct sockaddr_in))。 - 输出:函数返回后,
*addrlen会被设置为实际填充到src_addr中的地址信息的长度。 - 如果你将
src_addr设置为NULL,addrlen也必须设置为NULL。
-
工作流程
recvfrom 的工作流程可以概括为:
- 检查数据: 检查套接字
sockfd的接收缓冲区中是否有数据。 - 阻塞/非阻塞:
- 如果没有数据且
flags中没有MSG_DONTWAIT,recvfrom会阻塞,直到有数据到达。 - 如果没有数据且设置了
MSG_DONTWAIT,recvfrom会立即返回-1。
- 如果没有数据且
- 接收数据: 如果有数据,它会将数据从内核缓冲区拷贝到用户提供的
buf中,拷贝的字节数不超过len。 - 获取源地址: 它会获取数据包的源 IP 地址和端口号,并将这些信息填充到
src_addr指向的结构体中。 - 返回: 返回实际接收到的字节数。
完整示例代码
下面是一个经典的 UDP 客户端/服务器示例,清晰地展示了 recvfrom 的用法。
服务器端 (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, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 1. 创建套接字
if ((server_fd = socket(AF_INET, SOCK_DGRAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 设置套接字选项,允许地址重用(可选)
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET; // IPv4
address.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用的网络接口
address.sin_port = htons(PORT); // 转换为网络字节序
// 3. 绑定套接字到端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 4. 循环接收数据
while (1) {
// 清空缓冲区
memset(buffer, 0, BUFFER_SIZE);
// 使用 recvfrom 接收数据
// src_addr 不为 NULL,addrlen 初始化为 sizeof(address)
int n = recvfrom(server_fd, (char *)buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&address, (socklen_t *)&addrlen);
if (n < 0) {
perror("recvfrom failed");
// 可以选择继续循环或退出
continue;
}
// 打印接收到的数据和客户端信息
printf("Received from %s:%d\n",
inet_ntoa(address.sin_addr), // 将网络地址转换为字符串
ntohs(address.sin_port)); // 将网络字节序的端口号转换为主机字节序
printf("Message: %s\n", buffer);
// 可以在这里添加回复客户端的逻辑,使用 sendto
// char *response = "Hello from server";
// sendto(server_fd, response, strlen(response), 0, (struct sockaddr *)&address, addrlen);
}
// 5. 关闭套接字 (理论上不会执行到这里)
close(server_fd);
return 0;
}
客户端 (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 = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
// 1. 创建套接字
if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
printf("\n Socket creation error \n");
return -1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 将 IP 地址从字符串转换为网络地址
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
printf("\nInvalid address/ Address not supported \n");
return -1;
}
// 2. 循环发送和接收
while (1) {
printf("Enter message for server: ");
fgets(buffer, BUFFER_SIZE, stdin);
buffer[strcspn(buffer, "\n")] = 0; // 去掉换行符
// 发送数据到服务器
// 注意:对于 UDP 客户端,bind 不是必须的,系统会自动分配一个临时端口
sendto(sock, buffer, strlen(buffer), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
printf("Message sent.\n");
// 接收服务器的回复
// 对于客户端,我们通常不关心是谁发的(因为我们知道是服务器),src_addr 可以设为 NULL
memset(buffer, 0, BUFFER_SIZE);
int n = recvfrom(sock, (char *)buffer, BUFFER_SIZE, 0, NULL, NULL);
if (n > 0) {
printf("Server reply: %s\n", buffer);
} else {
perror("recvfrom failed or no reply");
}
}
close(sock);
return 0;
}
recvfrom vs. recv
这是一个非常常见的问题,理解它们的区别至关重要。
| 特性 | recvfrom |
recv |
|---|---|---|
| 用途 | 无连接 (UDP) 套接字 | 面向连接 (TCP) 套接字 |
| 连接状态 | 不需要 connect(),可以接收来自任何地址的数据 |
需要 connect() 建立连接后才能使用 |
| 源地址 | 可以获取发送方的地址信息 | 不能获取,因为连接已经建立,数据只来自已连接的对端。 |
| 参数 | sockfd, buf, len, flags, src_addr, addrlen |
sockfd, buf, len, flags |
| 本质 | recv 是 recvfrom 的一个特例,当套接字是 SOCK_STREAM (TCP) 类型时,recv 内部会调用 recvfrom,并忽略地址参数。 |
常见错误及处理
-
EAGAIN或EWOULDBLOCK:- 原因: 在非阻塞模式下调用
recvfrom,但当时没有数据可读。 - 处理: 这是正常行为,不是错误,你的程序应该继续执行其他任务,或者稍后再试。
- 原因: 在非阻塞模式下调用
-
ECONNREFUSED:- 原因: (UDP) 发送了一个数据包,但目标端口上没有应用程序在监听,ICMP "端口不可达" 消息会返回给发送方,导致下一次
recvfrom失败。 - 处理: 检查服务器是否正在运行,以及端口号是否正确。
- 原因: (UDP) 发送了一个数据包,但目标端口上没有应用程序在监听,ICMP "端口不可达" 消息会返回给发送方,导致下一次
-
EMSGSIZE:- 原因: 接收到的数据包长度超过了套接字缓冲区的大小限制(由
SO_RCVBUF选项控制)。 - 处理: 增大套接字缓冲区大小,或者确保发送方不会发送过大的数据包。
- 原因: 接收到的数据包长度超过了套接字缓冲区的大小限制(由
-
EFAULT:- 原因:
buf或src_addr指向了无效的内存地址。 - 处理: 检查指针是否正确初始化。
- 原因:
希望这份详细的解释能帮助你完全掌握 C 语言中的 recvfrom 函数!
