当前位置: 首页 > news >正文

Linux 网络编程中核心函数`recv`。

<摘要>

recv 是 Linux/Unix 系统中用于从已连接的套接字接收数据的系统调用。它是 read 系统调用的套接字特化版本,提供了额外的控制和标志。其核心功能是从 TCP 或已连接的 UDP 套接字的接收缓冲区中读取数据,并将数据存入用户提供的缓冲区。它是所有网络通信中数据接收的基础,广泛应用于客户端和服务器程序中,用于获取对端发送的应用层数据。


<解析>

recv 函数是网络数据流的“接收端”。当数据通过网络到达机器内核后,会被暂存在对应套接字的接收缓冲区中。recv 的工作就是从内核的缓冲区中将这些数据拷贝到应用程序自己定义的内存空间中,以便程序进行处理。

1) 函数的概念与用途
  • 功能:从已连接的套接字接收消息。
  • 场景
    • TCP 客户端/服务器:在成功建立 TCP 连接(connect/accept)后,使用 recv 来读取对方发送的数据流。
    • 已连接的 UDP 套接字:对调用了 connect 指定了对端地址的 UDP 套接字,使用 recv 来接收来自该特定对端的数据报。
    • 不适用:用于未连接的 UDP 套接字(应使用 recvfrom)。
2) 函数的声明与出处

recv 定义在 <sys/socket.h> 头文件中,是 POSIX 标准的一部分。

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
3) 返回值含义与取值范围
  • 成功:返回实际读取到的字节数。这个值可能小于参数 len 指定的缓冲区大小。
  • 返回 0:这意味着对端已经关闭了连接(对于 TCP 来说,收到了 FIN 包)。这是一个重要的信号,应用程序通常应该也关闭本地的这个套接字。
  • 失败:返回 -1,并设置相应的错误码 errno
    • EAGAINEWOULDBLOCK:套接字被设置为非阻塞模式,并且当前接收操作会被阻塞。这不是一个真正的错误,只是提示“请稍后再试”。
    • EINTR:这个调用在阻塞期间被信号中断。通常需要重新调用 recv
    • ECONNREFUSED:对端拒绝连接(通常用于 UDP 或异步错误)。
    • ENOTCONN:套接字未连接。
    • ENOMEM:没有足够的内存来接收消息。
4) 参数的含义与取值范围
  1. int sockfd

    • 作用:一个已连接的套接字的文件描述符。
    • 取值范围:一个由 socket 创建并已成功连接(通过 connectaccept)的有效描述符。
  2. void *buf

    • 作用:指向一段应用程序分配的内存空间的指针,用于存放接收到的数据。
    • 取值范围:指向一块大小至少为 len 的可写内存。
  3. size_t len

    • 作用:指定缓冲区 buf最大长度,即本次调用最多能接收多少字节的数据,防止缓冲区溢出。
    • 取值范围:通常就是 buf 缓冲区的大小。
  4. int flags

    • 作用:修改接收操作行为的标志位。可以通过按位或 | 组合多个标志。
    • 常见取值
      • 0默认行为。阻塞等待,直到有数据可用。
      • MSG_DONTWAIT非阻塞操作。即使没有数据可读,也立即返回,而不是阻塞。失败时设置 errnoEAGAIN/EWOULDBLOCK
      • MSG_PEEK窥探数据。从接收缓冲区中拷贝数据到 buf,但不会将这些数据从缓冲区中移除。下一次调用 recv 还会看到相同的数据。
      • MSG_WAITALL等待全部数据。请求内核等待,直到接收到恰好 len 个字节的数据后才返回。但在某些情况下(如收到信号、连接中断),它仍然可能返回少于 len 的数据。
      • MSG_OOB接收带外数据。用于处理紧急数据。
5) 函数使用案例

示例 1:基础的 TCP 回显服务器(处理接收循环和连接关闭)
此示例展示一个简易的 TCP 服务器,它接收客户端数据并回显。它正确处理了 recv 返回 0 的情况。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>#define PORT 8080
#define BUFFER_SIZE 1024int main() {int server_fd, new_socket;struct sockaddr_in address;int opt = 1;int addrlen = sizeof(address);char buffer[BUFFER_SIZE] = {0};// 创建套接字文件描述符if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 强制附加端口,避免 "address already in use" 错误if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {perror("setsockopt");exit(EXIT_FAILURE);}address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);// 绑定套接字到端口if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}// 开始监听if (listen(server_fd, 3) < 0) {perror("listen");exit(EXIT_FAILURE);}printf("Server listening on port %d\n", PORT);// 接受一个传入连接if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {perror("accept");exit(EXIT_FAILURE);}printf("Connection accepted. Waiting for data...\n");// 接收循环while(1) {// 清空缓冲区memset(buffer, 0, BUFFER_SIZE);// 核心:调用 recv 阻塞等待数据ssize_t bytes_received = recv(new_socket, buffer, BUFFER_SIZE, 0);printf("recv() returned: %zd\n", bytes_received);if (bytes_received > 0) {printf("Received %zd bytes: '%s'\n", bytes_received, buffer);// 回显相同的数据send(new_socket, buffer, bytes_received, 0);printf("Echoed back.\n");} else if (bytes_received == 0) {// 对端关闭了连接printf("Client closed the connection. Closing socket.\n");break;} else {// recv 出错perror("recv failed");break;}}close(new_socket);close(server_fd);return 0;
}

使用 telnet 127.0.0.1 8080 或下面的客户端示例进行测试。

示例 2:简单的 TCP 客户端
此示例展示一个客户端如何使用 recv 接收服务器的响应。

#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 SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUFFER_SIZE 1024int main() {int sock = 0;struct sockaddr_in serv_addr;char *message = "Hello from client!";char buffer[BUFFER_SIZE] = {0};// 1. 创建套接字if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("Socket creation error");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) {perror("Invalid address/ Address not supported");return -1;}// 2. 连接到服务器if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {perror("Connection Failed");return -1;}// 3. 发送数据send(sock, message, strlen(message), 0);printf("Hello message sent\n");// 4. 接收服务器的回显数据 (核心: 调用recv)ssize_t bytes_received = recv(sock, buffer, BUFFER_SIZE, 0);if (bytes_received > 0) {buffer[bytes_received] = '\0'; // 确保字符串终止printf("Server echoed: %s\n", buffer);} else if (bytes_received == 0) {printf("Server closed the connection unexpectedly.\n");} else {perror("recv failed");}close(sock);return 0;
}

示例 3:使用 MSG_PEEK 和 MSG_DONTWAIT 标志
此示例演示如何非阻塞地“窥探”接收缓冲区中的数据,而不将其移除。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <errno.h>int main() {// ... (创建一对已连接的套接字用于演示,省略了socketpair创建代码)// int sockfd[2];// socketpair(AF_UNIX, SOCK_STREAM, 0, sockfd);// 假设 sockfd[0] 和 sockfd[1] 是已连接的一对套接字int recv_sock = sockfd[0]; // 用于接收的套接字int send_sock = sockfd[1]; // 用于发送的套接字const char *message = "Peekaboo!";char buffer[20] = {0};// 1. 向发送端套接字写入数据send(send_sock, message, strlen(message), 0);printf("Sent: '%s'\n", message);// 2. 使用 MSG_PEEK | MSG_DONTWAIT 窥探数据ssize_t peeked_bytes = recv(recv_sock, buffer, sizeof(buffer), MSG_PEEK | MSG_DONTWAIT);if (peeked_bytes > 0) {buffer[peeked_bytes] = '\0';printf("Peeked (%zd bytes): '%s'\n", peeked_bytes, buffer);printf("Data is still in the kernel's receive buffer.\n");} else if (peeked_bytes == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {printf("Peek would block (no data available).\n");} else {perror("Peek failed");}// 3. 正常接收数据(这次数据会被移除)memset(buffer, 0, sizeof(buffer));ssize_t real_bytes = recv(recv_sock, buffer, sizeof(buffer), 0);if (real_bytes > 0) {buffer[real_bytes] = '\0';printf("Actually received (%zd bytes): '%s'\n", real_bytes, buffer);}close(recv_sock);close(send_sock);return 0;
}
6) 编译方式与注意事项

编译命令:

# 编译服务器
gcc -o tcp_server tcp_server.c
# 编译客户端
gcc -o tcp_client tcp_client.c
# 编译PEEK示例 (需要补充socketpair的代码)
# gcc -o recv_peek_demo recv_peek_demo.c

注意事项:

  1. 返回值处理永远不要假设 recv 会一次性读完你要求的数据量。必须检查返回值,它告诉你实际读到了多少字节。这对于TCP这种字节流协议至关重要。
  2. 阻塞 vs 非阻塞:默认情况下,套接字是阻塞的。recv 会一直等待,直到有数据可读。如果套接字被设置为非阻塞(O_NONBLOCK),recv 会立即返回,如果没有数据,则返回 -1 并设置 errnoEAGAIN/EWOULDBLOCK
  3. 连接关闭返回值 0 表示对端已正常关闭连接。这是需要处理的重要边界条件,而不是错误。
  4. 缓冲区与字符串recv 接收的是原始字节数据,不会自动在末尾添加字符串终止符 \0。如果你要将接收到的数据当作 C 字符串处理,必须手动添加 buffer[bytes_received] = '\0';
  5. MSG_WAITALL 的误区:即使指定了 MSG_WAITALL,在信号中断、连接错误或进程被杀死的情况下,它仍然可能返回少于请求字节数的数据。不能完全依赖它。
  6. UDP 的使用recv 只能用于已连接 (connected) 的 UDP 套接字。对于未连接的 UDP 套接字,应使用 recvfrom 来同时获取数据和对端地址。
7) 执行结果说明
  • 示例1 & 2
    1. 先运行 ./tcp_server,服务器启动并等待连接。
    2. 再运行 ./tcp_client,客户端连接服务器并发送消息。
    3. 服务器输出:会打印 recv() returned: 18Received 18 bytes: 'Hello from client!',然后将数据回显。
    4. 客户端输出:会打印 Server echoed: Hello from client!
    5. 使用 Ctrl+C 关闭客户端后,服务器会检测到连接关闭 (recv 返回 0),打印关闭信息并退出。
  • 示例3:运行后会展示先窥探到的数据和之后实际接收到的数据是相同的,证明 MSG_PEEK 没有消耗缓冲区中的数据。
8) 图文总结:recv 工作流程
成功拷贝到数据
(字节数 > 0)
接收缓冲区为空
且对端已关闭连接 (FIN)
发生错误
阻塞模式
非阻塞模式
应用程序调用 recv()
内核从套接字接收缓冲区
拷贝数据到用户空间缓冲区 buf
检查拷贝结果
返回实际读取的字节数
返回 0
返回 -1
并设置相应的 errno
套接字模式
是否阻塞?
缓冲区空则睡眠等待
缓冲区空则立即返回 -1 (EAGAIN)
应用程序处理数据
应用程序关闭连接
应用程序根据 errno 处理错误
http://www.xdnf.cn/news/1445023.html

相关文章:

  • Qt6用Chart模块做数据可视化?别再用老套路,看看这套35张图背后的秘密
  • MySQL :索引原理
  • 【面试题】BPE和WordPiece的区别?
  • Anaconda3出现Fatal error in launcher: Unable to create process using.....问题
  • STM32CubeMX + HAL 库:基于 I²C 通信的 BMP280气压海拔测量
  • 【超详细】别再看零散的教程了!一篇搞定Gitee从注册、配置到代码上传与管理(内含避坑指南最佳实践)
  • PS大神级AI建模技巧!效率翻倍工作流,悄悄收藏!
  • Wan系列模型解析--详细架构图
  • 机器学习在Backtrader多因子模型中的应用
  • 美团龙猫利用expat库实现的保存xml指定范围数据到csv的C程序
  • TypeScript 泛型入门(新手友好、完整详解)
  • XSENS VISION NAVIGATOR助力智能城市自动化清洁机器人精确导航
  • TLSF内存算法适配HTOS
  • 【Unity UGUI Canvas(画布)(1)】
  • 【音视频】FMP4 介绍
  • 【正点原子K210连载】第三十一章 音频FFT实验 摘自【正点原子】DNK210使用指南-CanMV版指南
  • 【论文阅读】-《THE JPEG STILL PICTURE COMPRESSION STANDARD》
  • Android 接入deepseek
  • 关于ES中文分词器analysis-ik快速安装
  • k8s使用StatefulSet(有状态)部署单节点 MySQL方案(使用本地存储)
  • 【Bug】Nexus无法正常启动的五种解决方法
  • SuperMap GIS基础产品FAQ集锦(20250901)
  • Elasticsearch 数字字段随机取多值查询缓慢-原理分析与优化方案
  • 504 Gateway Timeout:服务器作为网关或代理时未能及时获得响应如何处理?
  • 揭秘设计模式:优雅地为复杂对象结构增添新功能-访问者模式
  • go语言面试之Goroutine详解
  • Linux使用-Linux系统管理
  • WPF里的几何图形Path绘制
  • 硬件驱动C51单片机——裸机(1)
  • 三、Scala方法与函数