C语言sendmsg如何正确使用?

99ANYc3cd6
预计阅读时长 37 分钟
位置: 首页 C语言 正文

sendmsg 是一个比 sendsendto 更强大、更底层的网络 I/O 函数,它允许你一次性发送多个数据块,并且可以附加控制信息(如文件描述符),这在高级网络编程中非常有用。

c语言 sendmsg
(图片来源网络,侵删)

sendmsg 是什么?

sendmsg 函数通过一个 msghdr 结构体来描述要发送的所有信息,这个结构体包含了:

  1. 发送的数据:一个或多个缓冲区。
  2. 目标地址:数据要发送到的地址(用于面向非连接的套接字,如 UDP)。
  3. 辅助数据:也称为“控制消息”,可以用来传递额外的信息,最常见的是在进程间传递文件描述符。

函数原型

#include <sys/socket.h>
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

参数说明:

  • sockfd: 套接字文件描述符。
  • msg: 指向 msghdr 结构体的指针,该结构体封装了所有发送信息。
  • flags: 传递给套接字的标志,常用的有 MSG_DONTWAIT(非阻塞发送)、MSG_NOSIGNAL(在 TCP 套接字上发送到已关闭的连接时不产生 SIGPIPE 信号)等,通常设为 0。

返回值:

  • 成功时,返回实际发送的字节数。
  • 出错时,返回 -1,并设置 errno

核心结构体 msghdr

sendmsg 的威力完全来自于 msghdr 结构体,它的定义如下:

c语言 sendmsg
(图片来源网络,侵删)
struct msghdr {
    void         *msg_name;       // 指向目标地址的指针 (用于 UDP/SOCK_DGRAM)
    socklen_t     msg_namelen;    // 目标地址的长度
    struct iovec *msg_iov;        // 指向分散/聚集数组 (iovec) 的指针
    int           msg_iovlen;     // iovec 数组中的元素个数
    void         *msg_control;    // 指向辅助数据 (控制消息) 的缓冲区
    socklen_t     msg_controllen; // 辅助数据缓冲区的大小
    int           msg_flags;      // 通常设置为0,由内核填充
};

1 msg_namemsg_namelen

  • 作用: 这与 sendto 函数中的 dest_addraddrlen 参数完全相同。
  • 使用场景: 仅当套接字是面向非连接的(typeSOCK_DGRAM 的 UDP 套接字)时才需要,对于面向连接的套接字(typeSOCK_STREAM 的 TCP 套接字),此字段会被忽略,因为连接已经建立。
  • : 如果不需要指定目标地址,可以将 msg_name 设为 NULLmsg_namelen 设为 0。

2 msg_iov, msg_iovlen (分散/聚集 I/O - Scatter/Gather I/O)

这是 sendmsg 最核心的功能之一,允许你一次性发送多个不连续的内存块

  • 作用: msg_iov 指向一个 iovec 结构体数组。iovec 的定义如下:

    struct iovec {
        void  *iov_base; // 缓冲区起始地址
        size_t iov_len;  // 缓冲区长度
    };
  • 工作原理:

    • Scatter (分散) 写: sendmsg 会按照 msg_iov 数组的顺序,依次从每个 iov_base 指向的内存地址中读取 iov_len 字节的数据,然后将它们按顺序拼接成一个连续的数据流发送出去,这避免了需要先将所有数据拷贝到一个大的、连续的缓冲区中。
    • Gather (聚集) 读: recvmsg 则执行相反的操作,将接收到的连续数据流按顺序填充到 msg_iov 数组指向的多个不连续的缓冲区中。
  • msg_iovlen: 指定 iovec 数组中有多少个元素。

    c语言 sendmsg
    (图片来源网络,侵删)

3 msg_control, msg_controllen (辅助数据 / 控制消息)

  • 作用: 用于发送“元数据”或“控制信息”,而不是应用程序数据,最常见的用途是在进程间传递文件描述符
  • 数据格式: 辅助数据由一系列 cmsghdr (control message header) 结构体组成。msg_control 指向这个消息块的起始地址,msg_controllen 是整个消息块的总长度。
  • cmsghdr 结构体:
    struct cmsghdr {
        socklen_t cmsg_len;   // 数据部分的总长度 (包括本结构体)
        int       cmsg_level; // 协议层 (SOL_SOCKET)
        int       cmsg_type;  // 消息类型 (SCM_RIGHTS)
        // unsigned char cmsg_data[]; // 实际数据紧跟在后面
    };
  • 传递文件描述符的步骤:
    1. 创建一个缓冲区来存放 cmsghdr 和文件描述符数组。
    2. 填充 cmsghdr 结构体。
    3. 将要传递的文件描述符数组放在 cmsg_data 区域。
    4. 设置 msg_control 指向这个缓冲区,msg_controllen 为缓冲区总大小。

使用示例

示例 1:使用 sendmsg 发送多个缓冲区 (Scatter/Gather)

这个例子展示了如何将两个字符串拼接成一个数据包发送,而不需要手动合并它们。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    // 1. 创建 UDP 套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }
    // 2. 设置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0) {
        perror("inet_pton");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    // 3. 准备要发送的数据 (两个不连续的缓冲区)
    const char *part1 = "Hello, ";
    const char *part2 = "world!";
    // 4. 设置 msghdr 结构体
    struct msghdr msg;
    struct iovec iov[2]; // 两个缓冲区
    // 设置第一个缓冲区
    iov[0].iov_base = (void *)part1;
    iov[0].iov_len = strlen(part1);
    // 设置第二个缓冲区
    iov[1].iov_base = (void *)part2;
    iov[1].iov_len = strlen(part2);
    msg.msg_iov = iov;
    msg.msg_iovlen = 2; // 两个缓冲区
    // 对于 UDP,需要指定目标地址
    msg.msg_name = &server_addr;
    msg.msg_namelen = sizeof(server_addr);
    // 不使用辅助数据和控制消息
    msg.msg_control = NULL;
    msg.msg_controllen = 0;
    // 5. 发送消息
    ssize_t num_bytes = sendmsg(sockfd, &msg, 0);
    if (num_bytes < 0) {
        perror("sendmsg");
    } else {
        printf("Sent %zd bytes using sendmsg.\n", num_bytes);
    }
    close(sockfd);
    return 0;
}

示例 2:使用 sendmsg 传递文件描述符

这是一个更高级的例子,展示了如何在两个相关进程(通常是父子进程)之间传递一个文件描述符。

发送端代码 (send_fd.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h> // 用于 Unix domain socket
#include <fcntl.h>
// 辅助函数:构建用于发送文件描述符的控制消息
void send_fd(int sock_fd, int fd_to_send) {
    struct msghdr msg;
    struct cmsghdr *cmsg;
    struct iovec iov;
    char buf[CMSG_SPACE(sizeof(int))]; // 足够大的缓冲区
    int data = 42; // 一些数据,与 fd 一起发送
    // 设置 iovec
    iov.iov_base = &data;
    iov.iov_len = sizeof(data);
    // 设置 msghdr
    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = buf;
    msg.msg_controllen = sizeof(buf);
    // 构建 cmsghdr
    cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS; // 表示正在传递文件描述符
    cmsg->cmsg_len = CMSG_LEN(sizeof(int));
    *(int *)CMSG_DATA(cmsg) = fd_to_send; // 将要传递的 fd 放入数据区
    // 发送消息
    if (sendmsg(sock_fd, &msg, 0) < 0) {
        perror("sendmsg");
        exit(EXIT_FAILURE);
    }
}
int main() {
    int sock_fd;
    struct sockaddr_un addr;
    // 创建 Unix domain socket
    sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock_fd < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }
    // 绑定地址
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, "/tmp/fd_socket.sock", sizeof(addr.sun_path) - 1);
    unlink(addr.sun_path); // 如果已存在则删除
    if (bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("bind");
        close(sock_fd);
        exit(EXIT_FAILURE);
    }
    // 监听
    if (listen(sock_fd, 1) < 0) {
        perror("listen");
        close(sock_fd);
        exit(EXIT_FAILURE);
    }
    printf("Waiting for a connection...\n");
    int conn_fd = accept(sock_fd, NULL, NULL);
    if (conn_fd < 0) {
        perror("accept");
        close(sock_fd);
        exit(EXIT_FAILURE);
    }
    close(sock_fd); // 监听套接字可以关闭了
    // 打开一个文件,准备传递它的描述符
    int file_fd = open("testfile.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
    if (file_fd < 0) {
        perror("open");
        close(conn_fd);
        exit(EXIT_FAILURE);
    }
    write(file_fd, "This is a test file.", 20);
    printf("Opened file with fd: %d\n", file_fd);
    // 使用 sendmsg 传递文件描述符
    send_fd(conn_fd, file_fd);
    // 关闭本地文件描述符,但接收端现在拥有它
    close(file_fd);
    close(conn_fd);
    return 0;
}

接收端代码 (recv_fd.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <fcntl.h>
// 辅助函数:接收文件描述符
int recv_fd(int sock_fd) {
    struct msghdr msg;
    struct cmsghdr *cmsg;
    struct iovec iov;
    char buf[CMSG_SPACE(sizeof(int))];
    int data;
    int received_fd;
    // 设置 iovec
    iov.iov_base = &data;
    iov.iov_len = sizeof(data);
    // 设置 msghdr
    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = buf;
    msg.msg_controllen = sizeof(buf);
    // 接收消息
    if (recvmsg(sock_fd, &msg, 0) < 0) {
        perror("recvmsg");
        exit(EXIT_FAILURE);
    }
    // 从 cmsghdr 中提取文件描述符
    cmsg = CMSG_FIRSTHDR(&msg);
    if (cmsg && cmsg->cmsg_type == SCM_RIGHTS) {
        received_fd = *(int *)CMSG_DATA(cmsg);
        printf("Received file descriptor: %d\n", received_fd);
        return received_fd;
    } else {
        fprintf(stderr("No file descriptor received.\n"));
        exit(EXIT_FAILURE);
    }
}
int main() {
    int sock_fd;
    struct sockaddr_un addr;
    // 创建 Unix domain socket
    sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock_fd < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }
    // 连接到服务器
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, "/tmp/fd_socket.sock", sizeof(addr.sun_path) - 1);
    if (connect(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("connect");
        close(sock_fd);
        exit(EXIT_FAILURE);
    }
    // 接收文件描述符
    int new_fd = recv_fd(sock_fd);
    close(sock_fd); // 关闭连接套接字
    // 使用接收到的文件描述符进行操作
    char read_buf[100];
    lseek(new_fd, 0, SEEK_SET); // 将文件指针移到开头
    int bytes_read = read(new_fd, read_buf, sizeof(read_buf) - 1);
    if (bytes_read > 0) {
        read_buf[bytes_read] = '\0';
        printf("Read from received fd %d: %s\n", new_fd, read_buf);
    }
    close(new_fd);
    unlink("/tmp/fd_socket.sock"); // 清理 socket 文件
    return 0;
}

编译和运行:

  1. 分别编译两个文件:gcc send_fd.c -o send_fdgcc recv_fd.c -o recv_fd
  2. 先运行接收端:./recv_fd
  3. 再运行发送端:./send_fd
  4. 你会看到接收端成功接收并读取了发送端创建的文件内容。

sendmsg vs. send / sendto

特性 send / sendto sendmsg
数据缓冲区 单个、连续的缓冲区 (void *buf, size_t len) 多个、不连续的缓冲区 (struct iovec 数组)
目标地址 sendto 有地址参数;send 无 (仅用于连接套接字) 通过 msghdr 结构体统一管理
辅助数据 不支持 强大的支持,可传递文件描述符等元数据
复杂度 简单,适用于常规发送 复杂,功能强大,适用于高性能和高级场景
性能 发送多块数据需要多次系统调用或手动合并缓冲区 一次系统调用即可发送多块数据,效率更高

sendmsg 是 C 语言网络编程中的一个“瑞士军刀”,虽然它的使用比 sendsendto 更复杂,但它在以下场景中是不可替代的:

  1. 高性能网络应用: 当你需要发送多个数据块时,使用 sendmsg 可以避免数据拷贝,减少系统调用次数,从而提高性能。
  2. 需要传递元数据的场景: 特别是在进程间传递文件描述符sendmsg 是标准且安全的方式(相比 fork 共享描述符,传递描述符可以用于不相关的进程)。

如果你正在编写一个简单的客户端或服务器,sendsendto 可能就足够了,但如果你在构建一个高性能的中间件、一个需要动态传递资源的系统,或者一个复杂的 RPC 框架,那么深入理解和掌握 sendmsg 是非常有价值的。

-- 展开阅读全文 --
头像
织梦为何出现两个根网址?如何解决?
« 上一篇 04-17
dede会员视频功能如何开发实现?
下一篇 » 04-17

相关文章

取消
微信二维码
支付宝二维码

目录[+]