当前位置: 首页 > article >正文

【Linux系统】进程间通信:实现命名管道通信




在这里插入图片描述





认识命名管道通信

命名管道通信的结构图示:


在这里插入图片描述



图中的 ServerClient 是不同的进程, Server 负责发送数据, Client 则是接收数据,进程之间通过命名管道进行数据通信



准备工作:


创建以下文件

Server.hpp  #服务器类的头文件,包含服务器类的声明和各个函数的声明
Server.cc	#服务器类的实现文件,包含Server.hpp中声明的方法的具体实现代码。
Client.hpp	#客户端类的头文件,包含客户端类的声明和各个函数的声明
Client.cc	#客户端类的实现文件,包含Client.hpp中声明的方法的具体实现代码。
Makefile	#构建项目的Makefile文件,定义了编译规则、依赖关系、编译选项等信息,用于自动化编译过程。


Makefile


只能生成一个: 注意,我们需要形成两个可执行文件,而如果只是下面这样写,make 指令从上到下扫描只会形成一个可执行文件就停下了!

SERVER=server
CLIENT=client
CC=g++
SERVER_SRC=Server.cc
Client_SRC=Client.cc

$(SERVER):$(SERVER_SRC)
	$(CC) -o $@ $^ -std=c++11

$(CLIENT):$(Client_SRC)
	$(CC) -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f $(SERVER) $(CLIENT)


若想形成两个可执行文件,则需要加上下面这段:

.PHONY:all
all:$(SERVER) $(CLIENT)

原理:若想形成目标 all, 则需要获取到 $(SERVER) $(CLIENT),则需要执行后面两组操作形成两个可执行文件


SERVER=server
CLIENT=client
CC=g++
SERVER_SRC=Server.cc
Client_SRC=Client.cc

.PHONY:all
all:$(SERVER) $(CLIENT)
    
$(SERVER):$(SERVER_SRC)
	$(CC) -o $@ $^ -std=c++11

$(CLIENT):$(Client_SRC)
	$(CC) -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f $(SERVER) $(CLIENT)


命名管道通信理论


我们两个进程共享同一份文件资源:

这份公共资源:一般要让指定的一个进程先行创建,其他进程再获取该资源并共享使用

  • 创建&&使用
  • 获取&&使用

本次我们会让 Server 进程创建命名管道,Client 进程只需获取并使用该文件



实现命名管道通信


创建命名管道


代码创建管道:本质也是新建文件,使用函数 mkfifo 创建命名管道 FIFO 文件,其中的mode 为创建文件的权限

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


因为共享一个文件,因此我们可以将该共享文件资源的相关信息封装成一个头文件,其实就是共享区域



创建 Comm.hpp


加入该共享文件资源的相关信息:

  • 文件名路径
  • 创建该命名管道需要的 mode

意思是:

Server 进程创建命名管道,需要指定文件路径和文件名,如 ./myfifo ,表示在当前目录下创建一个命名管道名为 myfifo

Client 进程只需获取并使用该文件,也需要指定文件路径和文件名,如 ./myfifo

那么这两个进程都需要使用同一个文件名用于创建或打开该文件,我们可以将该 “同类” 资源放在 Comm.hpp 中,表示 Server 进程和 Client 进程共享这一份即可



创建 Server


(1)由 Server 创建共享命名管道文件

就直接写在构造函数里面:创建命名管道文件

因为命名管道通信的基础是需要一个命名管道,因此, Server 类创建的同时,就应该创建一个命名管道文件,因此可以放于该类的构造函数中

在这里插入图片描述



运行结果当然是成功创建文件

在这里插入图片描述


**优化一下:**既然需要给自定义 mode,则跟着也要将缺省权限:权限掩码清零:umask(0)



(2)由 Server 释放共享命名管道文件

既然 Server 创建该命名管道文件,则也需要由该进程释放该文件

删除该命名文件的程序写法:使用函数 unlink

在这里插入图片描述


删文件的本质:就是在指定目录下,将该文件名和 inode 的映射关系移除,根据 inode 将这个文件的硬链接数 -1,当硬链接数为零时自然就会被系统释放掉该文件所有资源

对于系统命令 rm

当你使用 rm 命令删除一个文件时,实际上是将该文件名从目录中移除,断开了文件名和 inode 之间的映射关系。

这个操作并不会立即删除文件的实际数据,而是减少了该文件的 inode 的硬链接数。


这就是为什么 unlink 也可以用于删除文件


将删除文件的操作放于 Server 的析构函数处:

~Server()
{
    int ret = ::unlink(gfileName.c_str());
    if(ret < 0) {
        std::cerr << "unlink failed!" << '\n';
    }
    std::cout << "unlink success" << '\n';
}



建立命名管道的通信,需要先创建一个命名管道,当通信结束后,需要删除该命名管道文件

这些关于命名管道通信的 ”环境配置“ 操作,可以额外封装成一个 Init

注:其实是否需要封装 Init 类 都可以,看自己选择,喜欢原来那样在 Server 类中创建和删除该文件也行



封装 Init


  • 封装到 Init :为了代码直观性,我们将创建和关闭管道的工作封装成一个 Init 类,而不是在 Server 类中
  • 封装 Init 类后,同时定义一个全局变量 Init init :该全局变量生命周期随程序,当程序执行时,文件自动创建,当程序结束时,文件自动销毁,这样更加容易理解
  • Server 还算作文件创建者:这个 Init 类是放到 Server.hpp 中的,意在还是让 Server 作为文件创建者

class Init
{
public:
    Init()
    {
        // 创建共享命名管道文件
        // int mkfifo(const char *pathname, mode_t mode);
        umask(0);
        int ret = ::mkfifo(gfileName.c_str(), gmode);
        if(ret < 0) {
            std::cerr << "mkfifo failed!" << '\n';
        }
        std::cout << "mkfifo is successfully created" << '\n';
        sleep(3);
    }
    ~Init()
    {
        int ret = ::unlink(gfileName.c_str());
        if(ret < 0) {
            std::cerr << "unlink failed!" << '\n';
        }
        std::cout << "unlink success" << '\n';
    }
};


Init init;  // 定义全局变量


Server 类的打开和关闭管道文件

Server 类 和 Client 类本质上就是要读写操作一个管道文件,因此就需要获取该管道文件的 fd,则将 fd 设置为类成员会方便很多

class Server
{
public:
    Server()
        : _fd(g_default_fd)
    {
    }
    ~Server()
    {
    }

    // 打开文件函数
    bool OpenPipe()
    {
        // int open(const char *pathname, int flags);
        _fd = ::open(gfileName.c_str(), O_RDONLY); // 只读方式打开:默认文件一定存在
        if (_fd < 0)
        {
            std::cerr << "open pipe filed !" << '\n';
            return false;
        }
        return true;
    }
    // 关闭文件函数
    void ClosePipe()
    {
        if (_fd > 0)
            ::close(_fd);
    }

private: 
    int _fd;
};


Server 类的接收数据函数

Server 需要从命名管道中读取由 Client 发送的数据

// 接收数据函数
// 编程规范
// std::string *: 输出型参数
// const std::string & : 输入型参数
// std::string & : 输入输出型参数
int RecvPipe(std::string *out)
{
    char buff[1024];
    // ssize_t read(int fd, void *buf, size_t count);
    ssize_t n = read(_fd, buff, sizeof(buff)-1); // -1目的:读字符串数据出来最后一位留作 '\0'
    if(n < 0) 
    {
        std::cerr << "read pipe filed !" << '\n';
        return -1;
    }
    else if(n > 0)
    {
        buff[n] = 0;
        *out = buff;
    }
    return n;
}


封装 Client

该类为客户端类,总体上和 Server 类差不多,唯一不同的是: Client 类是写数据的类,会有一个写数据的函数

#pragma once

#include "Comm.hpp"

class Client
{
public:
    Client()
        : _fd(g_default_fd)
    {
    }
    ~Client()
    {
    }

    // 打开文件
    bool OpenPipe()
    {
        // int open(const char *pathname, int flags);
        _fd = ::open(gfileName.c_str(), O_WRONLY); // 只写方式打开:作为写端
        if (_fd < 0)
        {
            std::cerr << "open pipe filed !" << '\n';
            return false;
        }
        return true;
    }
    // 关闭文件
    void ClosePipe()
    {
        if (_fd > 0)
            ::close(_fd);
    }

    // 发送数据
    // std::string & : 输入输出型参数
    int SendPipe(std::string &in)
    {
        //ssize_t write(int fd, const void *buf, size_t count);
        ssize_t n = write(_fd, in.c_str(), in.size());
        if(n < 0)
        {
            std::cerr << "write pipe filed !" << '\n';
            return -1;
        }
        return n;
    }

private: 
    int _fd;
};


初步通信


client.cc client 端循环写入

server.cc server 端循环读出



实现通信

client 端没有写入信息,则 server 端阻塞等待数据

在这里插入图片描述



代码演示效果如下:

注意, server 端是读端,负责读取数据与创建管道文件,因此需要先运行 server

然后再运行 client 端,否则 client 端打不开管道文件



解释一下这些程序的使用:client 进程,我们人为手动键盘输入一些字符,回车确认后,在 server 进程就会收到你输入的消息


在这里插入图片描述




关于为什么不能使用 cin

使用 cin >> message; 会遇到一个问题:当输入包含空格的字符串时,cin
只会读取第一个单词,而剩余的部分会留在输入流中。这会导致后续的循环中 cin 继续读取剩余的部分,从而导致 "Please Enter#: " 被连续打印多次。



上面的代码仅仅是一个基本轮廓框架,还有比较多的bug和细节需要填补:


填补细节和修 Bug


1、当我们关闭 client 端时,你会发现 server 端死循环了!!

这是因为,管道文件写端关闭,读端读到 n=0 本应该退出,但是我们的死循环逻辑中没有这个,因此在 server 端的代码中可以加上


while (true)
{
    cout << "Client Say#: ";
    int n = server.RecvPipe(&message);
    if (n > 0)
    {
        cout << message << '\n';
    }
    else 
    {
        cout << "读取结束!" << '\n';
        break;
    }
}
cout << "client quit, me too!" << '\n';

在这里插入图片描述



2、优化细节:封装 server 端 和 client 端重复的 OpenPipe()ClosePipe()

你可以发现 server 端 和 client 端都有一个 OpenPipe()ClosePipe() ,其中的代码大部分是重复的,我们可以将重复的代码抽取出来封装成共享的函数,然后两端调用即可,这样二次封装既减少代码重复,又可以增加可读性


实际的操作: 将重复部分抽取出来封装成 OpenPipe()ClosePipe() ,在 server 类 和 client 类中设计新的 ”专属“ 函数来调用这两个重复的 OpenPipe()ClosePipe() ,如在 serverOpenPipeForRead() 函数中调用 OpenPipe(gForRead)



server

这里的 gForReadopen 函数的选项,定义成了全局数据,后面有具体讲解

// 打开文件
bool OpenPipeForRead()
{
    _fd = OpenPipe(gForRead);
    if(_fd < 0) return false;
    return true;
}

// 关闭文件
void ClosePipeRead()
{
    ClosePipe(_fd);
}


client

这里的 gForWriteopen 函数的选项,定义成了全局数据,后面有具体讲解

// 打开文件
bool OpenPipeForWrite()
{
    _fd = OpenPipe(gForWrite);
    if(_fd < 0) return false;
    return true;
}

// 关闭文件
void ClosePipeWrited()
{
    ClosePipe(_fd);
}



公共代码部分:Comm.hpp

通过定义两种 flags,满足读写端的需求

const int gForRead = O_RDONLY;
const int gForWrite = O_WRONLY;

int OpenPipe(int flags)
{
    // int open(const char *pathname, int flags);
    int fd = ::open(gfileName.c_str(), flags);
    if (fd < 0)
    {
        std::cerr << "open pipe filed !" << '\n';
        return -1;
    }
    return fd;
}

// 关闭文件
void ClosePipe(int fd)
{
    if (fd > 0)
        ::close(fd);
}

管道通信,需要先存在读端,然后写端才能正常运行,若没有读端,则写端不会被打开

若写端打开了,读端关闭了或没打开(即不存在),则写端也会直接被释放!

若读端打开了,写端关闭了或没打开(即不存在),则读端会阻塞等待写端打开并写入数据

因此在管道通信中,需要确保读端优先被打开


3、一个超级细节的点:若读端打开文件时,写端还没打开,读端的 open 函数会阻塞住,等待写端打开文件

观察前面的代码,在管道通信中,我们需要先通过 Server 端读端,创建并打开文件,再通过 Client 端写端打开文件,才能进行读写通信。

对于管道通信,写端关闭,同时读端会读到 n=0 ,则读端关闭



此时就引出一个问题:

问题:在我们前面的代码中, Server 端读端打开文件,而 Client 端写端没有打开文件之前, Server 端的读端不会被系统关闭呢?

答:因为此时 Server 端的读端是阻塞住了!open 函数内部检测到该文件的引用计数为 1(即读端打开),此时不会将读端关闭,因为写端连文件都没打开,又何谈是读端没有读取到数据的问题,写端连文件都没打开,因此就不属于前面讲的管道那种场景(写端关闭&&读端会读到 n=0 则读端关闭)

此时,读端会在 open 函数出阻塞等待,等待第二个进程打开该管道文件,系统知道这个管道文件一定要两个文件打开才能进行通信,因此就需要等待



总结来说:

读端打开文件,写端未打开文件,引用计数 1,open 函数识别到此时文件状态是:只有一个进程操作该文件,则阻塞等待

读端打开文件,写端也打开文件,引用计数 2,open 函数识别到此时文件状态是:有两个进程操作该文件,当写端关闭时,引用计数从 2 变为 1,读端读到 n=0 就关闭管道与读端进程



验证一下:

我在 Server 端打开文件的 open 函数前后都加上一个打印标记

Server server;
cout << "Pos 1" << '\n';
server.OpenPipeForRead();
cout << "Pos 2" << '\n';

在这里插入图片描述


观察动图,你可以发现,当写端 Client 没有启动时, Server 端读端只会打印 Pos 1,而不会打印 Pos 2,说明 Server 端读端阻塞在 open 函数处,而不会继续往后执行

只有当 写端 Client 启动,打开文件后,才会继续打印 Pos 2



完整代码


Client.hpp

#pragma once

#include "Comm.hpp"

class Client
{
public:
    Client()
        : _fd(g_default_fd)
    {
    }
    ~Client()
    {
    }

    // 打开文件
    bool OpenPipeForWrite()
    {
        _fd = OpenPipe(gForWrite);
        if(_fd < 0) return false;
        return true;
    }

    // 关闭文件
    void ClosePipeWrited()
    {
        ClosePipe(_fd);
    }


    // 发送数据
    // std::string & : 输入输出型参数
    int SendPipe(std::string &in)
    {
        //ssize_t write(int fd, const void *buf, size_t count);
        ssize_t n = write(_fd, in.c_str(), in.size());
        if(n < 0)
        {
            std::cerr << "write pipe filed !" << '\n';
            return -1;
        }
        return n;
    }

private: 
    int _fd;
};


Client.cc

#include<iostream>
#include<cstdio>
#include "Client.hpp"
using namespace std;

int main()
{
    Client client;
    client.OpenPipeForWrite();

    string message;
    
    while(true)
    {
        cout << "Please Enter#: ";
        //cin >> message;
        //这里不建议使用cin, 当你要输入的命令有空格, cin 将字符串分成好几段,然后读取就有bug
        getline(std::cin, message);
        client.SendPipe(message);
    }

    client.ClosePipeWrited();
    return 0;
}


Server.hpp
#pragma once

#include "Comm.hpp"

class Init
{
public:
    Init()
    {
        // 创建共享命名管道文件
        // int mkfifo(const char *pathname, mode_t mode);
        umask(0);
        int ret = ::mkfifo(gfileName.c_str(), gmode);
        if (ret < 0)
        {
            std::cerr << "mkfifo failed!" << '\n';
        }
        std::cout << "mkfifo is successfully created" << '\n';
        sleep(3);
    }
    ~Init()
    {
        int ret = ::unlink(gfileName.c_str());
        if (ret < 0)
        {
            std::cerr << "unlink failed!" << '\n';
        }
        std::cout << "unlink success" << '\n';
    }
};

Init init; // 定义全局变量



class Server
{
public:
    Server()
        : _fd(g_default_fd)
    {
    }
    ~Server()
    {
    }

    // 打开文件
    bool OpenPipeForRead()
    {
        _fd = OpenPipe(gForRead);
        if(_fd < 0) return false;
        return true;
    }

    // 关闭文件
    void ClosePipeRead()
    {
        ClosePipe(_fd);
    }

    // 接收数据
    // 编程规范
    // std::string *: 输出型参数
    // const std::string & : 输入型参数
    // std::string & : 输入输出型参数
    int RecvPipe(std::string *out)
    {
        char buff[1024];
        // ssize_t read(int fd, void *buf, size_t count);
        ssize_t n = read(_fd, buff, sizeof(buff)-1); // -1目的:读字符串数据出来最后一位留作 '\0'
        if(n < 0) 
        {
            std::cerr << "read pipe filed !" << '\n';
            return -1;
        }
        else if(n > 0)
        {
            buff[n] = 0;
            *out = buff;
        }
        return n;
    }

private: 
    int _fd;
};



Server.cc
#include <iostream>
#include "Server.hpp"
using namespace std;

int main()
{
    Server server;
    server.OpenPipeForRead();
    
    string message;
    while (true)
    {
        cout << "Client Say#: ";
        int n = server.RecvPipe(&message);
        if (n > 0)
        {
            cout << message << '\n';
        }
        else 
        {
            cout << "读取结束!" << '\n';
            break;
        }
    }
    cout << "client quit, me too!" << '\n';
    server.ClosePipeRead();
    return 0;
}


Comm.hpp
#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string gfileName = "./myfifo";
const mode_t gmode = 0600;
const int g_default_fd = -1; // 默认的管道 fd
const int gForRead = O_RDONLY;
const int gForWrite = O_WRONLY;

int OpenPipe(int flags)
{
    // int open(const char *pathname, int flags);
    int fd = ::open(gfileName.c_str(), flags);
    if (fd < 0)
    {
        std::cerr << "open pipe filed !" << '\n';
        return -1;
    }
    return fd;
}

// 关闭文件
void ClosePipe(int fd)
{
    if (fd > 0)
        ::close(fd);
}



Makefile
SERVER=server 
CLIENT=client
CC=g++
SERVER_SRC=Server.cc 
CLIENT_SRC=Client.cc 

.PHONY:all
all:$(SERVER) $(CLIENT)


$(SERVER):$(SERVER_SRC)
	$(CC) -o $@ $^ -std=c++11
$(CLIENT):$(CLIENT_SRC)
	$(CC) -o $@ $^ -std=c++11

.PHONY:clean 
clean:
	rm -rf $(SERVER) $(CLIENT)



http://www.kler.cn/a/526164.html

相关文章:

  • 后端token校验流程
  • python3+TensorFlow 2.x(三)手写数字识别
  • pytorch线性回归模型预测房价例子
  • C++,STL,【目录篇】
  • 研发的立足之本到底是啥?
  • 36、【OS】【Nuttx】OSTest分析(2):环境变量测试
  • Linux:基础IO(一.C语言文件接口与系统调用、默认打开的文件流、详解文件描述符与dup2系统调用)
  • vector有用的自己不太熟悉的函数
  • 设计转换Apache Hive的HQL语句为Snowflake SQL语句的Python程序方法
  • 【Qt】各种常用的按钮(button)
  • 深入理解Linux内核的虚拟地址到物理地址转换机制及缓存优化
  • 6.工厂模式(Factory Method)
  • JVM_程序计数器的作用、特点、线程私有、本地方法的概述
  • java求职学习day22
  • WireShark4.4.2浏览器网络调试指南:数据统计(八)
  • LeetCode题练习与总结:区间加法 Ⅱ -- 598
  • 科研绘图系列:R语言绘制散点图(scatter plot)
  • Java 大视界 -- Java 大数据在量子通信安全中的应用探索(69)
  • E. Correct Placement
  • 单词翻转(信息学奥赛一本通1144)
  • SpringBoot 原理分析
  • 智慧园区管理系统为企业提供高效运作与风险控制的智能化解决方案
  • 园区管理智能化创新引领企业效能提升与风险控制新趋势
  • LabVIEW微位移平台位移控制系统
  • 【hot100】刷题记录(7)-除自身数组以外的乘积
  • 如何构建树状的思维棱镜认知框架