UNIX下C语言编程与实践59-UNIX TCP 数据传输:send 与 recv 函数的使用与数据处理

在 UNIX TCP 通信中,sendrecv 是完成数据传输的核心函数------send 负责将数据从进程缓冲区发送到 TCP 连接,recv 负责从 TCP 连接接收数据到进程缓冲区。TCP 通信的编程思想,本文将详细讲解这两个函数的功能、参数与使用细节,通过完整实例演示服务器端与客户端的数据传输流程,深入分析 TCP 粘包问题的成因与解决方法,并梳理常见错误与优化策略。

一、TCP 数据传输核心函数解析

在 TCP 服务器端实例(如 tcp1.c)中明确其核心地位------在 accept 建立连接后,必须通过这两个函数完成数据交互。以下从函数原型、参数含义、返回值三个维度展开解析。

1.1 send 函数:发送数据到 TCP 连接

send 函数的功能是将进程缓冲区中的数据,通过已连接的 TCP 套接字发送给对端(客户端或服务器端),其行为受 TCP 协议的可靠传输机制(如确认、重传)保障。

(1)函数原型与参数

函数定义在 <sys/socket.h> 头文件中,原型如下:

复制代码
#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int s, const void *msg, size_t len, int flags);

四个参数的核心作用 TCP 通信场景的对应关系如下表所示:

参数名 数据类型 核心含义 使用场景
s int 已建立连接的 TCP 套接字描述符(由 acceptconnect 返回,非侦听套接字) 服务器端:accept 返回的新套接字(如 nSock1);客户端:connect 成功后的套接字
msg const void * 指向待发送数据缓冲区的指针,数据以字节流形式存储(如字符串、结构体序列化后的数据) 发送字符串:char buf[] = "Hello TCP!";msg = buf;发送二进制数据:struct Data data;msg = &data
len size_t 待发送数据的字节数(需准确计算,避免多传或漏传) 发送字符串:strlen(buf)(不含结束符 '\0');发送结构体:sizeof(struct Data)
flags int 发送标志,控制发送行为,通常设为 0(默认方式),特殊场景使用 MSG_OOB TCP 通信默认用 0;需紧急数据传输时用 MSG_OOB(如中断信号通知)
(2)返回值与关键说明

函数返回值为 ssize_t 类型(带符号的整数),含义如下: - 成功 :返回实际发送的字节数(可能小于 len,因 TCP 发送缓冲区可能暂时满,需重试发送剩余数据); - 失败 :返回 -1,并通过 errno 标识错误类型(如 EBADF 表示套接字无效,EPIPE 表示连接已关闭)。

隐含的注意点write 函数类似,send 并非"一次调用必发送全部数据",需通过循环调用确保 len 字节数据完全发送,避免数据丢失。

1.2 recv 函数:从 TCP 连接接收数据

recv 函数的功能是从已连接的 TCP 套接字中,读取对端发送的数据并存储到进程缓冲区,其行为受 TCP 协议的字节流特性影响------数据按发送顺序接收,但无固定边界。

(1)函数原型与参数

函数定义在 <sys/socket.h> 头文件中,原型如下(与 send 函数参数结构对称,编程风格):

复制代码
#include <sys/types.h>
#include <sys/socket.h>

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

四个参数的核心作用与 TCP 服务器端实例(如 tcp1.c)的对应关系如下表所示:

参数名 数据类型 核心含义 使用场景
s int 已建立连接的 TCP 套接字描述符(与 sends 一致) tcp1.caccept 返回的 nSock1,用于接收客户端发送的 HTTP 报文
buf void * 指向接收数据缓冲区的指针,需预先分配足够内存(避免缓冲区溢出) 常用 char buf[2048];,大小根据预期接收的数据量设定(如 2048 字节可容纳普通 HTTP 头部)
len size_t 接收缓冲区的最大容量(字节数),避免数据超出缓冲区导致内存越界 sizeof(buf),确保不超过缓冲区大小(如 sizeof(buf) = 2048
flags int 接收标志,控制接收行为,通常设为 0(默认阻塞接收) tcp1.c0,阻塞等待客户端数据;需非阻塞接收时可结合 fcntl 设置套接字属性
(2)返回值与关键说明

函数返回值为 ssize_t 类型,含义如下: - 成功 :返回实际接收的字节数(可能小于 len,因 TCP 可能分批次传递数据); - 对端关闭连接 :返回 0(TCP 四次挥手完成,无更多数据可接收); - 失败 :返回 -1,并通过 errno 标识错误类型(如 EINTR 表示信号中断,EAGAIN 表示非阻塞模式下无数据)。

实例的印证tcp1.c 中通过 recv(nSock1, buf, sizeof(buf), 0) 接收客户端数据,虽未处理返回值判断,但实际开发中需根据返回值区分"数据接收""连接关闭""错误"三种场景,避免程序异常。

二、实战实例:TCP 服务器端与客户端数据传输

结合 TCP 通信的编程范式(如 CreateSock 函数创建侦听套接字、accept 建立连接),编写完整的服务器端与客户端程序,演示 sendrecv 函数的协同使用,涵盖"服务器端接收数据并响应""客户端发送数据并接收响应"的全流程。

2.1 服务器端程序(recv 接收数据,send 发送响应)

服务器端流程:创建侦听套接字 → 绑定地址与端口 → 侦听连接 → 接收客户端连接 → recv 接收客户端数据 → send 发送响应 → 关闭连接。核心代码如下(错误检查与函数调用风格):

复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

// 错误检查宏
#define VerifyErr(a, b) \
    if (a) { fprintf(stderr, "%s failed. errno: %d\n", (b), errno); return 1; }

// 创建侦听套接字
int CreateListenSock(int port, int backlog) {
    int sockfd;
    struct sockaddr_in addr;

    // 1. 创建 TCP 套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    VerifyErr(sockfd < 0, "socket");

    // 2. 设置端口重用(避免 bind 失败)
    int opt = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 3. 绑定地址与端口
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(port);
    VerifyErr(bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0, "bind");

    // 4. 侦听连接
    VerifyErr(listen(sockfd, backlog) < 0, "listen");

    printf("Server listen on port %d...\n", port);
    return sockfd;
}

int main() {
    int listen_fd, conn_fd;
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    char recv_buf[1024] = {0};
    char send_buf[1024] = {0};
    ssize_t recv_len, send_len;

    // 1. 创建侦听套接字(端口 9000,连接队列长度 5)
    listen_fd = CreateListenSock(9000, 5);
    VerifyErr(listen_fd < 0, "CreateListenSock");

    // 2. 接收客户端连接(阻塞)
    conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
    VerifyErr(conn_fd < 0, "accept");
    printf("Client connected (fd: %d)\n", conn_fd);

    // 3. recv 接收客户端数据(阻塞,直到有数据或连接关闭)
    recv_len = recv(conn_fd, recv_buf, sizeof(recv_buf)-1, 0); // 留 1 字节存 '\0'
    if (recv_len < 0) {
        fprintf(stderr, "recv failed. errno: %d\n", errno);
        close(conn_fd);
        close(listen_fd);
        return 1;
    } else if (recv_len == 0) {
        printf("Client closed connection\n");
        close(conn_fd);
        close(listen_fd);
        return 0;
    }
    recv_buf[recv_len] = '\0'; // 字符串结尾补 '\0'
    printf("Received from client: %s (len: %zd)\n", recv_buf, recv_len);

    // 4. send 发送响应数据给客户端
    snprintf(send_buf, sizeof(send_buf), "Server received: %s", recv_buf);
    send_len = send(conn_fd, send_buf, strlen(send_buf), 0);
    if (send_len < 0) {
        fprintf(stderr, "send failed. errno: %d\n", errno);
    } else {
        printf("Sent to client: %s (len: %zd)\n", send_buf, send_len);
    }

    // 5. 关闭连接与侦听套接字
    close(conn_fd);
    close(listen_fd);
    return 0;
}

2.2 客户端程序(send 发送数据,recv 接收响应)

客户端流程:创建 TCP 套接字 → 连接服务器端 → send 发送数据 → recv 接收服务器端响应 → 关闭连接。核心代码如下:

复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

// 错误检查宏
#define VerifyErr(a, b) \
    if (a) { fprintf(stderr, "%s failed. errno: %d\n", (b), errno); return 1; }

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <server_ip> <server_port>\n", argv[0]);
        return 1;
    }

    int sockfd;
    struct sockaddr_in server_addr;
    char send_buf[1024] = {0};
    char recv_buf[1024] = {0};
    ssize_t send_len, recv_len;
    const char *server_ip = argv[1];
    int server_port = atoi(argv[2]);

    // 1. 创建 TCP 套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    VerifyErr(sockfd < 0, "socket");

    // 2. 初始化服务器端地址结构
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(server_port);
    // IP 地址转换(字符串 → 网络字节顺序整数)
    VerifyErr(inet_aton(server_ip, &server_addr.sin_addr) == 0, "inet_aton");

    // 3. 连接服务器端
    VerifyErr(connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0, "connect");
    printf("Connected to server %s:%d\n", server_ip, server_port);

    // 4. 输入并 send 发送数据
    printf("Input data to send: ");
    fgets(send_buf, sizeof(send_buf)-1, stdin);
    // 去除 fgets 读取的换行符
    send_buf[strcspn(send_buf, "\n")] = '\0';
    send_len = send(sockfd, send_buf, strlen(send_buf), 0);
    if (send_len < 0) {
        fprintf(stderr, "send failed. errno: %d\n", errno);
        close(sockfd);
        return 1;
    }
    printf("Sent to server: %s (len: %zd)\n", send_buf, send_len);

    // 5. recv 接收服务器端响应
    recv_len = recv(sockfd, recv_buf, sizeof(recv_buf)-1, 0);
    if (recv_len < 0) {
        fprintf(stderr, "recv failed. errno: %d\n", errno);
        close(sockfd);
        return 1;
    } else if (recv_len == 0) {
        printf("Server closed connection\n");
        close(sockfd);
        return 0;
    }
    recv_buf[recv_len] = '\0';
    printf("Received from server: %s (len: %zd)\n", recv_buf, recv_len);

    // 6. 关闭套接字
    close(sockfd);
    return 0;
}

2.3 程序执行与结果分析

  1. 编译程序 (假设服务器端文件为 tcp_server.c,客户端为 tcp_client.c):

    gcc tcp_server.c -o tcp_server
    gcc tcp_client.c -o tcp_client

  2. 启动服务器端

    ./tcp_server
    Server listen on port 9000...

  3. 启动客户端并发送数据 (服务器端 IP 为 127.0.0.1):

    ./tcp_client 127.0.0.1 9000
    Connected to server 127.0.0.1:9000
    Input data to send: Hello from TCP client!
    Sent to server: Hello from TCP client! (len: 23)
    Received from server: Server received: Hello from TCP client! (len: 45)

  4. 服务器端输出

    Client connected (fd: 4)
    Received from client: Hello from TCP client! (len: 23)
    Sent to client: Server received: Hello from TCP client! (len: 45)

该实例延续了TCP 通信的核心流程(socketbindlistenaccept/connect→数据传输),并补充了 sendrecv 的完整使用逻辑,解决了tcp1.c 中未处理返回值的问题,更符合实际开发需求。

三、send 与 recv 函数的 flags 参数详解

未详细讲解 flags 参数,但该参数在特殊场景(如紧急数据传输、非阻塞接收)中至关重要。以下梳理常见的 flags 取值及其适用场景,结合编程思想说明使用方式。

flags 取值 核心作用 适用场景 使用示例
0(默认) 常规发送/接收模式: - send:阻塞(默认),直到数据写入 TCP 发送缓冲区或出错; - recv:阻塞(默认),直到有数据接收、连接关闭或出错 绝大多数 TCP 数据传输场景(如 HTTP 报文接收、普通字符串传输),无需特殊处理 send(conn_fd, buf, strlen(buf), 0); recv(conn_fd, buf, sizeof(buf), 0);
MSG_OOB 发送/接收带外数据(Out-of-Band Data): - TCP 带外数据仅 1 字节,用于传递紧急信号(如中断、终止命令); - 需配合套接字选项 SO_OOBINLINE 使用,或通过 select 监控 POLLPRI 事件 需要紧急通知对端的场景(如远程调试中的强制中断、实时系统中的告警信号) // 发送带外数据(1 字节 '!') send(conn_fd, "!", 1, MSG_OOB); // 接收带外数据 recv(conn_fd, buf, 1, MSG_OOB);
MSG_DONTWAIT 非阻塞模式: - send:若 TCP 发送缓冲区满,不阻塞,直接返回 -1 并置 errno=EAGAIN; - recv:若无数据,不阻塞,直接返回 -1 并置 errno=EAGAIN 并发场景(如 I/O 多路复用),避免单个连接阻塞导致其他连接无法处理(timeout3.c 的非阻塞思想) // 非阻塞接收数据 recv_len = recv(conn_fd, buf, sizeof(buf), MSG_DONTWAIT); if (recv_len < 0 && errno == EAGAIN) { printf("No data available now\n"); }
MSG_WAITALL recv 专用:阻塞直到接收完 len 字节数据,或连接关闭/出错(仅在数据完整接收时返回 len 需接收固定长度数据的场景(如结构体、文件块),避免分批次接收导致数据不完整 // 接收 1024 字节固定长度数据 recv_len = recv(conn_fd, buf, 1024, MSG_WAITALL); if (recv_len == 1024) { printf("Received full data\n"); }

隐含建议 :TCP 实例均使用 flags=0,因常规场景无需特殊处理;若需非阻塞或紧急数据传输,可结合 fcntl(设置套接字非阻塞属性)或 select(监控带外数据事件),与 flags 参数配合使用,确保程序兼容性。

四、TCP 粘包问题:成因与解决方法

文中虽未提及"粘包"术语,但 TCP 字节流特性导致的"数据边界模糊"问题在实际开发中普遍存在。以下基于文中的编程思想,分析粘包问题的成因,并给出三种工程化解决方法。

4.1 粘包问题的成因

TCP 是面向字节流 的协议,不维护数据的"消息边界"------发送方多次发送的小数据,可能被 TCP 合并为一个数据段发送;接收方一次 recv 可能读取到发送方多次发送的数据,导致"粘包"。具体成因包括:

  1. TCP 拥塞控制与 Nagle 算法 :TCP 为提高传输效率,会合并小数据块(如发送方连续调用 send("a",1,0)send("b",1,0),TCP 可能合并为"ab"发送);
  2. 接收方缓冲区未满 :接收方 recv 调用不及时,TCP 接收缓冲区中积累了多批数据,一次 recv 读取全部数据(如接收方缓冲区有"hello"和"world",一次 recv 读取到"helloworld");
  3. 数据传输延迟:网络延迟导致发送方多批数据几乎同时到达接收方,被接收方一次性读取。

粘包示例 : - 客户端连续发送两次数据:send(sockfd, "hello", 5, 0);send(sockfd, "world", 5, 0);; - 服务器端一次 recv 可能读取到 "helloworld"(粘包),而非预期的"hello"和"world"。

4.2 粘包问题的解决方法

解决粘包的核心思路是为数据添加"边界标识",让接收方能够区分不同批次的数据。以下三种方法均符合文中的编程风格,可直接集成到 TCP 通信程序中。

(1)方法一:固定消息长度

原理 :发送方与接收方约定固定的消息长度(如 100 字节),发送方每次发送 100 字节数据(不足时补零),接收方每次 recv 固定 100 字节,确保数据边界清晰。

实现代码: - 发送方(补零确保固定长度):

复制代码
#define FIXED_LEN 100
char send_buf[FIXED_LEN] = {0};
strncpy(send_buf, "hello", FIXED_LEN); // 不足 100 字节补零
send(sockfd, send_buf, FIXED_LEN, 0);
  • 接收方(固定长度接收):

    char recv_buf[FIXED_LEN] = {0};
    ssize_t recv_len = recv(conn_fd, recv_buf, FIXED_LEN, MSG_WAITALL); // 等待接收 100 字节
    if (recv_len == FIXED_LEN) {
    printf("Received: %s\n", recv_buf); // 输出 "hello"(后面补零不影响字符串打印)
    }

适用场景:数据长度固定的场景(如传感器固定格式的采集数据),实现简单但灵活性低。

(2)方法二:使用特殊分隔符

原理 :发送方在每个消息末尾添加特殊分隔符(如 '\n''\0' 或自定义字符),接收方逐字节读取数据,遇到分隔符即认为一个消息结束。

实现代码 : - 发送方(添加 '\n' 分隔符):

复制代码
char *msg = "hello\nworld\n"; // 两个消息,用 '\n' 分隔
send(sockfd, msg, strlen(msg), 0);
  • 接收方(逐字节读取,识别分隔符):

    char recv_buf[1024] = {0};
    char *ptr = recv_buf;
    ssize_t total_len = 0;
    // 循环接收,直到读取到分隔符
    while (1) {
    ssize_t len = recv(conn_fd, ptr, 1, 0); // 逐字节读取
    if (len < 0) {
    perror("recv");
    break;
    } else if (len == 0) {
    printf("Connection closed\n");
    break;
    }
    total_len += len;
    if (*ptr == '\n') { // 遇到分隔符,处理当前消息
    *ptr = '\0'; // 替换分隔符为字符串结束符
    printf("Received: %s\n", recv_buf); // 依次输出 "hello"、"world"
    // 重置缓冲区指针,准备接收下一个消息
    memset(recv_buf, 0, sizeof(recv_buf));
    ptr = recv_buf;
    total_len = 0;
    } else {
    ptr++;
    }
    }

适用场景:文本数据传输(如日志、命令),分隔符需避免与业务数据冲突(如传输二进制数据时不可用)。

(3)方法三:定义消息头包含数据长度

原理:消息分为"消息头"和"消息体"两部分------消息头固定长度(如 4 字节),存储消息体的长度;接收方先读取消息头,获取消息体长度后,再读取对应长度的消息体,彻底解决粘包问题(文中推荐的通用方法)。

实现代码: - 定义消息结构(消息头 + 消息体):

复制代码
// 消息头:4 字节,存储消息体长度(网络字节顺序)
typedef struct {
    uint32_t body_len; // 消息体长度(大端序)
} MsgHeader;

// 发送消息函数:先发送消息头,再发送消息体
int send_msg(int sockfd, const char *body, size_t body_len) {
    MsgHeader header;
    // 消息体长度转换为网络字节顺序
    header.body_len = htonl(body_len);
    // 1. 发送消息头(固定 4 字节)
    ssize_t send_len = send(sockfd, &header, sizeof(MsgHeader), 0);
    if (send_len != sizeof(MsgHeader)) {
        perror("send header");
        return -1;
    }
    // 2. 发送消息体(按消息头中的长度发送)
    send_len = send(sockfd, body, body_len, 0);
    if (send_len != body_len) {
        perror("send body");
        return -1;
    }
    return 0;
}
  • 接收消息函数(先读消息头,再读消息体):

    // 接收消息函数:先接收消息头,再接收消息体
    char *recv_msg(int sockfd, ssize_t *out_body_len) {
    MsgHeader header;
    ssize_t recv_len;
    // 1. 接收消息头(固定 4 字节,使用 MSG_WAITALL 确保完整接收)
    recv_len = recv(sockfd, &header, sizeof(MsgHeader), MSG_WAITALL);
    if (recv_len != sizeof(MsgHeader)) {
    perror("recv header");
    return NULL;
    }
    // 消息体长度转换为主机字节顺序
    size_t body_len = ntohl(header.body_len);
    *out_body_len = body_len;
    // 2. 分配消息体缓冲区
    char body = (char)malloc(body_len + 1);
    if (body == NULL) {
    perror("malloc");
    return NULL;
    }
    // 3. 接收消息体(按消息头中的长度接收)
    recv_len = recv(sockfd, body, body_len, MSG_WAITALL);
    if (recv_len != body_len) {
    perror("recv body");
    free(body);
    return NULL;
    }
    body[body_len] = '\0'; // 字符串结尾补 '\0'
    return body;
    }

  • 调用示例(发送"hello"和"world"两个消息):

    // 发送方
    send_msg(sockfd, "hello", 5);
    send_msg(sockfd, "world", 5);

    // 接收方
    ssize_t body_len;
    char *body = recv_msg(conn_fd, &body_len);
    if (body != NULL) {
    printf("Received (len: %zd): %s\n", body_len, body); // 输出 "hello"
    free(body);
    }
    body = recv_msg(conn_fd, &body_len);
    if (body != NULL) {
    printf("Received (len: %zd): %s\n", body_len, body); // 输出 "world"
    free(body);
    }

适用场景:通用场景(文本/二进制数据),灵活性高,TCP 项目实践(如自定义协议)均推荐此方法。

五、常见错误与解决方案

结合文中的错误处理思想(如 VerifyErr 宏)与 TCP 数据传输的实战经验,梳理 sendrecv 函数使用过程中常见的错误场景,分析原因并给出解决方案。

  • 错误 1:send/recv 返回值未处理,导致数据不完整

    原因:未考虑 send 实际发送字节数小于 len(如 TCP 发送缓冲区满),或 recv 实际接收字节数小于 len(如数据分批次到达),直接认为数据已完整传输;

    解决方案: 1. send 循环发送,直到所有数据发送完成:

    复制代码
    ssize_t send_all(int sockfd, const char *buf, size_t len) {
        size_t sent = 0;
        while (sent < len) {
            ssize_t ret = send(sockfd, buf + sent, len - sent, 0);
            if (ret < 0) {
                perror("send");
                return -1;
            }
            sent += ret;
        }
        return sent;
    }
    1. recv 循环接收,直到获取预期长度数据或连接关闭:

      ssize_t recv_all(int sockfd, char *buf, size_t len) {
      size_t received = 0;
      while (received < len) {
      ssize_t ret = recv(sockfd, buf + received, len - received, 0);
      if (ret < 0) {
      perror("recv");
      return -1;
      } else if (ret == 0) {
      return received; // 连接关闭,返回已接收长度
      }
      received += ret;
      }
      return received;
      }

  • 错误 2:send 失败,errno = EPIPE

    原因:对端已关闭连接(如客户端意外退出),但本地进程仍调用 send 发送数据,TCP 发送 FIN 后收到 RST,导致 EPIPE 错误;

    解决方案: 1. 发送前通过 recv 检查连接状态(recv 返回 0 表示连接关闭); 2. 捕获 SIGPIPE 信号(默认导致进程退出),自定义信号处理函数:

    复制代码
    void handle_sigpipe(int sig) {
        printf("Connection closed by peer (SIGPIPE)\n");
        // 可在此处关闭套接字、清理资源
    }
    
    // 主函数中注册信号处理
    signal(SIGPIPE, handle_sigpipe);
  • 错误 3:recv 阻塞导致程序无响应

    原因:recv 默认阻塞模式,若对端长时间不发送数据,本地进程会一直阻塞在 recv 调用,无法处理其他任务(如文中 tcp1.c 未处理的问题);

    解决方案: 1. 设置套接字为非阻塞模式,结合 select/poll 监控数据到达:

    复制代码
    // 设置套接字为非阻塞
    int flags = fcntl(conn_fd, F_GETFL, 0);
    fcntl(conn_fd, F_SETFL, flags | O_NONBLOCK);
    
    // 使用 select 监控读事件,超时时间 5 秒
    fd_set readfds;
    struct timeval timeout = {5, 0};
    FD_ZERO(&readfds);
    FD_SET(conn_fd, &readfds);
    
    int ret = select(conn_fd + 1, &readfds, NULL, NULL, &timeout);
    if (ret < 0) {
        perror("select");
    } else if (ret == 0) {
        printf("recv timeout (5s)\n");
    } else {
        if (FD_ISSET(conn_fd, &readfds)) {
            // 有数据到达,调用 recv
            recv(conn_fd, buf, sizeof(buf), 0);
        }
    }
    1. 使用 MSG_DONTWAIT 标志,非阻塞接收:

      recv_len = recv(conn_fd, buf, sizeof(buf), MSG_DONTWAIT);
      if (recv_len < 0) {
      if (errno == EAGAIN) {
      printf("No data available now, try later\n");
      } else {
      perror("recv");
      }
      }

  • 错误 4:缓冲区溢出导致内存越界

    原因:recvlen 参数设置过大(超过缓冲区实际大小),或接收数据后未添加字符串结束符,导致打印/处理时访问非法内存;

    解决方案: 1. recvlen 参数设为 sizeof(buf) - 1,预留 1 字节存储 '\0'

    复制代码
    char buf[1024];
    recv_len = recv(conn_fd, buf, sizeof(buf)-1, 0);
    if (recv_len > 0) {
        buf[recv_len] = '\0'; // 补字符串结束符
    }
    1. 使用动态内存分配(如 malloc),根据实际接收数据长度调整缓冲区大小(参考"消息头+消息体"方法)。

六、TCP 数据传输效率优化策略

文中虽未提及优化,但基于 UNIX 系统调用的特性,可通过以下策略提高 sendrecv 的传输效率,减少系统调用次数与内存拷贝开销。

6.1 使用缓冲区批量发送数据

频繁调用 send 发送小数据会增加系统调用开销(用户态→内核态切换)。优化方法是:在进程中设置发送缓冲区,积累一定量的数据后,一次性调用 send 发送。

复制代码
#define BUF_SIZE 4096
char send_buf[BUF_SIZE] = {0};
size_t buf_len = 0;

// 批量发送函数:数据先写入缓冲区,满则发送
int batch_send(int sockfd, const char *data, size_t data_len) {
    // 若数据超过缓冲区剩余空间,先发送现有缓冲区数据
    if (buf_len + data_len > BUF_SIZE) {
        ssize_t ret = send(sockfd, send_buf, buf_len, 0);
        if (ret < 0) {
            perror("send batch");
            return -1;
        }
        buf_len = 0; // 重置缓冲区
    }
    // 将新数据写入缓冲区
    memcpy(send_buf + buf_len, data, data_len);
    buf_len += data_len;
    return 0;
}

// 刷新缓冲区:发送剩余数据
int flush_send(int sockfd) {
    if (buf_len > 0) {
        ssize_t ret = send(sockfd, send_buf, buf_len, 0);
        if (ret < 0) {
            perror("flush send");
            return -1;
        }
        buf_len = 0;
    }
    return 0;
}

6.2 调整 TCP 缓冲区大小

TCP 发送/接收缓冲区默认大小可能较小(如 4KB),大文件传输时需频繁调用 send/recv。可通过 setsockopt 调整缓冲区大小,减少系统调用次数。

复制代码
// 设置 TCP 发送缓冲区大小为 64KB
int send_buf_size = 64 * 1024;
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &send_buf_size, sizeof(send_buf_size));

// 设置 TCP 接收缓冲区大小为 64KB
int recv_buf_size = 64 * 1024;
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &recv_buf_size, sizeof(recv_buf_size));

6.3 避免不必要的数据拷贝

若发送的数据已在内存中(如文件映射到内存),可通过 sendfile 函数直接从文件描述符发送数据,避免用户态与内核态之间的数据拷贝(文中未涉及,但 Linux 系统推荐使用)。

复制代码
// 从文件发送数据到套接字(无数据拷贝)
int send_file(int sockfd, int file_fd) {
    off_t offset = 0;
    struct stat file_stat;
    fstat(file_fd, &file_stat);
    // sendfile:直接将文件数据发送到套接字
    ssize_t ret = sendfile(sockfd, file_fd, &offset, file_stat.st_size);
    if (ret < 0) {
        perror("sendfile");
        return -1;
    }
    return 0;
}

七、总结

基于《精通UNIX下C语言编程与项目实践笔记.pdf》的 TCP 编程思想,本文系统梳理了 sendrecv 函数的使用与数据处理要点,可总结为以下关键点:

  • 函数核心地位sendrecv 是 TCP 数据传输的"最终执行者",在 accept/connect 建立连接后,需通过这两个函数完成数据交互,其返回值处理是程序稳定性的关键;
  • flags 参数场景化flags=0 适用于常规场景,MSG_OOB 用于紧急数据,MSG_DONTWAIT 用于非阻塞,MSG_WAITALL 用于固定长度接收,需根据业务需求选择;
  • 粘包问题解决:TCP 字节流特性导致粘包,需通过"固定长度""特殊分隔符""消息头+消息体"三种方法添加数据边界,其中"消息头+消息体"是文风格的通用解决方案;
  • 错误与优化:需处理"数据不完整""连接关闭""阻塞无响应"等错误,通过循环发送/接收、非阻塞模式、缓冲区调整等策略提高传输效率。

掌握 sendrecv 函数的使用,是 UNIX TCP 通信编程的核心。在实际开发中,需结合文中的错误检查思想(如 VerifyErr 宏),严格处理返回值,针对粘包问题选择合适的解决方案,并通过优化策略提升传输效率,才能构建出稳定、高效的 TCP 应用程序。

相关推荐
迎風吹頭髮3 小时前
UNIX下C语言编程与实践55-TCP 协议基础:面向连接的可靠传输机制与三次握手、四次挥手
c语言·网络·unix
HaSaKing_7215 小时前
二三级等保检测对比项
linux·服务器·网络
迎風吹頭髮5 小时前
UNIX下C语言编程与实践35-UNIX 守护进程编写:后台执行、脱离终端、清除掩码与信号处理
java·c语言·unix
2301_793167996 小时前
网络管理部分
linux·运维·服务器·网络·php
序属秋秋秋6 小时前
《Linux系统编程之入门基础》【Linux的前世今生】
linux·运维·服务器·开源·unix·gnu
qiuiuiu4136 小时前
正点原子RK3568学习日记-GIT
linux·c语言·开发语言·单片机
搬砖的小码农_Sky6 小时前
Windows操作系统上`ping`命令的用法详解
运维·网络·windows
思考的笛卡尔8 小时前
密码学基础:RSA与AES算法的实现与对比
网络·算法·密码学
AALoveTouch12 小时前
网球馆自动预约系统的反调试
javascript·网络