《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

相关推荐
_不会dp不改名_2 小时前
HCIP笔记8--中间系统到中间系统协议1
网络·笔记·hcip
伯明翰java2 小时前
Redis学习笔记-Set集合(2)
redis·笔记·学习
jennychary12 小时前
网工学习笔记:loopback 和route id
网络·笔记·学习
人工智能训练2 小时前
openEuler系统中home文件夹下huawei、HwHiAiUser、lost+found 文件夹的区别和作用
linux·运维·服务器·人工智能·windows·华为·openeuler
YJlio2 小时前
Active Directory 工具学习笔记(10.2):AdExplorer 实战(二)— 对象 / 属性 / 搜索 / 快照
java·笔记·学习
米花町的小侦探2 小时前
Ubuntu安装多版本golang
linux·ubuntu·golang
casdfxx2 小时前
v3s点不亮framebuffer st7735r,之reset被拉低。
linux·运维·服务器
孙同学_2 小时前
【Linux篇】线程深度解析:概念、原理与实践
linux
摇滚侠2 小时前
ElasticSearch 教程入门到精通,条件分页排序查询,多条件范围查询,完全匹配高亮查询,聚合查询,映射关系,笔记13、14、15、16、17
大数据·笔记·elasticsearch