【Linux】单机版QQ之管道中的命名管道(上)

简介: 【Linux】单机版QQ之管道中的命名管道(上)

前言



命名管道是什么呢?

管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。

如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。

命名管道是一种特殊类型的文件。


一、命名管道



在学习命名管道前我们先看看在命令行创建命名管道的函数mkfifo:

c5cdbea7e3ef4ebd93d9e705909e847b.png


fifo的意思就是first in first out也就是先进先出的意思,比如我们直接在目录下创建一个命名管道文件:

fa9a958c7a624ab09a50f944bc306d18.png

在文件的权限部分的第一个P代表的是管道文件,下面我们讲讲命名管道的原理:

c9b447ce4809483e874f717cb6132c96.png

同样有两个进程,上面的是父进程下面是子进程,父进程的一个3号文件描述符表中记录一个文件的地址,这也是被父进程打开的文件,当我们创建一个子进程时,想让子进程打开和父进程打开的相同的那个文件,对于操作系统来说是不会重新再创建一个相同的文件的,操作系统会查询子进程要打开的文件是否已经被打开,如果找到这个被打开的文件就把这个文件的地址填入子进程的文件描述符表中,这样子进程就指向父进程打开的这个文件了,并且在文件中会有像ret这样的变量,当我们有文件描述符指向这个文件时这个变量就会++这也就是引用计数,关闭文件后就会--。那么如何保证两个毫不相关的进程看到的是同一个文件并打开呢?其实很简单,因为文件的唯一性是用路径表示的,只要让不同的进程通过文件路径+文件名看到同一个文件并打开,就是看到了同一个资源,这就具备了进程间通信的前提。


接下来我们用代码演示命名管道,首先需要创建两个文件client.cc写客户端代码,serve.cc写服务端代码,因为这次我们要实现两个可执行程序,所以我们需要在makefile中生成两个可执行程序,makefile代码如下图:

3737b738482b478a8a9387a9109eae53.png


.PHONY后面加上all,就是说我的目标文件是all,我们让all只有依赖关系没有依赖方法,这样就会去找server和client的依赖关系,就生成了两个可执行程序。下面我们正式编写代码,还记得我们刚开始讲的mkfifo函数吗?此函数的参数需要路径和选项(下面有C库中的mkfifo函数的说明),对于路径我们直接搞一个const string类型的字符串来保存路径,因为服务端和客户端需要打开同一份文件所以我们再创建一个公共的hpp头文件,把我们刚刚定义的路径放进去:

b2266821218540c081ecd939c41546f8.png

下面我们再看一下C库中的mkfifo函数说明:

6fd6261bfc7246d491af775d7ba503ab.png

我们可以看到此函数有两个参数,第一个参数是路径,第二个参数是mode,mode是什么呢?mode_t类型又是什么呢?实际上mode_t就是一种无符号整数,我们在讲文件的时候提到过,就是一种让你控制要创建的文件初始是什么权限,我们默认将权限给成0666。

66e5fd990fb54e0f88f39dc877426215.png下面我们编写服务端的代码,不知道大家还记不记得之前说过的,对于权限我们给的是0666但是经过权限掩码的影响会变成其他的,所以我们如果不想被权限掩码所影响就将默认的权限掩码设置为0。

3624a894ce54481eb96f9631137d8154.png

因为我们在创建管道文件的时候会有可能失败,所以我们用if语句判断一下,函数返回值如果等于0就是成功否则就是失败,失败我们就打印对应的错误然后返回1.下一步就是让服务端开启管道文件,开启后就可以正常通信了:

int main()
{
    //1.创建管道文件,我们今天只需要一次创建
    umask(0);   //这个设置并不影响系统的默认设置,只会影响当前进程
    int n = mkfifo(fifoname.c_str(),mode);
    if (n!=0)
    {
        std::cout<<errno<<" : "<<strerror(errno)<<std::endl;
        return 1;
    }
    std::cout<<"creat fifo file sucess"<<std::endl;
    // 2.让服务端直接开启管道文件
    int rfd = open(fifoname.c_str(),O_RDONLY);
    if (rfd<=0)
    {
        std::cout<<errno<<" : "<<strerror(errno)<<std::endl;
        return 2;
    }
    std::cout<<"open fifo success , begin ipc"<<std::endl;
    return 0;
}


开启管道文件很简单,就是打开我们创建的管道文件,这里只需要以只读方式打开就可以。同样要判断打开失败的情况,成功后我们就打印打开管道文件成功。下面我们实现开始正常通信的代码:

int main()
{
    //1.创建管道文件,我们今天只需要一次创建
    umask(0);   //这个设置并不影响系统的默认设置,只会影响当前进程
    int n = mkfifo(fifoname.c_str(),mode);
    if (n!=0)
    {
        std::cout<<errno<<" : "<<strerror(errno)<<std::endl;
        return 1;
    }
    std::cout<<"creat fifo file sucess"<<std::endl;
    // 2.让服务端直接开启管道文件
    int rfd = open(fifoname.c_str(),O_RDONLY);
    if (rfd<=0)
    {
        std::cout<<errno<<" : "<<strerror(errno)<<std::endl;
        return 2;
    }
    std::cout<<"open fifo success , begin ipc"<<std::endl;
    //3.正常通信
    char buffer[NUM];
    while (true)
    {
        buffer[0] = 0;
        ssize_t n = read(rfd,buffer,sizeof(buffer)-1);
        if (n>0)
        {
            buffer[n] = 0;
            std::cout<<"client# "<<buffer<<std::endl;
        }
        else if (n==0)
        {
            std::cout<<"client quit,me to"<<std::endl;
            break;
        }
        else 
        {
            std::cout<<errno<<" : "<<strerror(errno)<<std::endl;
            break;
        }
    }
    //关闭不要的fd
    close(rfd);
    return 0;
}


当我们正常通信的时候需要从缓冲区读数据所以先创建一个缓冲区,缓冲区大小设置为宏放在公共头文件中,因为我们把读到的数据当字符串看,所以在调用read函数的时候要让sizeof-1不要读\0,然后把缓冲区初始化一下对于C语言,直接在0位置放个\0就会认为是空字符串。然后我们判断函数返回值,如果已经读到数据结尾我们就在最后的位置放一个\0,因为我们打印字符串的时候是按照C语言的标准打印,而C语言字符串必须以\0结尾,因为服务端接收客户端发来的消息,所以在打印字符串前面加上客户端的名称。当返回值等于0说明客户端不在写东西了,客户端已经退出了,客户端都退出了就让服务端也退出,else就是读取失败,打印失败原因即可。通信结束后我们关闭管道文件即可。下面我们实现客户端:

#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include "comm.hpp"
#include <assert.h>
int main()
{
    //1.不需要创建管道文件,只需要打开对应的文件即可
    int wfd = open(fifoname.c_str(),O_WRONLY);
    if (wfd<0)
    {
        std::cerr<<errno<<":"<<strerror(errno)<<std::endl;
        return 1;
    }
    close(wfd);
    return 0;
}


客户端不需要创建管道文件,因为服务端已经创建了所以我们和服务端一样打开即可,打开后因为我们的客户端要写入消息所以以只写方式打开,当打开函数的返回值小于0直接打印报错信息,接下来我们实现通信方式:

int main()
{
    //1.不需要创建管道文件,只需要打开对应的文件即可
    int wfd = open(fifoname.c_str(),O_WRONLY);
    if (wfd<0)
    {
        std::cerr<<errno<<":"<<strerror(errno)<<std::endl;
        return 1;
    }
    //可以进行常规通信了
    char buffer[NUM];
    while (true)
    {
        std::cout<<"请输入你的消息# ";
        char* msg = fgets(buffer,sizeof(buffer),stdin);
        assert(msg);
        (void)msg;
        buffer[strlen(buffer)-1] = 0;
        if (strcasecmp(buffer,"quit")==0)
        {
            break;
        }
        ssize_t n = write(wfd,buffer,strlen(buffer));
        assert(n>=0);
        (void)n;
    }
    close(wfd);
    return 0;
}



目录
相关文章
|
2月前
|
NoSQL Linux Redis
linux安装单机版redis详细步骤,及python连接redis案例
这篇文章提供了在Linux系统中安装单机版Redis的详细步骤,并展示了如何配置Redis为systemctl启动,以及使用Python连接Redis进行数据操作的案例。
62 2
|
3月前
|
消息中间件 Linux 开发者
Linux进程间通信秘籍:管道、消息队列、信号量,一文让你彻底解锁!
【8月更文挑战第25天】本文概述了Linux系统中常用的五种进程间通信(IPC)模式:管道、消息队列、信号量、共享内存与套接字。通过示例代码展示了每种模式的应用场景。了解这些IPC机制及其特点有助于开发者根据具体需求选择合适的通信方式,促进多进程间的高效协作。
137 3
|
3月前
|
存储 Linux 数据处理
在Linux中,什么是管道操作,以及如何使用它?
在Linux中,什么是管道操作,以及如何使用它?
|
3月前
|
开发者 API Windows
从怀旧到革新:看WinForms如何在保持向后兼容性的前提下,借助.NET新平台的力量实现自我进化与应用现代化,让经典桌面应用焕发第二春——我们的WinForms应用转型之路深度剖析
【8月更文挑战第31天】在Windows桌面应用开发中,Windows Forms(WinForms)依然是许多开发者的首选。尽管.NET Framework已演进至.NET 5 及更高版本,WinForms 仍作为核心组件保留,支持现有代码库的同时引入新特性。开发者可将项目迁移至.NET Core,享受性能提升和跨平台能力。迁移时需注意API变更,确保应用平稳过渡。通过自定义样式或第三方控件库,还可增强视觉效果。结合.NET新功能,WinForms 应用不仅能延续既有投资,还能焕发新生。 示例代码展示了如何在.NET Core中创建包含按钮和标签的基本窗口,实现简单的用户交互。
66 0
|
3月前
|
消息中间件 Linux
Linux0.11 管道(十一)
Linux0.11 管道(十一)
25 0
|
3月前
|
数据挖掘 Linux 应用服务中间件
在Linux中,如何在Linux中使用管道?
在Linux中,如何在Linux中使用管道?
|
16天前
|
运维 安全 Linux
Linux中传输文件文件夹的10个scp命令
【10月更文挑战第18天】本文详细介绍了10种利用scp命令在Linux系统中进行文件传输的方法,涵盖基础文件传输、使用密钥认证、复制整个目录、从远程主机复制文件、同时传输多个文件和目录、保持文件权限、跨多台远程主机传输、指定端口及显示传输进度等场景,旨在帮助用户在不同情况下高效安全地完成文件传输任务。
112 5
|
15天前
|
Linux
Linux系统之expr命令的基本使用
【10月更文挑战第18天】Linux系统之expr命令的基本使用
51 4
|
2天前
|
缓存 监控 Linux