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

(图片来源网络,侵删)
sendmsg 是什么?
sendmsg 函数通过一个 msghdr 结构体来描述要发送的所有信息,这个结构体包含了:
- 发送的数据:一个或多个缓冲区。
- 目标地址:数据要发送到的地址(用于面向非连接的套接字,如 UDP)。
- 辅助数据:也称为“控制消息”,可以用来传递额外的信息,最常见的是在进程间传递文件描述符。
函数原型
#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 结构体,它的定义如下:

(图片来源网络,侵删)
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_name 和 msg_namelen
- 作用: 这与
sendto函数中的dest_addr和addrlen参数完全相同。 - 使用场景: 仅当套接字是面向非连接的(
type为SOCK_DGRAM的 UDP 套接字)时才需要,对于面向连接的套接字(type为SOCK_STREAM的 TCP 套接字),此字段会被忽略,因为连接已经建立。 - 值: 如果不需要指定目标地址,可以将
msg_name设为NULL,msg_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数组指向的多个不连续的缓冲区中。
- Scatter (分散) 写:
-
msg_iovlen: 指定iovec数组中有多少个元素。
(图片来源网络,侵删)
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[]; // 实际数据紧跟在后面 };- 传递文件描述符的步骤:
- 创建一个缓冲区来存放
cmsghdr和文件描述符数组。 - 填充
cmsghdr结构体。 - 将要传递的文件描述符数组放在
cmsg_data区域。 - 设置
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;
}
编译和运行:
- 分别编译两个文件:
gcc send_fd.c -o send_fd和gcc recv_fd.c -o recv_fd。 - 先运行接收端:
./recv_fd。 - 再运行发送端:
./send_fd。 - 你会看到接收端成功接收并读取了发送端创建的文件内容。
sendmsg vs. send / sendto
| 特性 | send / sendto |
sendmsg |
|---|---|---|
| 数据缓冲区 | 单个、连续的缓冲区 (void *buf, size_t len) |
多个、不连续的缓冲区 (struct iovec 数组) |
| 目标地址 | sendto 有地址参数;send 无 (仅用于连接套接字) |
通过 msghdr 结构体统一管理 |
| 辅助数据 | 不支持 | 强大的支持,可传递文件描述符等元数据 |
| 复杂度 | 简单,适用于常规发送 | 复杂,功能强大,适用于高性能和高级场景 |
| 性能 | 发送多块数据需要多次系统调用或手动合并缓冲区 | 一次系统调用即可发送多块数据,效率更高 |
sendmsg 是 C 语言网络编程中的一个“瑞士军刀”,虽然它的使用比 send 和 sendto 更复杂,但它在以下场景中是不可替代的:
- 高性能网络应用: 当你需要发送多个数据块时,使用
sendmsg可以避免数据拷贝,减少系统调用次数,从而提高性能。 - 需要传递元数据的场景: 特别是在进程间传递文件描述符,
sendmsg是标准且安全的方式(相比fork共享描述符,传递描述符可以用于不相关的进程)。
如果你正在编写一个简单的客户端或服务器,send 和 sendto 可能就足够了,但如果你在构建一个高性能的中间件、一个需要动态传递资源的系统,或者一个复杂的 RPC 框架,那么深入理解和掌握 sendmsg 是非常有价值的。
