在上一篇文章Unix套接字编程及通信例子中,我们对Unix套接字编程有一个基本的了解。但在Unix套接字编程的领域中,有一组特殊而强大的工具:socketpair
、sendmsg
和 recvmsg
,它们为实现本地进程间通信提供了便捷的方式。
文章目录
- 1 socketpair
- 2 sendmsg和recvmsg
- 2.1 函数原型
- 2.2 msghdr结构体
- 2.3 cmsghdr结构体
- 2.4 实例
- 2.4.1 初始化
- 2.4.2 子进程实现
- 2.4.3 父进程实现
- 2.4.4 实验结果
- 2.4.5 完整代码
1 socketpair
socketpair
是一个用于在同一台计算机上创建一对相互连接的套接字的系统调用。这对套接字可以用于进程间的本地通信,通常用于父子进程或兄弟进程之间。它创建的套接字对是相互连接的,因此数据可以直接在这两个套接字之间传递,而无需经过内核缓冲区,从而提高了通信的效率。
int socketpair(int domain, int type, int protocol, int sv[2]);
domain
:地址族,通常设置为AF_UNIX
,表示使用Unix域套接字。type
:套接字类型,通常设置为SOCK_STREAM
或SOCK_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 dataprintf("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 */
};
-
msg_name
(消息地址):用于指定消息的目标地址,通常在发送消息时为
sendmsg
提供目标地址信息,而在接收消息时,可以存储发送方的地址。通常设置为NULL
,表示不指定目标地址。 -
msg_namelen
(地址长度):用于指定
msg_name
指向的地址结构的长度。通常在发送消息时为sendmsg
提供地址结构的长度,而在接收消息时则用于存储实际接收到的地址的长度。 -
msg_iov
(I/O 向量):msg_iov
是一个指向struct iovec
结构的指针,该结构用于指定消息中的数据缓冲区,可以是多个缓冲区。通过msg_iovlen
来指定缓冲区数组的长度。struct iovec {void *iov_base; /* 指向缓冲区的起始地址 */size_t iov_len; /* 缓冲区的大小 */ };
-
msg_iovlen
(I/O 向量长度):用于指定
msg_iov
指向的缓冲区数组的长度,即消息中包含多少个缓冲区。- 所以
sendmsg/recvmsg
与sendto/recvfrom
最明显的不同是,前者可以通过msg_iov
和msg_iovlen
发送/接收多个缓冲区,而后者只能发送/接收一个。
- 所以
-
msg_control
(控制信息):msg_control
用于传递辅助信息,通常是控制信息或者辅助数据。这可以包括在套接字编程中使用的辅助信息,如辅助文件描述符等。通常设置为NULL
,表示不传递控制信息。 -
msg_controllen
(控制信息长度):用于指定
msg_control
指向的控制信息的长度。在发送消息时,为sendmsg
提供控制信息的长度,而在接收消息时,用于存储实际接收到的控制信息的长度。 -
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
常见取值:
SOL_SOCKET
: 表示这是与套接字相关的辅助数据。- 自定义层级: 除了
SOL_SOCKET
,还可以定义其他自定义的层级,用于特定的应用或协议
cmsg_type
常见取值(对于SOL_SOCKET
层级):
SCM_RIGHTS
: 表示辅助数据用于传递文件描述符。SCM_CREDENTIALS
: 表示辅助数据用于传递进程凭证(例如用户标识)。
在Linux中提供了一些宏定义来使用这个结构体:
-
CMSG_FIRSTHDR
宏: 获取消息头的第一个辅助数据块。如果消息头中没有足够的空间来存储一个struct cmsghdr
,则返回NULL
。#define CMSG_FIRSTHDR(mhdr) ((mhdr)->msg_controllen >= sizeof(struct cmsghdr) ? (struct cmsghdr *)(mhdr)->msg_control : NULL)
-
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)))
-
CMSG_DATA
宏: 获取辅助数据块中实际数据的指针。通过传递辅助数据块的指针,可以获取实际数据的起始位置。#define CMSG_DATA(cmsg) ((unsigned char *)(cmsg) + CMSG_ALIGN(sizeof(struct cmsghdr)))
-
CMSG_LEN
宏: 计算一个辅助数据块的总长度,包括头部和实际数据。- 它的值等于结构体
cmsghdr
中的cmsg_len
字段的值
#define CMSG_LEN(len) (_CMSG_HDR_ALIGN(sizeof(struct cmsghdr)) + (len))
- 它的值等于结构体
-
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_len
、cmsg_level
和cmsg_type
字段。
cmsg_level
设置为SOL_SOCKET
,表示这是一个与套接字相关的辅助数据cmsg_type
设置为SCM_RIGHTS
, 表示这是一个用于传递文件描述符的辅助数据块*((int *)CMSG_DATA(cmsg))
设置cmsg
的data
字段为文件描述符,这里打开目录下的example.txt
文件
(4)发送数据
sendmsg(sockfd[1], &message, 0);
2.4.3 父进程实现
父进程则接收子进程发来的消息
(1)声明接收缓冲区和辅助信息结构体
接收和发送的数据大小要匹配,这里设置接收的iov_base
为buffer2
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 1024int 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;
}