sendmsg系统调用及示例

我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 recvmsg 和 sendmsg 函数,它们是功能最强大、最通用的套接字 I/O 函数,可以处理 read/writesend/recv 以及 sendto/recvfrom 的所有功能,并且还支持更高级的特性,如传输文件描述符、**发送和接收访问控制列表 **(ancillary data)。

sendmsg系统调用及示例-CSDN博客


1. 函数介绍

recvmsg 和 sendmsg 是 Linux 系统调用,它们提供了最灵活和最完整的套接字数据传输接口。它们是 read/writesend/recvsendto/recvfrom 的超集

  • sendmsg: 通过套接字发送数据。它允许你指定:
    • 要发送的数据(可以来自多个不连续的缓冲区,类似 writev)。
    • 目标地址(类似 sendto)。
    • 各种控制选项和标志。
    • 辅助数据(ancillary data),例如要通过 Unix 域套接字传递的文件描述符
  • recvmsg: 通过套接字接收数据。它允许你:
    • 将数据接收存储到多个不连续的缓冲区(类似 readv)。
    • 获取数据的来源地址(类似 recvfrom)。
    • 获取各种套接字状态信息。
    • 接收辅助数据(ancillary data),例如通过 Unix 域套接字传递的文件描述符

你可以把它们想象成一个多功能的包裹处理系统

  • 一个包裹(消息)可以包含主货物(常规数据)和特殊附件(辅助数据,如文件描述符)。
  • 主货物可以放在一个或多个箱子(缓冲区)里。
  • 包裹上贴有收件人地址(目标地址,用于发送)或发件人地址(源地址,用于接收)。
  • 包裹上还有特殊标记(标志位)指示如何处理它。

2. 函数原型

#include <sys/socket.h> // 必需

// 发送消息
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

// 接收消息
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

3. 功能

  • sendmsg: 根据 struct msghdr 结构中描述的所有信息(数据缓冲区、目标地址、辅助数据、标志)来构建并发送一个消息。
  • recvmsg: 从套接字接收一个消息,并将接收到的数据、源地址、辅助数据等信息填充到 struct msghdr 结构指向的缓冲区中。

4. 参数

这两个函数都通过一个核心的 struct msghdr 结构体来传递所有必要的信息。

struct msghdr 结构体

这是两个函数的核心,定义了消息的完整属性:

struct msghdr {
    void         *msg_name;       // 可选的地址 (struct sockaddr*)
    socklen_t     msg_namelen;    // 地址长度
    struct iovec *msg_iov;        // 缓冲区向量 (scatter/gather)
    size_t        msg_iovlen;     // 缓冲区向量的元素个数
    void         *msg_control;    // 辅助数据 (cmsghdr*)
    size_t        msg_controllen; // 辅助数据缓冲区大小
    int           msg_flags;      // 接收消息时的标志 (输出)
};
  • void *msg_name:
    • 发送时: 指向目标地址结构(如 sockaddr_in)的指针。对于面向连接的套接字(如 TCP),通常设为 NULL
    • 接收时: 指向用于存储源地址结构的缓冲区的指针。
  • socklen_t msg_namelen:
    • 发送时msg_name 指向的地址结构的大小。
    • 接收时输入时为 msg_name 缓冲区的大小;返回时为实际存储的地址结构的大小。
  • struct iovec *msg_iov: 这是一个 struct iovec 数组的指针,用于实现分散-聚集 I/O(scatter-gather I/O),即数据可以来自多个不连续的缓冲区(类似 readv/writev)。
    struct iovec 定义如下:struct iovec { void *iov_base; // 缓冲区起始地址 size_t iov_len; // 缓冲区长度 };
  • size_t msg_iovlenmsg_iov 数组中元素的个数。
  • void *msg_control: 指向用于发送或接收辅助数据(ancillary data)的缓冲区。辅助数据可以包含文件描述符、网络接口索引、IP 选项等。
  • size_t msg_controllen:
    • 发送时msg_control 缓冲区的大小。
    • 接收时输入时为 msg_control 缓冲区的大小;返回时为实际接收到的辅助数据的大小。
  • int msg_flags:
    • 发送时: 传递额外的发送标志(通常与 sendto/send 的 flags 参数相同)。
    • 接收时返回接收操作时应用的标志(例如,如果数据包被截断,可能会设置 MSG_TRUNC)。

函数参数

  • int sockfd: 一个有效的套接字文件描述符。
  • const struct msghdr *msg (sendmsg) / struct msghdr *msg (recvmsg): 指向描述消息属性的 msghdr 结构体的指针。
  • int flags: 控制发送或接收行为的标志位,与 send/recv/sendto/recvfrom 的 flags 参数类似。
    • 常见标志:MSG_DONTWAITMSG_PEEKMSG_WAITALLMSG_NOSIGNAL 等。

5. 返回值

  • 成功时:
    • 返回实际传输的字节数(即所有 msg_iov 缓冲区中数据的总和)。
  • 失败时:
    • 返回 -1,并设置全局变量 errno 来指示具体的错误原因。

6. 相似函数,或关联函数

  • send / sendto / recv / recvfromsendmsg/recvmsg 是这些函数的通用形式。
  • writev / readvsendmsg/recvmsg 结合 iovec 实现了类似的功能。
  • sendmmsg / recvmmsg: (Linux 特有) 可以在一次系统调用中发送或接收多个消息,性能更高。
  • CMSG_* 宏: 用于处理 msg_control 中的辅助数据(如 CMSG_FIRSTHDRCMSG_NXTHDRCMSG_DATA)。

7. 示例代码

示例 1:使用 sendmsg/recvmsg 替代 sendto/recvfrom (UDP)

这个例子展示了如何用 sendmsg 和 recvmsg 实现与 sendto 和 recvfrom 相同的功能(发送和接收 UDP 数据报)。

// msg_udp_client.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SERVER_PORT 8082
#define SERVER_IP "127.0.0.1"
#define BUFFER_SIZE 1024

int main() {
    int sock;
    struct sockaddr_in server_addr;
    struct msghdr msg;
    struct iovec iov[1]; // 只使用一个缓冲区
    char *message = "Hello from sendmsg/recvmsg client!";
    char buffer[BUFFER_SIZE];
    ssize_t bytes_sent, bytes_received;

    // 1. 创建 UDP 套接字
    sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 2. 配置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
        fprintf(stderr, "Invalid address\n");
        close(sock);
        exit(EXIT_FAILURE);
    }

    // --- 使用 sendmsg 发送 ---
    printf("Sending message using sendmsg...\n");

    // 准备 iovec
    iov[0].iov_base = message;
    iov[0].iov_len = strlen(message);

    // 准备 msghdr
    memset(&msg, 0, sizeof(msg));
    msg.msg_name = &server_addr;        // 目标地址
    msg.msg_namelen = sizeof(server_addr);
    msg.msg_iov = iov;                  // 数据缓冲区向量
    msg.msg_iovlen = 1;                 // 向量元素个数

    // 发送消息
    bytes_sent = sendmsg(sock, &msg, 0);
    if (bytes_sent < 0) {
        perror("sendmsg failed");
        close(sock);
        exit(EXIT_FAILURE);
    } else {
        printf("sendmsg sent %zd bytes.\n", bytes_sent);
    }

    // --- 使用 recvmsg 接收 ---
    printf("Receiving reply using recvmsg...\n");

    struct sockaddr_in src_addr;
    socklen_t src_addr_len = sizeof(src_addr);

    // 准备 iovec for recv
    iov[0].iov_base = buffer;
    iov[0].iov_len = BUFFER_SIZE - 1;

    // 准备 msghdr for recv
    memset(&msg, 0, sizeof(msg));
    msg.msg_name = &src_addr;          // 用于存储源地址
    msg.msg_namelen = src_addr_len;    // 输入:缓冲区大小
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    // 接收消息
    bytes_received = recvmsg(sock, &msg, 0);
    if (bytes_received < 0) {
        perror("recvmsg failed");
        close(sock);
        exit(EXIT_FAILURE);
    } else {
        buffer[bytes_received] = '\0';
        printf("recvmsg received %zd bytes from %s:%d: %s\n",
               bytes_received,
               inet_ntoa(src_addr.sin_addr), ntohs(src_addr.sin_port),
               buffer);
        // msg.msg_namelen 现在包含实际的地址大小
    }

    close(sock);
    return 0;
}
// msg_udp_server.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define PORT 8082
#define BUFFER_SIZE 1024

int main() {
    int server_fd;
    struct sockaddr_in server_addr, client_addr;
    struct msghdr msg;
    struct iovec iov[1];
    char buffer[BUFFER_SIZE];
    char reply[] = "Echo via sendmsg: ";
    char reply_buffer[BUFFER_SIZE];
    ssize_t bytes_received, bytes_sent;

    server_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (server_fd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(server_fd, (const struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("UDP server using sendmsg/recvmsg listening on port %d\n", PORT);

    while (1) {
        printf("Waiting for datagram...\n");

        socklen_t client_addr_len = sizeof(client_addr);
        iov[0].iov_base = buffer;
        iov[0].iov_len = BUFFER_SIZE - 1;

        memset(&msg, 0, sizeof(msg));
        msg.msg_name = &client_addr;
        msg.msg_namelen = client_addr_len;
        msg.msg_iov = iov;
        msg.msg_iovlen = 1;

        bytes_received = recvmsg(server_fd, &msg, 0);
        if (bytes_received < 0) {
            perror("recvmsg failed");
            continue;
        }

        buffer[bytes_received] = '\0';
        printf("Received %zd bytes from %s:%d: %s\n",
               bytes_received,
               inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port),
               buffer);

        // 构造回复
        int reply_len = snprintf(reply_buffer, BUFFER_SIZE, "%s%s", reply, buffer);
        if (reply_len >= BUFFER_SIZE) reply_len = BUFFER_SIZE - 1;

        // 使用 sendmsg 发送回复
        iov[0].iov_base = reply_buffer;
        iov[0].iov_len = reply_len;

        memset(&msg, 0, sizeof(msg));
        msg.msg_name = &client_addr; // 发送到刚才接收数据的客户端
        msg.msg_namelen = sizeof(client_addr);
        msg.msg_iov = iov;
        msg.msg_iovlen = 1;

        bytes_sent = sendmsg(server_fd, &msg, 0);
        if (bytes_sent < 0) {
            perror("sendmsg reply failed");
        } else {
            printf("Sent %zd bytes reply using sendmsg.\n", bytes_sent);
        }
    }

    return 0;
}

代码解释:

  1. 客户端和服务器都创建 UDP 套接字并进行必要的设置(服务器需要 bind)。
  2. 发送数据:
    • 准备一个 struct iovec 数组。这里只用一个元素指向消息缓冲区。
    • 准备一个 struct msghdr 结构体 msg
    • 设置 msg.msg_name 为目标地址,msg.msg_iov 为缓冲区向量,msg.msg_iovlen 为向量长度。
    • 调用 sendmsg(sock, &msg, 0) 发送数据。
  3. 接收数据:
    • 准备 struct iovec 和 struct msghdr
    • 设置 msg.msg_name 为用于存储源地址的缓冲区,msg.msg_namelen 为该缓冲区大小。
    • 设置 msg.msg_iov 和 msg.msg_iovlen
    • 调用 recvmsg(sock, &msg, 0) 接收数据。
    • 接收后,msg.msg_name 中包含了发送方的地址,msg.msg_namelen 被更新为实际地址大小。

示例 2:使用 sendmsg/recvmsg 进行 Scatter-Gather I/O

这个例子展示了如何使用 iovec 数组通过 sendmsg 发送来自多个缓冲区的数据。

// scatter_gather_client.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SERVER_PORT 8083
#define SERVER_IP "127.0.0.1"

int main() {
    int sock;
    struct sockaddr_in server_addr;
    struct msghdr msg;
    struct iovec iov[3]; // 使用 3 个缓冲区
    char part1[] = "Part1-";
    char part2[] = "Part2-";
    char part3[] = "Part3-END";
    char recv_buffer[1024];
    ssize_t bytes_sent, bytes_received;

    sock = socket(AF_INET, SOCK_STREAM, 0); // 使用 TCP
    if (sock < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
        fprintf(stderr, "Invalid address\n");
        close(sock);
        exit(EXIT_FAILURE);
    }

    // 连接到服务器
    if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect failed");
        close(sock);
        exit(EXIT_FAILURE);
    }

    // --- 使用 sendmsg 发送分散的数据 ---
    printf("Sending scattered data using sendmsg...\n");

    // 准备 iovec 数组
    iov[0].iov_base = part1;
    iov[0].iov_len = strlen(part1);
    iov[1].iov_base = part2;
    iov[1].iov_len = strlen(part2);
    iov[2].iov_base = part3;
    iov[2].iov_len = strlen(part3);

    // 准备 msghdr (TCP 不需要地址)
    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = iov;
    msg.msg_iovlen = 3;

    bytes_sent = sendmsg(sock, &msg, 0);
    if (bytes_sent < 0) {
        perror("sendmsg failed");
        close(sock);
        exit(EXIT_FAILURE);
    } else {
        printf("sendmsg sent %zd bytes (should be sum of parts: %zu).\n",
               bytes_sent, strlen(part1) + strlen(part2) + strlen(part3));
    }

    // 接收服务器的确认
    bytes_received = recv(sock, recv_buffer, sizeof(recv_buffer) - 1, 0);
    if (bytes_received > 0) {
        recv_buffer[bytes_received] = '\0';
        printf("Received confirmation: %s\n", recv_buffer);
    }

    close(sock);
    return 0;
}
// scatter_gather_server.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define PORT 8083
#define BACKLOG 10

int main() {
    int server_fd, client_fd;
    struct sockaddr_in address, client_address;
    socklen_t client_addr_len = sizeof(client_address);
    struct msghdr msg;
    struct iovec iov[3];
    char buffer1[50], buffer2[50], buffer3[50]; // 接收缓冲区
    char confirmation[] = "Data received successfully!";
    int opt = 1;

    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt failed");
        close(server_fd);
        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");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Scatter-Gather TCP server listening on port %d\n", PORT);

    client_fd = accept(server_fd, (struct sockaddr *)&client_address, &client_addr_len);
    if (client_fd < 0) {
        perror("accept failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Client connected. Waiting for scattered data...\n");

    // --- 使用 recvmsg 接收数据到多个缓冲区 ---
    iov[0].iov_base = buffer1;
    iov[0].iov_len = sizeof(buffer1) - 1;
    iov[1].iov_base = buffer2;
    iov[1].iov_len = sizeof(buffer2) - 1;
    iov[2].iov_base = buffer3;
    iov[2].iov_len = sizeof(buffer3) - 1;

    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = iov;
    msg.msg_iovlen = 3;

    ssize_t bytes_received = recvmsg(client_fd, &msg, 0);
    if (bytes_received < 0) {
        perror("recvmsg failed");
        close(client_fd);
        close(server_fd);
        exit(EXIT_FAILURE);
    } else {
        printf("recvmsg received %zd bytes.\n", bytes_received);
        // 确保字符串结束 (实际应用中需要更仔细地处理)
        size_t total_copied = 0;
        for (int i = 0; i < 3 && total_copied < (size_t)bytes_received; ++i) {
            size_t to_copy = iov[i].iov_len < (size_t)(bytes_received - total_copied) ?
                             iov[i].iov_len : (size_t)(bytes_received - total_copied);
            ((char*)iov[i].iov_base)[to_copy] = '\0';
            total_copied += to_copy;
        }
        printf("Data in buffers:\n  Buffer1: '%s'\n  Buffer2: '%s'\n  Buffer3: '%s'\n",
               buffer1, buffer2, buffer3);
    }

    // 发送确认
    send(client_fd, confirmation, strlen(confirmation), 0);

    close(client_fd);
    close(server_fd);
    return 0;
}

代码解释:

  1. 客户端使用 TCP 连接到服务器。
  2. 客户端将一个消息分割成三个部分,存放在 part1part2part3 三个不同的缓冲区中。
  3. 发送: 使用 sendmsg 和包含三个元素的 iovec 数组,将这三个缓冲区的数据一次性发送出去。这避免了三次 send 调用。
  4. 服务器 accept 连接。
  5. 接收: 服务器使用 recvmsg 和包含三个元素的 iovec 数组,将接收到的数据分散存储到 buffer1buffer2buffer3 三个不同的缓冲区中。
  6. 服务器打印出存储在各个缓冲区中的数据。

示例 3:通过 Unix 域套接字传递文件描述符 (辅助数据)

这个例子展示了 sendmsg/recvmsg 最强大的功能之一:传递文件描述符。这是进程间传递打开文件的一种高级方法。

// fd_passing.c
// 需要编译为两个程序: sender 和 receiver
// gcc -o sender fd_passing.c -DSENDER
// gcc -o receiver fd_passing.c -DRECEIVER

#include <sys/socket.h>
#include <sys/un.h> // Unix domain sockets
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SOCKET_PATH "/tmp/fd_pass_socket"

#ifdef SENDER
int main() {
    int sock, file_fd;
    struct sockaddr_un addr;
    struct msghdr msg;
    struct cmsghdr *cmsg;
    struct iovec iov[1];
    char ctrl_buf[CMSG_SPACE(sizeof(int))]; // 为一个 int (fd) 分配控制消息空间
    char data[] = "FD";
    int data_len = strlen(data);

    // 1. 创建要传递的文件
    file_fd = open("testfile.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (file_fd == -1) {
        perror("open testfile.txt");
        exit(EXIT_FAILURE);
    }
    write(file_fd, "This is data in the passed file.\n", 33);
    printf("Created and wrote to 'testfile.txt' (fd: %d)\n", file_fd);

    // 2. 创建 Unix 域套接字
    sock = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock == -1) {
        perror("socket");
        close(file_fd);
        exit(EXIT_FAILURE);
    }

    // 3. 连接到接收方
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);

    if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
        perror("connect");
        close(file_fd);
        close(sock);
        exit(EXIT_FAILURE);
    }
    printf("Connected to receiver.\n");

    // 4. 准备消息结构
    iov[0].iov_base = data;
    iov[0].iov_len = data_len;

    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;
    msg.msg_control = ctrl_buf;
    msg.msg_controllen = sizeof(ctrl_buf);

    // 5. 准备辅助数据 (控制消息)
    cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS; // 传递权限 (文件描述符)
    cmsg->cmsg_len = CMSG_LEN(sizeof(int));
    memcpy(CMSG_DATA(cmsg), &file_fd, sizeof(int)); // 将文件描述符复制到控制消息数据部分

    msg.msg_controllen = cmsg->cmsg_len; // 更新控制消息长度

    // 6. 发送消息 (包含文件描述符)
    if (sendmsg(sock, &msg, 0) == -1) {
        perror("sendmsg");
        close(file_fd);
        close(sock);
        exit(EXIT_FAILURE);
    }

    printf("Sent message with file descriptor %d.\n", file_fd);
    // 注意:发送后,发送方通常应该 close 掉这个 fd
    // 但在此例中我们不 close,以演示 fd 已被传递

    close(sock);
    // close(file_fd); // 通常在这里关闭,但我们想演示它已被传递
    printf("Sender finished. Check if 'testfile.txt' is closed (lsof `pwd`/testfile.txt).\n");
    return 0;
}

#elif defined(RECEIVER)
int main() {
    int listen_sock, conn_sock, received_fd = -1;
    struct sockaddr_un addr, client_addr;
    socklen_t client_len;
    struct msghdr msg;
    struct cmsghdr *cmsg;
    struct iovec iov[1];
    char ctrl_buf[CMSG_SPACE(sizeof(int))];
    char data[10];
    ssize_t data_len;

    // 1. 创建 Unix 域套接字
    listen_sock = socket(AF_UNIX, SOCK_STREAM, 0);
    if (listen_sock == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 清理可能存在的旧 socket 文件
    unlink(SOCKET_PATH);

    // 2. 绑定套接字
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);

    if (bind(listen_sock, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
        perror("bind");
        close(listen_sock);
        exit(EXIT_FAILURE);
    }

    // 3. 监听
    if (listen(listen_sock, 5) == -1) {
        perror("listen");
        close(listen_sock);
        exit(EXIT_FAILURE);
    }

    printf("Receiver listening on %s\n", SOCKET_PATH);

    // 4. 接受连接
    client_len = sizeof(client_addr);
    conn_sock = accept(listen_sock, (struct sockaddr*)&client_addr, &client_len);
    if (conn_sock == -1) {
        perror("accept");
        close(listen_sock);
        exit(EXIT_FAILURE);
    }
    printf("Connection accepted.\n");

    // 5. 准备接收消息
    iov[0].iov_base = data;
    iov[0].iov_len = sizeof(data) - 1;

    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;
    msg.msg_control = ctrl_buf;
    msg.msg_controllen = sizeof(ctrl_buf);

    // 6. 接收消息
    data_len = recvmsg(conn_sock, &msg, 0);
    if (data_len == -1) {
        perror("recvmsg");
        close(conn_sock);
        close(listen_sock);
        exit(EXIT_FAILURE);
    }
    data[data_len] = '\0';
    printf("Received data: %s\n", data);

    // 7. 解析辅助数据 (控制消息) 以获取文件描述符
    cmsg = CMSG_FIRSTHDR(&msg);
    if (cmsg && cmsg->cmsg_len == CMSG_LEN(sizeof(int)) &&
        cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) {
        memcpy(&received_fd, CMSG_DATA(cmsg), sizeof(int));
        printf("Received file descriptor: %d\n", received_fd);

        // 8. 使用接收到的文件描述符
        lseek(received_fd, 0, SEEK_SET); // 移到文件开头
        char buf[100];
        ssize_t bytes_read = read(received_fd, buf, sizeof(buf) - 1);
        if (bytes_read > 0) {
            buf[bytes_read] = '\0';
            printf("Read from received fd: %s", buf);
        }
        printf("Closing received file descriptor.\n");
        close(received_fd); // 关闭接收到的文件描述符
    } else {
        printf("No file descriptor received.\n");
    }

    close(conn_sock);
    close(listen_sock);
    unlink(SOCKET_PATH); // 清理 socket 文件
    printf("Receiver finished.\n");
    return 0;
}
#endif

编译和运行:

# Terminal 1
gcc -o receiver fd_passing.c -DRECEIVER
./receiver

# Terminal 2 (在 receiver 运行后)
gcc -o sender fd_passing.c -DSENDER
./sender

代码解释:

  1. 该代码通过宏定义 SENDER 和 RECEIVER 编译成两个不同的程序。
  2. **发送方 **(SENDER)
    • 创建一个名为 testfile.txt 的文件并写入数据。
    • 创建一个 Unix 域套接字并连接到接收方。
    • 准备一个 struct msghdr
    • 准备一个 iovec 来发送一些普通数据(“FD”)。
    • 关键: 准备辅助数据msg_control)。
      • 使用 CMSG_SPACE(sizeof(int)) 来分配足够的控制缓冲区。
      • 使用 CMSG_FIRSTHDR 获取第一个控制消息头指针。
      • 设置 cmsg_level 为 SOL_SOCKETcmsg_type 为 SCM_RIGHTS(表示传递文件描述符)。
      • 使用 CMSG_DATA(cmsg) 获取数据部分的指针,并将 file_fd 复制进去。
    • 调用 sendmsg 发送包含普通数据和辅助数据(文件描述符)的消息。
  3. **接收方 **(RECEIVER)
    • 创建并监听一个 Unix 域套接字。
    • accept 连接。
    • 准备 struct msghdr 和用于接收辅助数据的控制缓冲区 ctrl_buf
    • 调用 recvmsg 接收消息。
    • 关键: 解析接收到的辅助数据。
      • 使用 CMSG_FIRSTHDR 获取第一个控制消息头。
      • 检查 cmsg_levelcmsg_typecmsg_len 是否正确。
      • 如果正确,使用 CMSG_DATA(cmsg) 获取数据部分,并将文件描述符复制到 received_fd 变量中。
    • 现在,接收方可以像使用自己打开的文件一样使用 received_fd
    • 最后关闭接收到的文件描述符和套接字。

重要提示与注意事项:

  1. 通用性sendmsg/recvmsg 是最通用的套接字 I/O 函数,可以替代所有其他基本的发送和接收函数。
  2. Scatter-Gather: 通过 iovec 数组,可以高效地处理不连续的数据缓冲区,减少系统调用次数。
  3. 地址处理: 对于面向连接的套接字(如 TCP),msg_name 通常为 NULL。对于无连接的(如 UDP),它用于指定目标或获取源地址。
  4. 辅助数据: 这是 sendmsg/recvmsg 独有的强大功能。正确处理辅助数据需要使用 CMSG_* 系列宏,这比较复杂但非常有用(如传递文件描述符、设置网络接口等)。
  5. 性能: 在需要处理大量数据或复杂消息结构时,sendmsg/recvmsg 可能比多次调用 send/recv 更高效。
  6. sendmmsg/recvmmsg: 对于需要在一次系统调用中处理多个消息的高性能应用(如网络服务器),Linux 提供了这两个函数作为 sendmsg/recvmsg 的扩展。

总结:

sendmsg 和 recvmsg 是 Linux 套接字编程中最强大和灵活的 I/O 函数。它们不仅包含了其他所有基本套接字函数的功能,还引入了处理辅助数据的能力,这使得它们在高级进程间通信(如传递文件描述符)中不可或缺。虽然使用起来比 send/recv 复杂,但掌握它们对于编写高效、功能丰富的网络应用程序至关重要。

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注