Linux socket编程(12):Unix套接字之socketpair、sendmsg和recvmsg详解

2023-12-13 05:45:07

在上一篇文章Unix套接字编程及通信例子中,我们对Unix套接字编程有一个基本的了解。但在Unix套接字编程的领域中,有一组特殊而强大的工具:socketpairsendmsgrecvmsg,它们为实现本地进程间通信提供了便捷的方式。

1 socketpair

socketpair是一个用于在同一台计算机上创建一对相互连接的套接字的系统调用。这对套接字可以用于进程间的本地通信,通常用于父子进程或兄弟进程之间。它创建的套接字对是相互连接的,因此数据可以直接在这两个套接字之间传递,而无需经过内核缓冲区,从而提高了通信的效率。

int socketpair(int domain, int type, int protocol, int sv[2]);
  • domain:地址族,通常设置为 AF_UNIX,表示使用Unix域套接字。
  • type:套接字类型,通常设置为 SOCK_STREAMSOCK_DGRAM
  • protocol:指定使用的协议,通常设置为 0,表示使用默认协议。
  • sv:一个包含两个整数的数组,用于存储创建的套接字描述符。

这和匿名管道(pipe)很像,但匿名管道中的文件描述符是单方向的,只能支持一个方向的数据流,其中描述符0固定用于读,描述符1固定用于写。而socketpair是一个全双工通信通道,它同时支持双向的数据流。两个文件描述符都支持双向通信,下面来看一个例子:

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

void send_message(int sockfd, const char* message) {
    send(sockfd, message, strlen(message), 0);
}

void receive_message(int sockfd, char* buffer, size_t buffer_size) {
    ssize_t received_bytes = recv(sockfd, buffer, buffer_size - 1, 0);
    if (received_bytes > 0) {
        buffer[received_bytes] = '\0';  // Null-terminate the received data
        printf("Received: %s\n", buffer);
    } else {
        perror("Error receiving message");
    }
}

int main() {
    int sv[2];

    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {
        perror("Error creating socket pair");
        return -1;
    }

    pid_t pid = fork();

    if (pid == -1) {
        perror("Error forking");
        return -1;
    }

    if (pid == 0) {// 子进程
        close(sv[0]);
        char buffer[1024];
        receive_message(sv[1], buffer, sizeof(buffer));
        send_message(sv[1], "get a message from father");
        close(sv[1]);  // 关闭写端
    } else {// 父进程
        close(sv[1]);
        send_message(sv[0], "123");
        char buffer[1024];
        receive_message(sv[0], buffer, sizeof(buffer));
        close(sv[0]);  // 关闭读端
    }
    return 0;
}

在这个例子中,创建了一个子进程,其中sv[0]用于表示子进程的套接字,sv[1]用于表示父进程的套接字。在父进程中,向子进程发送123后开始接收数据,而子进程收到123后发送get a message from father给父进程,然后退出程序。父进程收到后也退出程序。实验结果如下:

在这里插入图片描述

在这里sv[0]sv[1]既用来读也用来写,表明这两个套接字都是全双工的。

2 sendmsg和recvmsg

2.1 函数原型

sendmsg函数向套接字发送消息,允许同时发送多个缓冲区的数据以及附带文件描述符等辅助信息。

recvmsg函数用于接收通过套接字传输的消息,并允许接收辅助数据,如控制信息、文件描述符等。

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
  • sockfd:套接字描述符
  • msg:指向 struct msghdr结构的指针,该结构定义了消息的各个部分,包括数据缓冲区、控制信息等
  • flags:标志参数,通常设置为 0

2.2 msghdr结构体

struct msghdr结构的定义如下:

struct msghdr {
    void         *msg_name;       /* optional address */
    socklen_t     msg_namelen;    /* size of address */
    struct iovec *msg_iov;        /* scatter/gather array */
    size_t        msg_iovlen;     /* # elements in msg_iov */
    void         *msg_control;    /* ancillary data, see below */
    size_t        msg_controllen; /* ancillary data buffer len */
    int           msg_flags;      /* flags on received message */
};
  1. msg_name(消息地址):

    用于指定消息的目标地址,通常在发送消息时为sendmsg提供目标地址信息,而在接收消息时,可以存储发送方的地址。通常设置为NULL,表示不指定目标地址。

  2. msg_namelen(地址长度):

    用于指定msg_name指向的地址结构的长度。通常在发送消息时为sendmsg提供地址结构的长度,而在接收消息时则用于存储实际接收到的地址的长度。

  3. msg_iov(I/O 向量):

    msg_iov是一个指向struct iovec结构的指针,该结构用于指定消息中的数据缓冲区,可以是多个缓冲区。通过msg_iovlen来指定缓冲区数组的长度。

    struct iovec {
        void  *iov_base; /* 指向缓冲区的起始地址 */
        size_t iov_len;  /* 缓冲区的大小 */
    };
    
  4. msg_iovlen(I/O 向量长度):

    用于指定msg_iov指向的缓冲区数组的长度,即消息中包含多少个缓冲区。

    • 所以sendmsg/recvmsgsendto/recvfrom最明显的不同是,前者可以通过msg_iovmsg_iovlen发送/接收多个缓冲区,而后者只能发送/接收一个。
  5. msg_control(控制信息):

    msg_control用于传递辅助信息,通常是控制信息或者辅助数据。这可以包括在套接字编程中使用的辅助信息,如辅助文件描述符等。通常设置为NULL,表示不传递控制信息。

  6. msg_controllen(控制信息长度):

    用于指定 msg_control 指向的控制信息的长度。在发送消息时,为sendmsg提供控制信息的长度,而在接收消息时,用于存储实际接收到的控制信息的长度。

  7. msg_flags(消息标志):

    用于存储消息的标志,包括一些操作的状态信息。在recvmsg函数中,可以通过msg_flags获取一些接收消息时的状态信息。

2.3 cmsghdr结构体

在使用msg_control时,通常会搭配使用struct cmsghdr结构,该结构定义了一种通用的、可扩展的辅助数据头部。

struct cmsghdr {
    socklen_t cmsg_len;    /* 辅助数据的总长度 */
    int       cmsg_level;  /* 源层协议,一般设置为 SOL_SOCKET */
    int       cmsg_type;   /* 辅助数据的类型 */
    /* 后续紧随辅助数据 */
    //unsigned char cmsg_data[];
};

cmsg_level常见取值:

  1. SOL_SOCKET 表示这是与套接字相关的辅助数据。
  2. 自定义层级: 除了 SOL_SOCKET,还可以定义其他自定义的层级,用于特定的应用或协议

cmsg_type 常见取值(对于SOL_SOCKET层级):

  1. SCM_RIGHTS 表示辅助数据用于传递文件描述符。
  2. SCM_CREDENTIALS 表示辅助数据用于传递进程凭证(例如用户标识)。

在Linux中提供了一些宏定义来使用这个结构体:

  1. CMSG_FIRSTHDR宏: 获取消息头的第一个辅助数据块。如果消息头中没有足够的空间来存储一个struct cmsghdr,则返回 NULL

    #define CMSG_FIRSTHDR(mhdr) ((mhdr)->msg_controllen >= sizeof(struct cmsghdr) ? (struct cmsghdr *)(mhdr)->msg_control : NULL)
    
  2. CMSG_NXTHDR宏: 获取下一个辅助数据块。通过传递当前的辅助数据块,可以获取下一个辅助数据块的指针。如果没有下一个块,返回 NULL

    #define CMSG_NXTHDR(mhdr, cmsg) ((char *)(cmsg) + CMSG_ALIGN((cmsg)->cmsg_len) + sizeof(struct cmsghdr) > (char *)(mhdr)->msg_control + (mhdr)->msg_controllen ? NULL : (struct cmsghdr *)((char *)(cmsg) + CMSG_ALIGN((cmsg)->cmsg_len)))
    
  3. CMSG_DATA宏: 获取辅助数据块中实际数据的指针。通过传递辅助数据块的指针,可以获取实际数据的起始位置。

    #define CMSG_DATA(cmsg) ((unsigned char *)(cmsg) + CMSG_ALIGN(sizeof(struct cmsghdr)))
    
  4. CMSG_LEN宏: 计算一个辅助数据块的总长度,包括头部和实际数据。

    • 它的值等于结构体cmsghdr中的cmsg_len字段的值
    #define CMSG_LEN(len) (_CMSG_HDR_ALIGN(sizeof(struct cmsghdr)) + (len))
    
  5. CMSG_SPACE宏: 计算辅助数据块所需的总空间,包括头部和实际数据,并进行对齐

    #define CMSG_SPACE(len) (_CMSG_HDR_ALIGN(sizeof(struct cmsghdr)) + _CMSG_DATA_ALIGN(len))
    

    如下图所示,为msg_control字段的示意图

在这里插入图片描述

  • 图中的pad是为了字节对齐的填充部分

2.4 实例

上面的理论挺复杂的,理论还是得通过实践才能更好的理解。

目的:使用多个struct iovec来发送和接收一个缓冲区的数据,并在msg_control字段中传递文件描述符作为辅助数据。

2.4.1 初始化

首先声明两个buffer,这里设置buffer1的初始值为0xab,而buffer2的初始值为0xcd,为了后续判断内容是否成功接收。然后创建用于父子进程通信的套接字,并fork子进程。

#define BUF_SIZE 1024
unsigned char buffer1[BUF_SIZE], buffer2[BUF_SIZE];
memset(buffer1, 0xab, sizeof(buffer1));
memset(buffer2, 0xcd, sizeof(buffer2));

int sockfd[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sockfd);
pid_t pid = fork();

2.4.2 子进程实现

子进程发送buffer1的内容给父进程,同时在辅助信息中传递一个文件描述符。

(1)声明msghdr结构体

struct msghdr message = {0};

(2)填充发送缓冲区

首先填充我们要发送的字段:struct iovec *msg_iov,这里声明一个字段iov[1],内容为buffer1

struct iovec iov[1];
iov[0].iov_base = buffer1;
iov[0].iov_len = sizeof(buffer1);

message.msg_iov = iov;
message.msg_iovlen = 1;

(3)填充辅助信息

我们希望通过辅助信息传递文件描述符,首先声明辅助信息字段:

char control_data[CMSG_SPACE(sizeof(int))];

message.msg_control = control_data;
message.msg_controllen = sizeof(control_data);
  • control_data用于存储辅助数据。CMSG_SPACE(sizeof(int))用于计算辅助数据所需的总空间,包括头部和实际数据的空间,并进行对齐。我们现在想传递一个文件描述符(int类型),所以使用 sizeof(int) 计算其大小。

填充辅助信息字段:

struct cmsghdr *cmsg = CMSG_FIRSTHDR(&message);
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;

int file_descriptor = open("example.txt", O_RDONLY);
*((int *)CMSG_DATA(cmsg)) = file_descriptor;

这里的CMSG_FIRSTHDR(&message)实际上就指向message.msg_control,通过这个宏定义强制转换,然后填充cmsg_lencmsg_levelcmsg_type字段。

  • cmsg_level设置为SOL_SOCKET,表示这是一个与套接字相关的辅助数据
  • cmsg_type设置为SCM_RIGHTS, 表示这是一个用于传递文件描述符的辅助数据块
  • *((int *)CMSG_DATA(cmsg))设置cmsgdata字段为文件描述符,这里打开目录下的example.txt文件

(4)发送数据

sendmsg(sockfd[1], &message, 0);

2.4.3 父进程实现

父进程则接收子进程发来的消息

(1)声明接收缓冲区和辅助信息结构体

接收和发送的数据大小要匹配,这里设置接收的iov_basebuffer2

struct iovec iov[1];
iov[0].iov_base = buffer2;
iov[0].iov_len = sizeof(buffer2);

char control_data[CMSG_SPACE(sizeof(int))];
struct msghdr message = {0};
message.msg_iov = iov;
message.msg_iovlen = 1;
message.msg_control = control_data;
message.msg_controllen = sizeof(control_data);

(2)接收消息

recvmsg(sockfd[0], &message, 0);

(3)打印接收缓冲区内容

这里就打印前4字节的内容,如果接收成功buffer2的内容应该为0xab,而不是0xcd。

printf("buffer2[0]~buffer2[4] = %x %x %x %x\n", buffer2[0], buffer2[1], buffer2[2], buffer2[3]);

(4)使用辅助信息中的文件描述符

这里得到子进程传过来的文件描述符,然后打开这个文件并读取到buffer2中,然后输出文件的内容。

// 从辅助数据中获取文件描述符
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&message);
int received_fd;
memcpy(&received_fd, CMSG_DATA(cmsg), sizeof(int));

ssize_t bytes_read = read(received_fd, buffer2, sizeof(buffer2));

2.4.4 实验结果

首先我们需要在目录下创建一个example.txt文件,随便输入一点内容:

在这里插入图片描述

接着我们运行程序,实验结果如下:

在这里插入图片描述

符合我们的预期。

2.4.5 完整代码

#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUF_SIZE 1024

int main() {
    int sockfd[2];
    unsigned char buffer1[BUF_SIZE], buffer2[BUF_SIZE];
	memset(buffer1, 0xab, sizeof(buffer1));
	memset(buffer2, 0xcd, sizeof(buffer2));

    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockfd) == -1) {
        perror("Error creating socket pair");
        return -1;
    }

    pid_t pid = fork();

    if (pid == -1) {
        perror("Error forking");
        return -1;
    }

    if (pid == 0) {
        // 子进程 (发送方)
        close(sockfd[0]);  // 关闭子进程中不需要的读端

        // 打开文件并获取文件描述符
        int file_descriptor = open("example.txt", O_RDONLY);
        if (file_descriptor == -1) {
            perror("Error opening file");
            return -1;
        }

        // 准备消息
        struct iovec iov[1];
        iov[0].iov_base = buffer1;
        iov[0].iov_len = sizeof(buffer1);

        char control_data[CMSG_SPACE(sizeof(int))];
        struct msghdr message = {0};
        message.msg_iov = iov;
        message.msg_iovlen = 1;
        message.msg_control = control_data;
        message.msg_controllen = sizeof(control_data);

        // 构建控制信息头部
        struct cmsghdr *cmsg = CMSG_FIRSTHDR(&message);
        cmsg->cmsg_len = CMSG_LEN(sizeof(int));
        cmsg->cmsg_level = SOL_SOCKET;
        cmsg->cmsg_type = SCM_RIGHTS;

        // 将文件描述符复制到辅助数据中
        *((int *)CMSG_DATA(cmsg)) = file_descriptor;

        // 发送消息
        if (sendmsg(sockfd[1], &message, 0) == -1) {
            perror("Error sending message");
            close(file_descriptor);
            return -1;
        }

        close(file_descriptor);  // 不再需要文件描述符

        close(sockfd[1]);  // 关闭写端
    } else {
        // 父进程 (接收方)
        close(sockfd[1]);  // 关闭父进程中不需要的写端

        struct iovec iov[1];
        iov[0].iov_base = buffer2;
        iov[0].iov_len = sizeof(buffer2);

        char control_data[CMSG_SPACE(sizeof(int))];
        struct msghdr message = {0};
        message.msg_iov = iov;
        message.msg_iovlen = 1;
        message.msg_control = control_data;
        message.msg_controllen = sizeof(control_data);

        // 接收消息
        if (recvmsg(sockfd[0], &message, 0) == -1) {
            perror("Error receiving message");
            return -1;
        }
		printf("buffer2[0]~buffer2[4] = %x %x %x %x\n", buffer2[0], buffer2[1], buffer2[2], buffer2[3]);
        // 从辅助数据中获取文件描述符
        struct cmsghdr *cmsg = CMSG_FIRSTHDR(&message);
        int received_fd;
        memcpy(&received_fd, CMSG_DATA(cmsg), sizeof(int));

        // 读取文件内容
        printf("Received file descriptor: %d\n", received_fd);
        ssize_t bytes_read = read(received_fd, buffer1, sizeof(buffer1));
        if (bytes_read == -1) {
            perror("Error reading file");
            return -1;
        }
        // 打印文件内容
        printf("Received data from file: %.*s\n", (int)bytes_read, buffer1);

        close(received_fd);  // 关闭接收到的文件描述符
        close(sockfd[0]);    // 关闭读端
    }

    return 0;
}

文章来源:https://blog.csdn.net/tilblackout/article/details/134621090
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。