第 17 章 高级进程间通信

高级进程间通信

UNINX 域套接字

前面讨论了 UNIX 系统各种 IPC、管道、共享内存、套接字、消息队列,本章将会学习一种高级 IPC,UNIX 域套接字机制。

UNIX 域套接字是一种用于在同一台计算机上的进程之间进行通信的机制。它类似于网络套接字,但不依赖于网络协议栈,而是在操作系统的内核中直接传递数据。

UNIX 域套接字使用文件系统路径作为套接字地址,进程可以通过打开和读写文件来进行通信。这种通信方式比使用网络套接字更高效,因为数据不需要通过网络协议栈进行封装和解封装。

UNIX 域套接字通常用于同一台计算机上的进程间通信,例如在客户端和服务器之间进行本地通信。它提供了一种可靠的、高性能的通信机制,适用于需要高速数据传输和低延迟的应用程序。

socketpair 函数

socketpair 函数是用于创建一对相互连接的 UNIX 域套接字的函数。它的原型如下:

#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);

使用注意事项:

socketpair 为什么会返回两个 socket 文件描述符?

socketpair 函数返回两个套接字文件描述符,一个用于读取数据,一个用于写入数据。这是因为 socketpair 函数创建的是一对相互连接的套接字,可以实现双向通信。
其中, sv[0] 代表读取数据的套接字文件描述符, sv[1] 代表写入数据的套接字文件描述符。通过这两个文件描述符,可以在两个进程之间进行双向通信,实现数据的发送和接收。
需要注意的是,socketpair 函数创建的套接字对是全双工的,即可以同时进行读取和写入操作。因此,通过这两个套接字文件描述符,可以实现双向的数据传输。

从第 1 个 fd 写的数据从第 2 个 fd 读出
从第 2 个 fd 写的数据从第 1 个 fd 读出

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
int main() {
    int sv[2];
    char buf[256];
     if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {
        perror("socketpair");
        return 1;
    }
     if (fork() == 0) {
        // 子进程
        close(sv[0]); // 关闭子进程中不需要的套接字端口
         // 子进程向父进程发送消息
        char childMsg[] = "Hello from child";
        write(sv[1], childMsg, strlen(childMsg) + 1);
         // 子进程从父进程接收消息
        read(sv[1], buf, sizeof(buf));
        printf("Child received message: %s\n", buf);
         close(sv[1]);
    } else {
        // 父进程
        close(sv[1]); // 关闭父进程中不需要的套接字端口
         // 父进程从子进程接收消息
        read(sv[0], buf, sizeof(buf));
        printf("Parent received message: %s\n", buf);
         // 父进程向子进程发送消息
        char parentMsg[] = "Hello from parent";
        write(sv[0], parentMsg, strlen(parentMsg) + 1);
         close(sv[0]);
    }
    return 0;
}

但是文件描述符是在进程内有效得啊,不同得进程肯定要命名的,那么就会引出命名 UNIX 域套接字

命名 UNIX 域套接字

UNIX 域套接字的地址由 sockaddr_un 结构表示,一般定义在 sys/un.h

服务端样例,如果不进行 unlink 那么下次绑定同样的路径地址,则会出现 Failed to bind add,除非手动将路径的文件删除

Domain TCP

#include <iostream>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

using namespace std;

int main()
{
    // 创建UNIX域套接字
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        cerr << "Failed to create unix domain socket" << endl;
        return EXIT_FAILURE;
    }
    // 设置地址
    sockaddr_un domain_addr;
    domain_addr.sun_family = AF_UNIX;
    strncpy(domain_addr.sun_path, "/tmp/socket", sizeof(domain_addr.sun_path) - 1);
    // 绑定地址
    if (bind(sockfd, (struct sockaddr *)&domain_addr, sizeof(domain_addr)) == -1)
    {
        cerr << "Failed to bind addr" << endl;
        return EXIT_FAILURE;
    }
    // 监听连接
    if (listen(sockfd, 1) == -1)
    {
        cerr << "Failed to listen on socket" << endl;
        close(sockfd);
        return EXIT_FAILURE;
    }
    cout << "Waiting to listen on socket" << endl;
    // 接收连接
    sockaddr_un client_addr;
    socklen_t client_len = sizeof(client_addr);
    int client_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);
    if (client_sockfd == -1)
    {
        cerr << "Failed to accept client connection" << endl;
        close(sockfd);
        return EXIT_FAILURE;
    }
    cout << "Client connected" << endl;
    // 处理客户端请求
    char buffer[1024];
    int len = read(client_sockfd, buffer, sizeof(buffer) - 1);
    if (len > 0)
    {
        buffer[len] = 0;
        cout << buffer << endl;
    }
    // 关闭套接字
    close(client_sockfd);
    close(sockfd);
    // 删除套接字文件
    unlink("/tmp/socket");
    return 0;
}

客户端样例

#include <iostream>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
int main()
{
    // 创建UNIX域套接字
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd == -1)
    {
        std::cerr << "创建套接字失败" << std::endl;
        return 1;
    }
    // 设置服务器地址
    sockaddr_un server_addr;
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, "/tmp/socket", sizeof(server_addr.sun_path) - 1);
    // 连接服务器
    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
    {
        std::cerr << "连接服务器失败" << std::endl;
        close(sockfd);
        return 1;
    }
    std::cout << "已连接到服务器" << std::endl;
    // 向服务器发送数据
    const char *message = "Hello, server!";
    if (write(sockfd, message, strlen(message)) == -1)
    {
        std::cerr << "发送数据失败" << std::endl;
    }
    // 关闭套接字
    close(sockfd);
    return 0;
}

Domain UDP

如果需要两个进程双方都能收发,则要创建两个 UNIX 域 UDP 套接字,二者分别做 UDP 服务端与客户端

服务端样例

#include <iostream>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <cstring>
int main()
{
    // 创建UNIX域套接字
    int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (sockfd == -1)
    {
        std::cerr << "创建套接字失败" << std::endl;
        return 1;
    }
    // 设置服务器地址
    sockaddr_un server_addr;
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, "/tmp/socket", sizeof(server_addr.sun_path) - 1);
    // 绑定套接字
    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
    {
        std::cerr << "绑定套接字失败" << std::endl;
        close(sockfd);
        return 1;
    }
    std::cout << "等待接收数据..." << std::endl;
    while (true)
    {
        char buffer[1024];
        sockaddr_un client_addr;
        socklen_t client_len = sizeof(client_addr);
        // 接收数据
        ssize_t bytesRead = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, &client_len);
        if (bytesRead == -1)
        {
            std::cerr << "接收数据失败" << std::endl;
            close(sockfd);
            return 1;
        }
        std::cout << "接收到的数据:" << buffer << std::endl;
    }
    // 关闭套接字
    close(sockfd);
    unlink("/tmp/socket");
    return 0;
}

客户端样例

#include <iostream>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <cstring>
int main()
{
    // 创建UNIX域套接字
    int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (sockfd == -1)
    {
        std::cerr << "创建套接字失败" << std::endl;
        return 1;
    }
    // 设置服务器地址
    sockaddr_un server_addr;
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, "/tmp/socket", sizeof(server_addr.sun_path) - 1);
    // 发送数据
    const char *message = "Hello, server!";
    ssize_t bytesSent = sendto(sockfd, message, strlen(message), 0, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (bytesSent == -1)
    {
        std::cerr << "发送数据失败" << std::endl;
        close(sockfd);
        return 1;
    }
    // 关闭套接字
    close(sockfd);
    return 0;
}

传送文件描述符

每个进程有自己的进程表项,是一个数组,下标为 fd 文件描述符。值为文件指针地址,其指向文件表

//文件表
|--------------|
|  文件状态标志 |
|--------------|
|当前文件偏移量 |
|--------------|
|    v节点指针  |
|--------------|

v 节点表中存储 v 节点信息和 v_data,v_data 中能找到 inode 信息

虽然不同进程打开同一个文件时,文件表不同,但是其 v 节点是相同的。有没有方法实现不同进程共享同一个文件表

发送进程

#include <iostream>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <cstring>
#include <cstdlib>
#include <fcntl.h>
 int main() {
    // 创建UNIX域套接字
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd == -1) {
        std::cerr << "创建套接字失败" << std::endl;
        return 1;
    }
     // 设置服务器地址
    sockaddr_un server_addr;
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, "/tmp/socket", sizeof(server_addr.sun_path) - 1);
     // 连接服务器
    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        std::cerr << "连接服务器失败" << std::endl;
        close(sockfd);
        return 1;
    }
     // 打开要传输的文件
    int filefd = open("file.txt", O_RDONLY);
    if (filefd == -1) {
        std::cerr << "打开文件失败" << std::endl;
        close(sockfd);
        return 1;
    }
     // 构建消息头
    char buf[CMSG_SPACE(sizeof(filefd))];
    memset(buf, 0, sizeof(buf));
    struct iovec iov;
    iov.iov_base = (void*)"";
    iov.iov_len = 1;
     // 构建消息
    struct msghdr msg;
    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = buf;
    msg.msg_controllen = sizeof(buf);
     // 构建文件描述符消息
    struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_len = CMSG_LEN(sizeof(filefd));
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    memcpy(CMSG_DATA(cmsg), &filefd, sizeof(filefd));
     // 发送消息
    if (sendmsg(sockfd, &msg, 0) == -1) {
        std::cerr << "发送消息失败" << std::endl;
    }
     // 关闭套接字和文件描述符
    close(filefd);
    close(sockfd);
     return 0;
}

接收进程

#include <iostream>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <cstring>
#include <cstdlib>
#include <fcntl.h>
 int main() {
    // 创建UNIX域套接字
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd == -1) {
        std::cerr << "创建套接字失败" << std::endl;
        return 1;
    }
     // 设置服务器地址
    sockaddr_un server_addr;
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, "/tmp/socket", sizeof(server_addr.sun_path) - 1);
     // 绑定套接字
    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        std::cerr << "绑定套接字失败" << std::endl;
        close(sockfd);
        return 1;
    }
     // 监听连接
    if (listen(sockfd, 1) == -1) {
        std::cerr << "监听套接字失败" << std::endl;
        close(sockfd);
        return 1;
    }
     std::cout << "等待发送进程连接..." << std::endl;
     // 接受发送进程连接
    sockaddr_un client_addr;
    socklen_t client_len = sizeof(client_addr);
    int client_sockfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
    if (client_sockfd == -1) {
        std::cerr << "接受连接失败" << std::endl;
        close(sockfd);
        return 1;
    }
     // 构建消息头
    char buf[CMSG_SPACE(sizeof(int))];
    memset(buf, 0, sizeof(buf));
    struct iovec iov;
    iov.iov_base = (void*)"";
    iov.iov_len = 1;
     // 构建消息
    struct msghdr msg;
    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = buf;
    msg.msg_controllen = sizeof(buf);
     // 接收消息
    if (recvmsg(client_sockfd, &msg, 0) == -1) {
        std::cerr << "接收消息失败" << std::endl;
        close(client_sockfd);
        close(sockfd);
        return 1;
    }
     // 解析文件描述符
    struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg);
    int received_fd;
    memcpy(&received_fd, CMSG_DATA(cmsg), sizeof(int));
     // 使用接收到的文件描述符进行操作
    char buffer[1024];
    ssize_t bytesRead = read(received_fd, buffer, sizeof(buffer));
    if (bytesRead == -1) {
        std::cerr << "读取文件失败" << std::endl;
    } else {
        std::cout << "接收到的文件内容:" << std::endl;
        std::cout.write(buffer, bytesRead);
        std::cout << std::endl;
    }
     // 关闭套接字和文件描述符
    close(received_fd);
    close(client_sockfd);
    close(sockfd);
     return 0;
}

总结

如果你看过了 unix env 部分,收获会非常大,毕竟这些东西都是上层应用的基石,经常回顾学习新知识并复习思考,我想在写代码的路上会越走越远的