我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 sendto
和 recvfrom
函数,它们是用于无连接(数据报)套接字(如 UDP)进行数据传输的核心系统调用,但也可以用于面向连接(流式)套接字。
1. 函数介绍
sendto
和 recvfrom
是 Linux 系统调用,专门设计用于在套接字上传输数据报(datagrams)。它们与 send
/write
和 recv
/read
的主要区别在于:sendto
和 recvfrom
显式地处理目标地址和源地址。
sendto
: 将数据从套接字发送到指定的目标地址。对于 UDP 套接字,这会创建一个数据报并发送到指定的主机和端口。对于 TCP 套接字,如果尚未连接,调用会失败。recvfrom
: 从套接字接收数据,并获取数据的来源地址(发送方的 IP 地址和端口号)。对于 UDP 套接字,这会接收一个数据报。对于 TCP 套接字,地址信息通常不被使用,因为连接是点对点的。
你可以把它们想象成写信和收信:
sendto
: 你写一封信(数据),并在信封上明确写上收信人的地址(目标地址),然后投递出去。recvfrom
: 你收到一封信(数据),信封上写着寄件人的地址(源地址),你可以知道是谁寄来的。
2. 函数原型
#include <sys/socket.h> // 必需
// 发送数据到指定地址
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
// 从套接字接收数据,并获取源地址
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
3. 功能
sendto
:- 通过套接字
sockfd
发送len
个字节的数据(从buf
指向的缓冲区)。 - 数据被发送到由
dest_addr
和addrlen
指定的目标地址。 - 对于数据报(如 UDP)套接字,这会创建一个独立的数据报。
- 通过套接字
recvfrom
:- 通过套接字
sockfd
接收最多len
个字节的数据,并将其存储在buf
指向的缓冲区中。 - 如果
src_addr
和addrlen
非 NULL,则将发送方的地址信息填充到src_addr
指向的结构体中,并更新*addrlen
为实际地址结构的大小。
- 通过套接字
4. 参数
这两个函数的参数非常相似,分别处理发送和接收。
sendto
int sockfd
: 有效的套接字文件描述符。const void *buf
: 指向包含要发送数据的缓冲区的指针。size_t len
: 要发送的字节数。int flags
: 控制发送行为的标志位。常见的有:0
: 使用默认行为。MSG_DONTWAIT
: 使发送操作非阻塞(如果套接字是阻塞的)。MSG_NOSIGNAL
: 在面向连接的套接字上,如果连接断开,不产生SIGPIPE
信号。
const struct sockaddr *dest_addr
: 指向目标地址结构的指针(例如sockaddr_in
或sockaddr_in6
)。socklen_t addrlen
:dest_addr
指向的地址结构的大小。
recvfrom
int sockfd
: 有效的套接字文件描述符。void *buf
: 指向用于存储接收数据的缓冲区的指针。size_t len
: 缓冲区buf
的大小,也是期望接收的最大字节数。int flags
: 控制接收行为的标志位。常见的有:0
: 使用默认行为。MSG_DONTWAIT
: 使接收操作非阻塞(如果套接字是阻塞的)。MSG_PEEK
: 查看传入的数据,但不从输入队列中移除。MSG_WAITALL
: 请求等待,直到读入请求的字节数。但当检测到错误或断开连接时,或套接字为非阻塞时,仍可能返回少于请求字节数的数据。
struct sockaddr *src_addr
:- 如果不需要获取发送方地址,可以传入
NULL
。 - 如果需要获取发送方地址,应传入指向
sockaddr_in
或sockaddr_in6
等结构的指针。
- 如果不需要获取发送方地址,可以传入
socklen_t *addrlen
:- 如果
src_addr
是NULL
,则addrlen
也必须是NULL
。 - 如果
src_addr
非NULL
,则addrlen
必须指向一个socklen_t
变量。 - 输入: 调用时,该变量应初始化为
src_addr
指向的缓冲区的大小(例如sizeof(struct sockaddr_in)
)。 - 输出: 返回时,该变量被更新为实际存储在
src_addr
中的地址结构的大小。
- 如果
5. 返回值
- 成功时:
sendto
: 返回实际发送的字节数。对于数据报套接字,这通常等于len
(如果成功发送了整个数据报)。recvfrom
: 返回实际接收的字节数。如果返回 0,可能表示对端已关闭连接(对于面向连接的套接字)或收到了一个零长度的数据报(对于数据报套接字,理论上可能)。
- 失败时:
- 两个函数在失败时都返回 -1,并设置全局变量
errno
来指示具体的错误原因(例如EAGAIN
/EWOULDBLOCK
非阻塞套接字上无数据可读/写,ECONNREFUSED
远程主机拒绝连接,EINTR
调用被信号中断等)。
- 两个函数在失败时都返回 -1,并设置全局变量
6. 相似函数,或关联函数
send
/write
: 用于发送数据,但不指定目标地址(通常用于已连接的套接字)。recv
/read
: 用于接收数据,但不获取源地址信息(通常用于已连接的套接字)。connect
: 对于数据报套接字,connect
可以设置默认目标地址,之后就可以使用send
/write
和recv
/read
而无需指定地址。socket
/bind
/sendto
/recvfrom
: 构成了 UDP 网络编程的基本工具集。
7. 示例代码
示例 1:UDP 客户端 (使用 sendto
和 recvfrom
)
这个例子演示了一个 UDP 客户端如何使用 sendto
向服务器发送消息,并使用 recvfrom
接收服务器的回复,同时获取服务器的地址。
// udp_client.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SERVER_PORT 8081
#define SERVER_IP "127.0.0.1"
#define BUFFER_SIZE 1024
int main() {
int sock;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
char *message = "Hello UDP Server!";
char buffer[BUFFER_SIZE];
ssize_t bytes_sent, bytes_received;
// 1. 创建 UDP 套接字
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
printf("UDP client socket created (fd: %d)\n", sock);
// 2. 配置服务器地址结构
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
fprintf(stderr, "Invalid address/ Address not supported\n");
close(sock);
exit(EXIT_FAILURE);
}
// 3. 发送数据到服务器 (使用 sendto)
printf("Sending message to %s:%d\n", SERVER_IP, SERVER_PORT);
bytes_sent = sendto(sock, message, strlen(message), 0,
(const struct sockaddr *)&server_addr, sizeof(server_addr));
if (bytes_sent < 0) {
perror("sendto failed");
close(sock);
exit(EXIT_FAILURE);
} else {
printf("Sent %zd bytes: %s\n", bytes_sent, message);
}
// 4. 接收服务器的回复 (使用 recvfrom 并获取源地址)
printf("Waiting for reply from server...\n");
bytes_received = recvfrom(sock, buffer, BUFFER_SIZE - 1, 0,
(struct sockaddr *)&client_addr, &client_addr_len);
if (bytes_received < 0) {
perror("recvfrom failed");
close(sock);
exit(EXIT_FAILURE);
} else {
buffer[bytes_received] = '\0'; // 确保字符串结束
printf("Received %zd bytes from server %s:%d: %s\n",
bytes_received,
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port),
buffer);
}
// 5. 关闭套接字
close(sock);
printf("UDP client socket closed.\n");
return 0;
}
代码解释:
- 创建一个
AF_INET
和SOCK_DGRAM
的 UDP 套接字。 - 填充
sockaddr_in
结构server_addr
,指定服务器的 IP 和端口。 - 关键: 调用
sendto(sock, message, ..., &server_addr, sizeof(server_addr))
将数据发送到指定的服务器地址。 - 关键: 调用
recvfrom(sock, buffer, ..., &client_addr, &client_addr_len)
接收数据。&client_addr
和&client_addr_len
用于接收发送方(即服务器)的地址信息。client_addr_len
在调用前初始化为sizeof(client_addr)
。
- 打印接收到的数据和服务器的地址(IP 和端口)。
- 关闭套接字。
示例 2:UDP 服务器 (使用 recvfrom
和 sendto
)
这个例子演示了一个 UDP 服务器如何使用 recvfrom
接收来自任意客户端的消息,并使用 sendto
将回复发送回消息的发送方。
// udp_server.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PORT 8081
#define BUFFER_SIZE 1024
int main() {
int server_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
ssize_t bytes_received, bytes_sent;
char reply[] = "Echo: ";
// 1. 创建 UDP 套接字
server_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (server_fd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
printf("UDP server socket created (fd: %d)\n", server_fd);
// 2. 配置服务器地址结构
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有接口
server_addr.sin_port = htons(PORT);
// 3. (可选) 绑定套接字到地址和端口
// 对于 UDP 服务器,绑定是常见的做法,以便客户端知道连接到哪个端口
if (bind(server_fd, (const struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("UDP server bound to port %d\n", PORT);
printf("UDP server is listening for messages...\n");
while (1) {
printf("Waiting for a datagram...\n");
// 4. 接收数据报 (使用 recvfrom 并获取客户端地址)
bytes_received = recvfrom(server_fd, buffer, BUFFER_SIZE - 1, 0,
(struct sockaddr *)&client_addr, &client_addr_len);
if (bytes_received < 0) {
perror("recvfrom failed");
continue; // 或 exit(EXIT_FAILURE);
}
buffer[bytes_received] = '\0'; // 确保字符串结束
printf("Received %zd bytes from client %s:%d: %s\n",
bytes_received,
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port),
buffer);
// 5. 构造回复消息
char reply_buffer[BUFFER_SIZE];
int reply_len = snprintf(reply_buffer, BUFFER_SIZE, "%s%s", reply, buffer);
if (reply_len >= BUFFER_SIZE) {
fprintf(stderr, "Reply message truncated.\n");
reply_len = BUFFER_SIZE - 1;
}
// 6. 发送回复到客户端 (使用 sendto 和之前获取的客户端地址)
printf("Sending reply to client %s:%d\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
bytes_sent = sendto(server_fd, reply_buffer, reply_len, 0,
(const struct sockaddr *)&client_addr, client_addr_len);
if (bytes_sent < 0) {
perror("sendto reply failed");
} else {
printf("Sent %zd bytes as reply.\n", bytes_sent);
}
}
// close(server_fd); // 不会执行到这里
return 0;
}
代码解释:
- 创建一个 UDP 套接字。
- 配置服务器地址
server_addr
,并调用bind()
将套接字绑定到该地址和端口。这是 UDP 服务器的标准做法。 - 进入一个无限循环。
- 关键: 调用
recvfrom(server_fd, buffer, ..., &client_addr, &client_addr_len)
等待并接收数据报。- 该调用会阻塞,直到有数据报到达。
client_addr
和client_addr_len
会被自动填充为发送该数据报的客户端的地址信息。
- 处理接收到的数据(这里简单地打印)。
- 关键: 调用
sendto(server_fd, reply_buffer, ..., &client_addr, client_addr_len)
将回复发送回刚才接收数据的那个客户端。地址信息直接来自上一步recvfrom
的输出。 - 循环继续,处理下一个客户端的数据报。
示例 3:对比 sendto
/recvfrom
与 connect
+ send
/recv
(UDP)
这个例子通过代码片段对比两种 UDP 客户端编程方式。
// 方式一:使用 sendto/recvfrom (显式地址)
void client_method_one() {
int sock = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr;
// ... 配置 server_addr ...
char *msg = "Hello";
// 发送时必须指定地址
sendto(sock, msg, strlen(msg), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
char buffer[1024];
struct sockaddr_in src_addr;
socklen_t src_len = sizeof(src_addr);
// 接收时可以获取源地址
recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&src_addr, &src_len);
close(sock);
}
// 方式二:使用 connect + send/recv (隐式地址)
void client_method_two() {
int sock = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr;
// ... 配置 server_addr ...
// 使用 connect 设置默认目标地址
connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr));
char *msg = "Hello";
// 发送时无需指定地址
send(sock, msg, strlen(msg), 0);
char buffer[1024];
// 接收时通常不关心源地址 (因为已连接)
recv(sock, buffer, sizeof(buffer), 0);
close(sock);
}
代码解释:
- 方式一 (
sendto
/recvfrom
):- 每次发送都必须明确指定目标地址 (
sendto
)。 - 接收时可以选择性地获取源地址 (
recvfrom
)。 - 更加灵活,一个套接字可以与多个不同的目标通信。
- 每次发送都必须明确指定目标地址 (
- 方式二 (
connect
+send
/recv
):- 通过
connect
一次性设置默认目标地址。 - 后续的
send
/write
和recv
/read
操作就像 TCP 一样,无需指定地址。 - 简化了编程模型,但牺牲了灵活性(主要针对单个目标通信)。
- 通过
重要提示与注意事项:
- 数据报边界: 对于 UDP (
SOCK_DGRAM
),sendto
发送的是一个完整的数据报,recvfrom
接收的也是一个完整的数据报。这与 TCP (SOCK_STREAM
) 的字节流不同。 - 无连接: UDP 是无连接的。服务器不需要
listen
和accept
。客户端不需要connect
(除非使用方式二)。 - 地址参数:
sendto
的dest_addr
和recvfrom
的src_addr
是它们与send
/recv
的核心区别。 - 错误处理:
sendto
可能因为目标不可达而失败(ECONNREFUSED
)。recvfrom
在没有数据时会阻塞(阻塞套接字)。 - 缓冲区大小:
recvfrom
的len
参数是缓冲区大小,返回值是实际收到的字节数。确保缓冲区足够大。 addrlen
初始化: 在调用recvfrom
时,务必在之前将*addrlen
初始化为目标缓冲区的大小。
总结:
sendto
和 recvfrom
是进行 UDP 网络编程(以及某些特殊情况下的 TCP 编程)的基础。它们提供了对数据报源地址和目标地址的直接控制,是实现无连接、不可靠但高效通信的关键工具。理解它们的参数和使用场景对于掌握网络编程至关重要。