我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 recvmsg
和 sendmsg
函数,它们是功能最强大、最通用的套接字 I/O 函数,可以处理 read
/write
、send
/recv
以及 sendto
/recvfrom
的所有功能,并且还支持更高级的特性,如传输文件描述符、**发送和接收访问控制列表 **(ancillary data)。
1. 函数介绍
recvmsg
和 sendmsg
是 Linux 系统调用,它们提供了最灵活和最完整的套接字数据传输接口。它们是 read
/write
、send
/recv
、sendto
/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_iovlen
:msg_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_DONTWAIT
,MSG_PEEK
,MSG_WAITALL
,MSG_NOSIGNAL
等。
- 常见标志:
5. 返回值
- 成功时:
- 返回实际传输的字节数(即所有
msg_iov
缓冲区中数据的总和)。
- 返回实际传输的字节数(即所有
- 失败时:
- 返回 -1,并设置全局变量
errno
来指示具体的错误原因。
- 返回 -1,并设置全局变量
6. 相似函数,或关联函数
send
/sendto
/recv
/recvfrom
:sendmsg
/recvmsg
是这些函数的通用形式。writev
/readv
:sendmsg
/recvmsg
结合iovec
实现了类似的功能。sendmmsg
/recvmmsg
: (Linux 特有) 可以在一次系统调用中发送或接收多个消息,性能更高。CMSG_*
宏: 用于处理msg_control
中的辅助数据(如CMSG_FIRSTHDR
,CMSG_NXTHDR
,CMSG_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;
}
代码解释:
- 客户端和服务器都创建 UDP 套接字并进行必要的设置(服务器需要
bind
)。 - 发送数据:
- 准备一个
struct iovec
数组。这里只用一个元素指向消息缓冲区。 - 准备一个
struct msghdr
结构体msg
。 - 设置
msg.msg_name
为目标地址,msg.msg_iov
为缓冲区向量,msg.msg_iovlen
为向量长度。 - 调用
sendmsg(sock, &msg, 0)
发送数据。
- 准备一个
- 接收数据:
- 准备
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;
}
代码解释:
- 客户端使用 TCP 连接到服务器。
- 客户端将一个消息分割成三个部分,存放在
part1
,part2
,part3
三个不同的缓冲区中。 - 发送: 使用
sendmsg
和包含三个元素的iovec
数组,将这三个缓冲区的数据一次性发送出去。这避免了三次send
调用。 - 服务器
accept
连接。 - 接收: 服务器使用
recvmsg
和包含三个元素的iovec
数组,将接收到的数据分散存储到buffer1
,buffer2
,buffer3
三个不同的缓冲区中。 - 服务器打印出存储在各个缓冲区中的数据。
示例 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
代码解释:
- 该代码通过宏定义
SENDER
和RECEIVER
编译成两个不同的程序。 - **发送方 **(
SENDER
)- 创建一个名为
testfile.txt
的文件并写入数据。 - 创建一个 Unix 域套接字并连接到接收方。
- 准备一个
struct msghdr
。 - 准备一个
iovec
来发送一些普通数据(“FD”)。 - 关键: 准备辅助数据(
msg_control
)。- 使用
CMSG_SPACE(sizeof(int))
来分配足够的控制缓冲区。 - 使用
CMSG_FIRSTHDR
获取第一个控制消息头指针。 - 设置
cmsg_level
为SOL_SOCKET
,cmsg_type
为SCM_RIGHTS
(表示传递文件描述符)。 - 使用
CMSG_DATA(cmsg)
获取数据部分的指针,并将file_fd
复制进去。
- 使用
- 调用
sendmsg
发送包含普通数据和辅助数据(文件描述符)的消息。
- 创建一个名为
- **接收方 **(
RECEIVER
)- 创建并监听一个 Unix 域套接字。
accept
连接。- 准备
struct msghdr
和用于接收辅助数据的控制缓冲区ctrl_buf
。 - 调用
recvmsg
接收消息。 - 关键: 解析接收到的辅助数据。
- 使用
CMSG_FIRSTHDR
获取第一个控制消息头。 - 检查
cmsg_level
,cmsg_type
,cmsg_len
是否正确。 - 如果正确,使用
CMSG_DATA(cmsg)
获取数据部分,并将文件描述符复制到received_fd
变量中。
- 使用
- 现在,接收方可以像使用自己打开的文件一样使用
received_fd
。 - 最后关闭接收到的文件描述符和套接字。
重要提示与注意事项:
- 通用性:
sendmsg
/recvmsg
是最通用的套接字 I/O 函数,可以替代所有其他基本的发送和接收函数。 - Scatter-Gather: 通过
iovec
数组,可以高效地处理不连续的数据缓冲区,减少系统调用次数。 - 地址处理: 对于面向连接的套接字(如 TCP),
msg_name
通常为NULL
。对于无连接的(如 UDP),它用于指定目标或获取源地址。 - 辅助数据: 这是
sendmsg
/recvmsg
独有的强大功能。正确处理辅助数据需要使用CMSG_*
系列宏,这比较复杂但非常有用(如传递文件描述符、设置网络接口等)。 - 性能: 在需要处理大量数据或复杂消息结构时,
sendmsg
/recvmsg
可能比多次调用send
/recv
更高效。 sendmmsg
/recvmmsg
: 对于需要在一次系统调用中处理多个消息的高性能应用(如网络服务器),Linux 提供了这两个函数作为sendmsg
/recvmsg
的扩展。
总结:
sendmsg
和 recvmsg
是 Linux 套接字编程中最强大和灵活的 I/O 函数。它们不仅包含了其他所有基本套接字函数的功能,还引入了处理辅助数据的能力,这使得它们在高级进程间通信(如传递文件描述符)中不可或缺。虽然使用起来比 send
/recv
复杂,但掌握它们对于编写高效、功能丰富的网络应用程序至关重要。