《Linux C编程实战》笔记:socketpair

socketpair() 用于创建 一对互相连接的套接字(sockets)

这两个 socket 是 全双工的,也就是说:

  • A 给 B 写数据

  • B 给 A 写数据

两个方向都可行。

就像一个 管道(pipe) ,但 比 pipe 更强大

至于为什么更强大,其实也很好理解,套接字毕竟是和网络相关的,所以网络那一块的高级特性,它也能用

特性 pipe socketpair
单/双向 单向 双向
用法 文件描述符 socket API
支持 send/recv
支持跨进程
支持传输文件描述符 ✔(UNIX domain)

函数原型

cpp 复制代码
int socketpair(int domain, int type, int protocol, int sv[2]);

调用后:

  • sv[0]sv[1]已经互相连接 的两个 socket(半双工/全双工取决于 type)。分别给两个进程使用

  • 写入一端 → 另一端能读到

  • 常用于:本地进程间通信 IPC、线程通知、事件唤醒、子进程通信、libevent/epoll trick

  • 返回0表示成功,返回-1表示失败.

第一个参数:domain

这个域决定"通信方式",socketpair 只允许本地通信,所以可用值非常有限。

意义 推荐度
AF_UNIX(或 AF_LOCAL UNIX 本地套接字(最常见、最推荐) ⭐⭐⭐⭐⭐
AF_INET 不可以 用于 socketpair
AF_INET6 不可以 用于 socketpair

➡️ 实际开发中 99.9% 固定写 AF_UNIX

第二个参数:type

表示"通信语义":

描述 双工性 是否保证可靠有序
SOCK_STREAM 类似 TCP 全双工 ✔️ 有序、可靠
SOCK_DGRAM 类似 UDP,但本地不丢包 全双工 ✔️ 有序、可靠(但消息保边界)

注意:

  • AF_UNIX + SOCK_DGRAM 是可靠的、不丢包的(不像 UDP)

  • SOCK_STREAM 更常见(像管道)

  • SOCK_DGRAM 会保持消息边界(适合模块间消息队列)

第三个参数:protocol

这个参数几乎 永远填 0

原因:

  • AF_UNIX 下,协议族基本只支持一种协议

  • 你可以理解为 "默认协议"

可取值:

描述
0 自动选择默认协议 ← 最常用
其他值 别填,会失败

第四个参数:sv(输出数组)

输出:

  • sv[0] ← 一个已连接的 socket

  • sv[1] ← 另一个已连接的 socket

行为:

  • sv[0] → 在 sv[1] 读到

  • sv[1] → 在 sv[0] 读到

pipe() 强得多:

  • pipe() 只能单向;socketpair() 可全双工

  • socketpair() 能配 epoll/边沿触发,比 pipe 好用

创建公式

最常用法(99%):

cpp 复制代码
int sv[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sv);

含义:

  • AF_UNIX = 本地 IPC

  • SOCK_STREAM = 有序字节流(像 TCP)

  • 0 = 默认协议

  • sv 两端可读可写(全双工)

socketpair 父子进程双向通信

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <errno.h>

int main() {
	
	int sv[2];
	if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {
		perror("socketpair");
		return 1;
	}
	pid_t pid = fork();
	if (pid < 0) {
		perror("fork");
		return 1;
	}
	//子进程
	else if (pid == 0) {
		close(sv[0]);//关闭父进程的端口
		int child_fd = sv[1];
		//子进程发给父进程
		const char* msg = "Hello from child";
		write(child_fd, msg, sizeof(msg));
		//子进程接受父进程的数据
		char buf[1024] = { '\0' };
		int n = read(child_fd, buf, sizeof(buf));
		if (n > 0) {
			std::cout << "[Child] Receive: " << buf << std::endl;
		}
		close(child_fd);
		return 0;
	}
	//父进程
	else {
		close(sv[1]); // 父关闭子端,只保留自己的
		int parent_fd = sv[0];

		// 父接收子的数据
		char buf[1024] = { 0 };
		int n = read(parent_fd, buf, sizeof(buf));
		if (n > 0)
			std::cout << "[Parent] Received: " << buf << std::endl;

		// 父发送回子
		const char* reply = "Hello from parent";
		write(parent_fd, reply, strlen(reply));

		close(parent_fd);
		return 0;
	}
}

输出:(输出不全是输出缓冲的问题)

bash 复制代码
[Parent] Received: Hello fr
[Child] Receive: Hello from parent

可以看到,父子进程可以相互通信。

作为对比,可以看《Linux C编程实战》笔记:管道_main和submain管道-CSDN博客

这一章里,管道如果要实现全双工通信,必须开两个管道

socketpair + sendmsg/recvmsg + SCM_RIGHTS传递文件描述符(FD)

这是 Unix 进程间通信的高级能力之一:

  • A 进程把一个 FD(文件、socket、管道、设备等)"发送"给 B 进程

  • B 收到后,得到可以直接读写的 FD,就像自己打开的一样

  • 但 B 不需要知道文件路径,也不需要权限(权限由发送者负责)

cpp 复制代码
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <stdlib.h>

int send_fd(int socket, int fd) {
    struct msghdr msg = {0};
    char buf[CMSG_SPACE(sizeof(int))];  // 控制信息空间
    memset(buf, 0, sizeof(buf));

    struct iovec io = { .iov_base = (void*)"F", .iov_len = 1 };
    msg.msg_iov = &io;
    msg.msg_iovlen = 1;

    msg.msg_control = buf;
    msg.msg_controllen = sizeof(buf);

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

    // 把 fd 写入控制数据区
    *((int*) CMSG_DATA(cmsg)) = fd;

    return sendmsg(socket, &msg, 0);
}

int recv_fd(int socket) {
    struct msghdr msg = {0};

    char m_buffer[1];
    struct iovec io = { .iov_base = m_buffer, .iov_len = sizeof(m_buffer) };
    msg.msg_iov = &io;
    msg.msg_iovlen = 1;

    char c_buffer[CMSG_SPACE(sizeof(int))];
    msg.msg_control = c_buffer;
    msg.msg_controllen = sizeof(c_buffer);

    if (recvmsg(socket, &msg, 0) < 0) {
        perror("recvmsg");
        return -1;
    }

    struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
    if (cmsg == nullptr) {
        std::cerr << "No control message received\n";
        return -1;
    }

    int fd;
    memcpy(&fd, CMSG_DATA(cmsg), sizeof(int));
    return fd;
}

int main() {
    int sv[2];
    socketpair(AF_UNIX, SOCK_STREAM, 0, sv);

    pid_t pid = fork();

    if (pid == 0) {
        // 子进程
        close(sv[0]);
        int child_sock = sv[1];

        int fd = recv_fd(child_sock);
        if (fd < 0) {
            std::cerr << "Child failed to receive fd\n";
            return 1;
        }

        std::cout << "[Child] Received FD = " << fd << std::endl;

        // 读文件
        char buf[128] = {0};
        int n = read(fd, buf, sizeof(buf));
        std::cout << "[Child] Read content: " << buf << std::endl;

        close(fd);
        close(child_sock);
    } else {
        // 父进程
        close(sv[1]);
        int parent_sock = sv[0];

        // 父打开文件
        int fd = open("test.txt", O_RDONLY|O_CREAT, S_IRUSR|S_IWUSR| S_IXUSR);
        if (fd < 0) {
            perror("open");
            return 1;
        }

        std::cout << "[Parent] Sending FD = " << fd << std::endl;
        send_fd(parent_sock, fd);

        close(fd);
        close(parent_sock);
    }

    return 0;
}

sendmsg/recvmsg 的难点是这块:

cpp 复制代码
msghdr
 ├── iovec(普通数据)
 └── control(控制数据,包括 FD)
       └── cmsghdr(描述控制数据类型)
              └── CMSG_DATA(真正的 FD)

CMSG_DATA(cmsg) 就是 FD 存放位置。

SCM_RIGHTS 就表示"我要传递文件描述符"。

struct msghdr

cpp 复制代码
struct msghdr {
    void     *msg_name;       // 地址,可用于 UDP,这里不用
    socklen_t msg_namelen;

    struct iovec *msg_iov;    // 普通数据缓冲区
    size_t        msg_iovlen; // 数组元素个数

    void     *msg_control;    // 控制消息(用于传 fd)
    size_t    msg_controllen; // 控制消息长度

    int msg_flags;// 接受消息时的标志
};

struct iovec

cpp 复制代码
struct iovec {
    void  *iov_base; // 数据起始
    size_t iov_len;  // 长度
};

用于发送普通数据(比如本例中的 "F")。

struct cmsghdr(控制消息头)

cpp 复制代码
struct cmsghdr {
    size_t cmsg_len;   // "cmsg_data 的数据长度" + "cmsghdr 结构体本身的长度"
    int    cmsg_level; // 指明该控制消息来自哪个协议族
    int    cmsg_type;  // 具体控制消息类型
    // 后面跟着实际数据,CMSG_DATA() 访问
};

当我们用于传递FD的时候

cpp 复制代码
cmsg_level = SOL_SOCKET
cmsg_type = SCM_RIGHTS

控制消息一定要通过 CMSG_* 宏访问

CMSG_SPACE

cpp 复制代码
size_t CMSG_SPACE(size_t len);

计算控制消息需要的 总空间(包含对齐 padding) 。用于为 msg_control 分配缓存大小。

参数 类型 含义
len size_t 要存放的实际数据长度(例如要传多个 FD 时是 n * sizeof(int)

返回:cmsghdr 结构体长度 + 数据长度 + 必要的对齐填充

即:最终控制消息在 msghdr 中占用的字节数(已对齐)

使用场景

为控制消息区分配空间:

cpp 复制代码
char buf[CMSG_SPACE(sizeof(int))];
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);

如果你想传 3 个文件描述符:

cpp 复制代码
CMSG_SPACE(3 * sizeof(int))

对于这个buf,实际上它才会被提供给内核

cpp 复制代码
recvmsg(sock, &msg, 0);
// 内核会把 FD 放到 buf 里,你通过 CMSG_NXTHDR/CMSG_DATA() 取出来

CMSG_LEN

cpp 复制代码
size_t CMSG_LEN(size_t len);

作用:填充 cmsg_len 字段的正确值(不包含尾部 padding)

参数 含义
len 数据部分的真实长度

返回值

cmsghdr 自身长度 + 数据长度

不包含 padding)(直接赋值给cmsg_len)

典型用法

cpp 复制代码
cmsg->cmsg_len = CMSG_LEN(sizeof(int));

这个值会被内核检查,必须正确。

CMSG_DATA

cpp 复制代码
unsigned char *CMSG_DATA(struct cmsghdr *cmsg);

作用:取得指向数据区域的指针

参数 类型 含义
cmsg struct cmsghdr* 指向控制消息头的指针

返回值

返回指向数据开始处的 unsigned char* 指针

典型用法(发送 FD)

cpp 复制代码
*((int*) CMSG_DATA(cmsg)) = fd;

多个 FD:

cpp 复制代码
int *fds = (int*)CMSG_DATA(cmsg);
fds[0] = fd1;
fds[1] = fd2;

CMSG_FIRSTHDR

cpp 复制代码
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msg);

作用:取得 msg第一个 控制消息的头部

参数 类型 含义
msg struct msghdr* 单次 recvmsg/sendmsg 的消息头

返回值

  • 如果 msg_controllen < sizeof(cmsghdr)返回 NULL

  • 否则返回:

cpp 复制代码
(struct cmsghdr*)msg->msg_control

典型用法(接收 FD)

cpp 复制代码
struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg);
if (cmsg && cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) {
    int fd = *((int*) CMSG_DATA(cmsg));
}

CMSG_NXTHDR

cpp 复制代码
struct cmsghdr *CMSG_NXTHDR(struct msghdr *msg, struct cmsghdr *cmsg);

作用:取得当前控制消息之后的下一个控制消息头

(如果只有一个控制消息,通常用不到)

参数 类型 含义
msg struct msghdr* 主消息头
cmsg struct cmsghdr* 当前控制消息

返回值

  • 若下一个控制消息存在 → 返回指针

  • 否则 → 返回 NULL

使用场景

当一个消息里包含多个控制信息:

cpp 复制代码
for (cmsg = CMSG_FIRSTHDR(&msg);
     cmsg != NULL;
     cmsg = CMSG_NXTHDR(&msg, cmsg))
{
    ...
}

例如一个消息同时传:

  • FD

  • Socket 超时设置

  • 权限信息

一般实际很少用。

sendmsgrecvmsg

cpp 复制代码
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

由于是socket系列的函数,flags也是类似的选择,例子见https://blog.csdn.net/ouliten/article/details/148208970?spm=1001.2014.3001.5502#t12

相关推荐
A小辣椒3 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒7 小时前
TShark:基础知识
linux
AlfredZhao9 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩2 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言