一、send() 函数概述

1.1 定义与作用

send() 函数用于将数据发送到已建立连接的套接字上,主要用于 面向连接的套接字(如 TCP)。它提供了比 write() 更丰富的功能,可以通过参数指定发送行为。

1.2 函数原型

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

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

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

2.1 参数解析

  • sockfd:套接字文件描述符,由 socket() 函数创建并返回,表示通信的端点。
  • buf:指向要发送的数据缓冲区的指针。
  • len:要发送的数据长度(字节数)。
  • flags:发送选项,控制发送行为。常用的标志包括:
    • 0:默认,无特殊选项。
    • MSG_DONTWAIT:非阻塞发送。
    • MSG_NOSIGNAL:避免发送 SIGPIPE 信号。

2.2 返回值

  • 成功:返回实际发送的字节数。
  • 失败:返回 -1,并设置 errno,指示具体的错误原因。

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

3.1 数据发送过程

  1. 用户空间到内核空间send() 将用户空间的数据复制到内核空间的发送缓冲区。
  2. 发送缓冲区管理:如果发送缓冲区已满,send() 会阻塞(阻塞模式)或返回错误(非阻塞模式)。
  3. 协议处理:内核协议栈处理数据,根据协议(如 TCP)进行封装和传输。
  4. 数据传输:通过网络将数据发送到目标主机。

3.2 阻塞与非阻塞行为

  • 阻塞模式:当发送缓冲区已满时,send() 会阻塞,直到有足够空间。
  • 非阻塞模式send() 立即返回,若无法发送,则返回 -1,并设置 errnoEAGAINEWOULDBLOCK

3.3 与 write() 的区别

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

四、send() 的使用示例

4.1 发送字符串数据

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

int send_message(int sockfd, const char *message) {
    size_t len = strlen(message);
    ssize_t sent_bytes = send(sockfd, message, len, 0);
    if (sent_bytes < 0) {
        perror("send failed");
        return -1;
    } else if (sent_bytes != len) {
        fprintf(stderr, "Partial send: %zd bytes sent\n", sent_bytes);
        // 根据需要处理剩余数据
    }
    return 0;
}

4.2 处理发送缓冲区满的情况

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
ssize_t send_all(int sockfd, const void *buf, size_t len) {
    size_t total_sent = 0;
    const char *ptr = (const char *)buf;
    while (total_sent < len) {
        ssize_t sent = send(sockfd, ptr + total_sent, len - total_sent, 0);
        if (sent <= 0) {
            if (sent < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) {
                // 被信号中断或缓冲区满,继续尝试
                continue;
            } else {
                // 发生错误
                perror("send failed");
                return -1;
            }
        }
        total_sent += sent;
    }
    return total_sent;
}

五、使用注意事项

5.1 部分发送与循环发送

  • 现象send() 可能无法一次发送完所有数据,需循环发送。
  • 原因:网络拥塞、发送缓冲区限制等。
  • 解决方案:在发送数据时,使用循环,不断调用 send(),直到所有数据发送完毕。

5.2 信号中断

  • 问题send() 可能被信号中断(EINTR),需要处理此情况。
  • 解决方案:在收到 EINTR 时,重新调用 send()

5.3 SIGPIPE 信号

  • 现象:向已关闭的套接字发送数据,会触发 SIGPIPE 信号,默认行为是终止进程。

  • 解决方案

    • 捕获并忽略 SIGPIPE 信号:

      1
      2
      
      #include <signal.h>
      signal(SIGPIPE, SIG_IGN);
      
    • 使用 MSG_NOSIGNAL 标志:

      1
      
      send(sockfd, buf, len, MSG_NOSIGNAL);
      

5.4 非阻塞模式处理

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

六、常见问题

6.1 忘记检查返回值

  • 问题:未检查 send() 的返回值,无法确定数据是否成功发送。
  • 建议:始终检查返回值,处理可能的错误和部分发送情况。

6.2 缓冲区管理不当

  • 问题:未考虑发送缓冲区的容量,导致程序阻塞或数据丢失。
  • 解决方案:在高并发或大量数据传输时,合理设置发送缓冲区大小,或使用非阻塞模式结合事件通知。

6.3 数据完整性

  • 问题:未处理部分发送,导致数据丢失或不完整。
  • 解决方案:实现循环发送,确保所有数据都被发送。

6.4 数据序列化与协议设计

  • 问题:直接发送复杂数据结构,可能导致数据不一致或兼容性问题。
  • 解决方案:对数据进行序列化,设计良好的通信协议,确保数据的可解析性和兼容性。

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

7.1 使用 flags 控制发送行为

  • MSG_DONTWAIT:非阻塞发送,即使套接字未设置为非阻塞模式。
  • MSG_OOB:发送带外数据(紧急数据),用于 TCP 套接字的紧急模式。
  • MSG_EOR:标记数据的结束,用于记录分界。

7.2 发送文件描述符(UNIX 域套接字)

  • 场景:在 UNIX 域套接字中,可以通过 sendmsg() 发送文件描述符,实现进程间共享文件描述符。
  • 实现:构造 msghdr 结构体,使用控制消息(cmsghdr)传递文件描述符。

7.3 高级 I/O 模型的结合

  • 异步 I/O:结合 epollkqueue 等机制,实现高性能的网络通信。
  • 零拷贝发送:使用 sendfile() 函数,可以将文件数据直接从内核空间发送,减少数据拷贝,提高效率。

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

8.1 套接字缓冲区

  • 发送缓冲区:内核为每个套接字维护的缓冲区,用于存储待发送的数据。
  • 接收缓冲区:用于存储接收到但未被应用程序读取的数据。
  • 设置缓冲区大小:可以使用 setsockopt() 函数设置 SO_SNDBUFSO_RCVBUF,调整缓冲区大小。

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 信号处理

  • SIGPIPE 信号:向已关闭的套接字发送数据,会触发此信号。
  • 处理方式
    • 忽略信号:signal(SIGPIPE, SIG_IGN);
    • 屏蔽信号:使用 sigaction() 函数。
    • 使用 MSG_NOSIGNAL 标志,避免触发信号。

九、示例代码:完整的客户端发送程序

 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
#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_IP   "127.0.0.1"
#define SERVER_PORT 8080

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    const char *message = "Hello, Server!";
    size_t message_len = strlen(message);

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

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

    // 设置非阻塞模式
    int flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

    // 设置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);

    // 连接服务器
    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        if (errno != EINPROGRESS) {
            perror("connect failed");
            close(sockfd);
            return -1;
        }
    }

    // 使用 select 等待连接完成
    fd_set wfds;
    FD_ZERO(&wfds);
    FD_SET(sockfd, &wfds);
    struct timeval tv = {5, 0}; // 超时 5 秒

    int ret = select(sockfd + 1, NULL, &wfds, NULL, &tv);
    if (ret <= 0) {
        fprintf(stderr, "connect timeout or error\n");
        close(sockfd);
        return -1;
    }

    // 检查连接是否成功
    int error = 0;
    socklen_t len = sizeof(error);
    getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len);
    if (error != 0) {
        fprintf(stderr, "connect failed: %s\n", strerror(error));
        close(sockfd);
        return -1;
    }

    // 发送数据
    ssize_t sent = send(sockfd, message, message_len, MSG_NOSIGNAL);
    if (sent < 0) {
        perror("send failed");
    } else {
        printf("Sent %zd bytes to server\n", sent);
    }

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