Linux 网络编程中核心函数`recv`。
<摘要>
recv
是 Linux/Unix 系统中用于从已连接的套接字接收数据的系统调用。它是 read
系统调用的套接字特化版本,提供了额外的控制和标志。其核心功能是从 TCP 或已连接的 UDP 套接字的接收缓冲区中读取数据,并将数据存入用户提供的缓冲区。它是所有网络通信中数据接收的基础,广泛应用于客户端和服务器程序中,用于获取对端发送的应用层数据。
<解析>
recv
函数是网络数据流的“接收端”。当数据通过网络到达机器内核后,会被暂存在对应套接字的接收缓冲区中。recv
的工作就是从内核的缓冲区中将这些数据拷贝到应用程序自己定义的内存空间中,以便程序进行处理。
1) 函数的概念与用途
- 功能:从已连接的套接字接收消息。
- 场景:
- TCP 客户端/服务器:在成功建立 TCP 连接(
connect
/accept
)后,使用recv
来读取对方发送的数据流。 - 已连接的 UDP 套接字:对调用了
connect
指定了对端地址的 UDP 套接字,使用recv
来接收来自该特定对端的数据报。 - 不适用:用于未连接的 UDP 套接字(应使用
recvfrom
)。
- TCP 客户端/服务器:在成功建立 TCP 连接(
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
。EAGAIN
或EWOULDBLOCK
:套接字被设置为非阻塞模式,并且当前接收操作会被阻塞。这不是一个真正的错误,只是提示“请稍后再试”。EINTR
:这个调用在阻塞期间被信号中断。通常需要重新调用recv
。ECONNREFUSED
:对端拒绝连接(通常用于 UDP 或异步错误)。ENOTCONN
:套接字未连接。ENOMEM
:没有足够的内存来接收消息。
4) 参数的含义与取值范围
-
int sockfd
- 作用:一个已连接的套接字的文件描述符。
- 取值范围:一个由
socket
创建并已成功连接(通过connect
或accept
)的有效描述符。
-
void *buf
- 作用:指向一段应用程序分配的内存空间的指针,用于存放接收到的数据。
- 取值范围:指向一块大小至少为
len
的可写内存。
-
size_t len
- 作用:指定缓冲区
buf
的最大长度,即本次调用最多能接收多少字节的数据,防止缓冲区溢出。 - 取值范围:通常就是
buf
缓冲区的大小。
- 作用:指定缓冲区
-
int flags
- 作用:修改接收操作行为的标志位。可以通过按位或
|
组合多个标志。 - 常见取值:
0
:默认行为。阻塞等待,直到有数据可用。MSG_DONTWAIT
:非阻塞操作。即使没有数据可读,也立即返回,而不是阻塞。失败时设置errno
为EAGAIN
/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
注意事项:
- 返回值处理:永远不要假设
recv
会一次性读完你要求的数据量。必须检查返回值,它告诉你实际读到了多少字节。这对于TCP这种字节流协议至关重要。 - 阻塞 vs 非阻塞:默认情况下,套接字是阻塞的。
recv
会一直等待,直到有数据可读。如果套接字被设置为非阻塞(O_NONBLOCK
),recv
会立即返回,如果没有数据,则返回-1
并设置errno
为EAGAIN
/EWOULDBLOCK
。 - 连接关闭:返回值
0
表示对端已正常关闭连接。这是需要处理的重要边界条件,而不是错误。 - 缓冲区与字符串:
recv
接收的是原始字节数据,不会自动在末尾添加字符串终止符\0
。如果你要将接收到的数据当作 C 字符串处理,必须手动添加buffer[bytes_received] = '\0';
。 - MSG_WAITALL 的误区:即使指定了
MSG_WAITALL
,在信号中断、连接错误或进程被杀死的情况下,它仍然可能返回少于请求字节数的数据。不能完全依赖它。 - UDP 的使用:
recv
只能用于已连接 (connect
ed) 的 UDP 套接字。对于未连接的 UDP 套接字,应使用recvfrom
来同时获取数据和对端地址。
7) 执行结果说明
- 示例1 & 2:
- 先运行
./tcp_server
,服务器启动并等待连接。 - 再运行
./tcp_client
,客户端连接服务器并发送消息。 - 服务器输出:会打印
recv() returned: 18
和Received 18 bytes: 'Hello from client!'
,然后将数据回显。 - 客户端输出:会打印
Server echoed: Hello from client!
。 - 使用
Ctrl+C
关闭客户端后,服务器会检测到连接关闭 (recv
返回 0),打印关闭信息并退出。
- 先运行
- 示例3:运行后会展示先窥探到的数据和之后实际接收到的数据是相同的,证明
MSG_PEEK
没有消耗缓冲区中的数据。