我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 sendto 和 recvfrom 函数,它们是用于无连接(数据报)套接字(如 UDP)进行数据传输的核心系统调用,但也可以用于面向连接(流式)套接字。
data-ad-format="fluid"
data-ad-layout-key="-7k+ex-4a-9w+4a">
sendto系统调用及示例-CSDN博客
sendto系统调用及示例
1. 函数介绍
sendto 和 recvfrom 是 Linux 系统调用,专门设计用于在套接字上传输数据报(datagrams)。它们与 send/write 和 recv/read 的主要区别在于:sendto 和 recvfrom 显式地处理目标地址和源地址。
你可以把它们想象成写信和收信:
2. 函数原型
1 2 3 4 5 6 7 8 9 10
| #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:
4. 参数
这两个函数的参数非常相似,分别处理发送和接收。
sendto
int flags: 控制发送行为的标志位。常见的有:
const struct sockaddr *dest_addr: 指向目标地址结构的指针(例如 sockaddr_in 或 sockaddr_in6)。
socklen_t addrlen: dest_addr 指向的地址结构的大小。
recvfrom
int flags: 控制接收行为的标志位。常见的有:
0: 使用默认行为。
MSG_DONTWAIT: 使接收操作非阻塞(如果套接字是阻塞的)。
MSG_PEEK: 查看传入的数据,但不从输入队列中移除。
MSG_WAITALL: 请求等待,直到读入请求的字节数。但当检测到错误或断开连接时,或套接字为非阻塞时,仍可能返回少于请求字节数的数据。
struct sockaddr *src_addr:
socklen_t *addrlen:
如果 src_addr 是 NULL,则 addrlen 也必须是 NULL。
如果 src_addr 非 NULL,则 addrlen 必须指向一个 socklen_t 变量。
输入: 调用时,该变量应初始化为 src_addr 指向的缓冲区的大小(例如 sizeof(struct sockaddr_in))。
输出: 返回时,该变量被更新为实际存储在 src_addr 中的地址结构的大小。
5. 返回值
成功时:
失败时:
- 两个函数在失败时都返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EAGAIN/EWOULDBLOCK 非阻塞套接字上无数据可读/写,ECONNREFUSED 远程主机拒绝连接,EINTR 调用被信号中断等)。
6. 相似函数,或关联函数
send / write: 用于发送数据,但不指定目标地址(通常用于已连接的套接字)。
recv / read: 用于接收数据,但不获取源地址信息(通常用于已连接的套接字)。
connect: 对于数据报套接字,connect 可以设置默认目标地址,之后就可以使用 send/write 和 recv/read 而无需指定地址。
socket / bind / sendto / recvfrom: 构成了 UDP 网络编程的基本工具集。
7. 示例代码
示例 1:UDP 客户端 (使用 sendto 和 recvfrom)
这个例子演示了一个 UDP 客户端如何使用 sendto 向服务器发送消息,并使用 recvfrom 接收服务器的回复,同时获取服务器的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| // 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) 接收数据。
打印接收到的数据和服务器的地址(IP 和端口)。
关闭套接字。
示例 2:UDP 服务器 (使用 recvfrom 和 sendto)
这个例子演示了一个 UDP 服务器如何使用 recvfrom 接收来自任意客户端的消息,并使用 sendto 将回复发送回消息的发送方。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| // 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) 等待并接收数据报。
处理接收到的数据(这里简单地打印)。
关键: 调用 sendto(server_fd, reply_buffer, …, &client_addr, client_addr_len) 将回复发送回刚才接收数据的那个客户端。地址信息直接来自上一步 recvfrom 的输出。
循环继续,处理下一个客户端的数据报。
示例 3:对比 sendto/recvfrom 与 connect + send/recv (UDP)
这个例子通过代码片段对比两种 UDP 客户端编程方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| // 方式一:使用 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):
方式二 (connect + send/recv):
重要提示与注意事项:
数据报边界: 对于 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 编程)的基础。它们提供了对数据报源地址和目标地址的直接控制,是实现无连接、不可靠但高效通信的关键工具。理解它们的参数和使用场景对于掌握网络编程至关重要。
https://www.calcguide.tech/2025/08/23/sendto系统调用及示例/