recv() 函数是从已连接的套接字接收数据的主要方式。


一、recv() 函数概述

1.1 定义与作用

recv() 函数用于从已连接的套接字中接收数据,主要用于 面向连接的套接字(如 TCP)。它提供了比 read() 更丰富的功能,可以通过参数指定接收行为。

1.2 函数原型

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

二、recv() 函数的参数与返回值

2.1 参数解析

  • sockfd:套接字文件描述符,由 socket() 函数创建并返回,表示通信的端点。
  • buf:指向用于存放接收数据的缓冲区的指针。
  • len:缓冲区的长度(字节数),即最多接收的数据量。
  • flags:接收选项,控制接收行为。常用的标志包括:
    • 0:默认,无特殊选项。
    • MSG_DONTWAIT:非阻塞接收。
    • MSG_PEEK:查看数据,但不从缓冲区中移除。
    • MSG_WAITALL:等待接收到指定的字节数,除非发生错误或连接关闭。

2.2 返回值

  • 成功:返回实际接收的字节数。
    • 返回 0:表示连接已关闭。
  • 失败:返回 -1,并设置 errno,指示具体的错误原因。

三、recv() 函数的工作原理

3.1 数据接收过程

  1. 内核空间到用户空间recv() 从内核接收缓冲区中读取数据,复制到用户提供的缓冲区中。
  2. 接收缓冲区管理:如果没有数据可读,recv() 会阻塞(阻塞模式)或返回错误(非阻塞模式)。
  3. 协议处理:对于 TCP,内核协议栈会处理数据的重组、顺序等,提供可靠的数据流。

3.2 阻塞与非阻塞行为

  • 阻塞模式:当没有数据可读时,recv() 会阻塞,直到有数据到达或发生错误。
  • 非阻塞模式recv() 立即返回,若没有数据,则返回 -1,并设置 errnoEAGAINEWOULDBLOCK

3.3 与 read() 的区别

  • 功能recv() 提供了 flags 参数,可控制接收行为;read() 没有此功能。
  • 适用范围recv() 专用于套接字通信;read() 可用于文件、管道等。

四、recv() 的使用示例

4.1 基本数据接收

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>

int receive_data(int sockfd) {
    char buffer[1024];
    ssize_t received_bytes = recv(sockfd, buffer, sizeof(buffer), 0);
    if (received_bytes < 0) {
        perror("recv failed");
        return -1;
    } else if (received_bytes == 0) {
        printf("Connection closed by peer.\n");
        return 0;
    } else {
        // 处理接收到的数据
        buffer[received_bytes] = '\0'; // 确保字符串以空字符结尾
        printf("Received %zd bytes: %s\n", received_bytes, buffer);
    }
    return received_bytes;
}

4.2 使用 MSG_PEEK 查看数据

1
2
3
ssize_t peek_data(int sockfd, void *buf, size_t len) {
    return recv(sockfd, buf, len, MSG_PEEK);
}

4.3 使用 MSG_WAITALL 接收指定长度的数据

1
2
3
ssize_t receive_exact(int sockfd, void *buf, size_t len) {
    return recv(sockfd, buf, len, MSG_WAITALL);
}

五、注意事项

5.1 处理返回值

  • 返回值为 0:表示对方关闭了连接,应当关闭本地的套接字。
  • 返回值为 -1:需要根据 errno 判断错误类型,可能是暂时性错误或致命错误。

5.2 非阻塞模式下的处理

  • 问题:在非阻塞套接字上,recv() 可能返回 -1,并设置 errnoEAGAINEWOULDBLOCK
  • 解决方案:在程序中处理这种情况,采用异步或事件驱动方式,等待数据可用。

5.3 数据边界与粘包问题

  • 现象:TCP 是流式协议,没有消息边界,可能会出现粘包或拆包。
  • 解决方案:设计应用层协议,添加消息长度或特殊分隔符,确保数据正确解析。

5.4 SIGPIPE 信号

  • 问题:一般在发送数据时才会触发 SIGPIPE 信号,但在某些异常情况下,recv() 也可能受到影响。
  • 建议:在接收数据时,也要注意处理异常和错误,避免程序崩溃。

六、疑难杂症

6.1 忘记处理部分接收

  • 问题:假设每次调用 recv() 都能接收到完整的数据,可能导致数据不完整或解析错误。
  • 解决方案:实现循环接收,直到满足特定条件,如接收到特定的字节数或检测到消息结束标志。

6.2 不检查连接关闭

  • 问题:未检查 recv() 返回值为 0 的情况,可能导致死循环或逻辑错误。
  • 解决方案:在接收数据时,始终检查返回值是否为 0,及时关闭套接字并清理资源。

6.3 数据缓冲区溢出

  • 问题:接收的数据超过缓冲区大小,导致缓冲区溢出,造成安全隐患。
  • 解决方案:确保 recv()len 参数不超过缓冲区大小,或使用动态缓冲区。

6.4 未处理 EINTR 错误

  • 问题recv() 被信号中断,返回 -1errnoEINTR,若未处理,可能导致程序异常退出。
  • 解决方案:在收到 EINTR 时,重新调用 recv()

七、recv() 函数的高级用法

7.1 使用 flags 控制接收行为

  • MSG_PEEK:查看数据,但不从接收缓冲区中移除,可用于检测数据。
  • MSG_DONTWAIT:非阻塞接收,即使套接字未设置为非阻塞模式。
  • MSG_WAITALL:等待接收到指定的字节数,除非发生错误或连接关闭。

7.2 实现超时接收

  • 方法:使用 select()poll()epoll() 等 I/O 多路复用机制,设置超时时间,等待套接字变为可读。
 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
#include <sys/select.h>

int recv_with_timeout(int sockfd, void *buf, size_t len, int timeout_sec) {
    fd_set readfds;
    struct timeval tv;

    FD_ZERO(&readfds);
    FD_SET(sockfd, &readfds);

    tv.tv_sec = timeout_sec;
    tv.tv_usec = 0;

    int ret = select(sockfd + 1, &readfds, NULL, NULL, &tv);
    if (ret > 0) {
        // 套接字可读
        return recv(sockfd, buf, len, 0);
    } else if (ret == 0) {
        // 超时
        fprintf(stderr, "recv timeout\n");
        return -2;
    } else {
        // 错误
        perror("select failed");
        return -1;
    }
}

7.3 接收带外数据

  • 概念:TCP 提供带外数据(Out-of-band data)的机制,用于发送紧急数据。
  • 使用:在接收带外数据时,可以使用 MSG_OOB 标志。
1
2
3
ssize_t recv_oob_data(int sockfd, void *buf, size_t len) {
    return recv(sockfd, buf, len, MSG_OOB);
}

八、拓展资料:关键概念解释

8.1 粘包与拆包问题

  • 现象:在 TCP 通信中,由于其面向字节流的特性,发送的多条消息可能粘连在一起,或一条消息被拆分成多次接收。
  • 原因:TCP 不保证每次发送的数据与接收的数据一一对应,数据的分段和组包由 TCP 协议栈决定。
  • 解决方案
    • 定长消息:约定固定的消息长度。
    • 消息头:在消息前添加长度字段,指示消息的总长度。
    • 特殊分隔符:使用特定的字符或字符串标记消息的结束。

8.2 阻塞与非阻塞模式

  • 阻塞模式:默认模式,I/O 操作会阻塞,直到完成或发生错误。

  • 非阻塞模式:I/O 操作立即返回,若无法完成,则返回错误。

  • 设置非阻塞模式

    1
    2
    3
    
    #include <fcntl.h>
    int flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
    

8.3 I/O 多路复用

  • 概念:使用单个线程或进程监控多个文件描述符的状态,提高资源利用率。
  • 常用机制select()poll()epoll()kqueue() 等。
  • 应用场景:高并发网络服务器,实时性要求高的应用。

8.4 信号中断

  • EINTR 错误:I/O 操作被信号中断,需在程序中处理此错误。
  • 处理方式:在收到 EINTR 时,重新调用被中断的系统调用。

九、示例代码:完整的服务器接收程序

 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
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define SERVER_PORT 8080

int main() {
    int listenfd, connfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[1024];

    // 忽略 SIGPIPE 信号
    signal(SIGPIPE, SIG_IGN);

    // 创建套接字
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) {
        perror("socket creation failed");
        return -1;
    }

    // 设置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;          // IPv4
    server_addr.sin_addr.s_addr = INADDR_ANY;  // 监听所有接口
    server_addr.sin_port = htons(SERVER_PORT); // 端口

    // 绑定地址
    if (bind(listenfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(listenfd);
        return -1;
    }

    // 开始监听
    if (listen(listenfd, 5) < 0) {
        perror("listen failed");
        close(listenfd);
        return -1;
    }

    printf("Server is listening on port %d...\n", SERVER_PORT);

    // 接受连接
    connfd = accept(listenfd, (struct sockaddr *)&client_addr, &client_len);
    if (connfd < 0) {
        perror("accept failed");
        close(listenfd);
        return -1;
    }

    printf("Client connected.\n");

    // 接收数据
    while (1) {
        ssize_t received = recv(connfd, buffer, sizeof(buffer) - 1, 0);
        if (received < 0) {
            if (errno == EINTR) {
                continue; // 被信号中断,重新接收
            } else {
                perror("recv failed");
                break;
            }
        } else if (received == 0) {
            printf("Client disconnected.\n");
            break;
        } else {
            buffer[received] = '\0'; // 确保字符串以空字符结尾
            printf("Received %zd bytes: %s\n", received, buffer);
            // 可以在此处处理数据,或发送响应
        }
    }

    // 关闭套接字
    close(connfd);
    close(listenfd);
    return 0;
}