当前位置: 首页 > 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.dtcms.com/a/364373.html

相关文章:

  • zynq 开发系列 新手入门:GPIO 连接 MIO 控制 LED 闪烁(SDK 端代码编写详解)
  • Spring Boot 实现数据库表变更监听的 Redis 消息队列方案
  • 单片机控制两只直流电机正反转C语言
  • 变频器实习DAY42 VF与IF电机启动方式
  • Excel 电影名匹配图片路径教程:自动查找并写入系统全路径
  • wpf 自定义控件,只能输入小数点,并且能控制小数点位数
  • 机器学习从入门到精通 - Python环境搭建与Jupyter魔法:机器学习起航必备
  • 如何在modelscope上上传自己的MCP服务
  • 【收藏】2025 前端开发者必备 SVG 资源大全
  • 【2025ICCV-持续学习方向】一种用于提示持续学习(Prompt-based Continual Learning, PCL)的新方法
  • 【CouponHub开发记录】SpringAop和分布式锁进行自定义注解实现防止重复提交
  • RAG|| LangChain || LlamaIndex || RAGflow
  • kafka概念之间关系梳理
  • mac idea 配置了Gitlab的远程地址,但是每次pull 或者push 都要输入密码,怎么办
  • 项目中常用的git命令
  • python基础案例-数据可视化
  • Streamlit 数据看板模板:非前端选手快速搭建 Python 数据可视化交互看板的实用工具
  • 【Linux】为什么死循环卡不死 Linux?3 个核心逻辑看懂进程优先级与 CPU 调度密码
  • Langchain4j 整合MongoDB 实现会话持久化存储详解
  • 电表连网不用跑现场!耐达讯自动化RS485转Profinet网关 远程配置+技术支持,真能做到!
  • 单元测试数据库回滚问题
  • 如何在FastAPI中巧妙隔离依赖项,让单元测试不再头疼?
  • 10 分钟掌握 Selenium 8 大元素定位法:从踩坑到精通
  • Python分布式任务队列:万级节点集群的弹性调度实践
  • 深入剖析Spring Boot中Spring MVC的请求处理流程
  • 电脑接入企业中的网线,为啥网卡上面显示AD域名
  • 智能电视小米电视浏览器兼容性踩坑电视黑屏或者电视白屏,Vue项目从Axios到Fetch的避坑指南
  • 【Pytest】解决Pytest中Teardown钩子的TypeError:实例方法与类方法的调用差异
  • 腾讯Hunyuan-MT-7B翻译模型完全指南:2025年开源AI翻译的新标杆
  • 线性代数第一讲—向量组