道家人 发表于 2024-8-20 08:03:40

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

还记得上一篇的匿名管道吗?

   文章目次



[*]前言
[*]一、命名管道
[*]总结

前言

命名管道是什么呢?
   管道应用的一个限定就是只能在具有共同先人(具有亲缘关系)的历程间通信。     如果我们想在不相干的历程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。     命名管道是一种特别类型的文件。
一、命名管道

在学习命名管道前我们先看看在下令行创建命名管道的函数mkfifo:
https://i-blog.csdnimg.cn/blog_migrate/23a16d4aa03db8f915de63529e79ac46.png
 fifo的意思就是first in first out也就是先进先出的意思,比如我们直接在目次下创建一个命名管道文件:
https://i-blog.csdnimg.cn/blog_migrate/cbbfb5df6e4ee9c49b9aa4fad01c9c84.png
在文件的权限部分的第一个P代表的是管道文件,下面我们讲讲命名管道的原理:
https://i-blog.csdnimg.cn/blog_migrate/2a9e8bcdd65286ab565f4a44d59a442a.png同样有两个历程,上面的是父历程下面是子历程,父历程的一个3号文件描述符表中记载一个文件的地址,这也是被父历程打开的文件,当我们创建一个子历程时,想让子历程打开和父历程打开的相同的那个文件,对于操作体系来说是不会重新再创建一个相同的文件的,操作体系会查询子历程要打开的文件是否已经被打开,如果找到这个被打开的文件就把这个文件的地址填入子历程的文件描述符表中,这样子历程就指向父历程打开的这个文件了,并且在文件中会有像ret这样的变量,当我们有文件描述符指向这个文件时这个变量就会++这也就是引用计数,关闭文件后就会--。那么如何保证两个绝不相干的历程看到的是同一个文件并打开呢?实在很简朴,因为文件的唯一性是用路径体现的,只要让不同的历程通过文件路径+文件名看到同一个文件并打开,就是看到了同一个资源,这就具备了历程间通信的条件。
 接下来我们用代码演示命名管道,起首必要创建两个文件client.cc写客户端代码,serve.cc写服务端代码,因为这次我们要实现两个可执行步伐,以是我们必要在makefile中天生两个可执行步伐,makefile代码如下图:
https://i-blog.csdnimg.cn/blog_migrate/44267ca7044db8471aa43577300ccc74.png
 .PHONY后面加上all,就是说我的目标文件是all,我们让all只有依靠关系没有依靠方法,这样就会去找server和client的依靠关系,就天生了两个可执行步伐。下面我们正式编写代码,还记得我们刚开始讲的mkfifo函数吗?此函数的参数必要路径和选项(下面有C库中的mkfifo函数的分析),对于路径我们直接搞一个const string类型的字符串来保存路径,因为服务端和客户端必要打开同一份文件以是我们再创建一个公共的hpp头文件,把我们刚刚界说的路径放进去:
https://i-blog.csdnimg.cn/blog_migrate/66d048bea64d5dbaa088b0f9d0263b82.png
下面我们再看一下C库中的mkfifo函数分析:
https://i-blog.csdnimg.cn/blog_migrate/b198251779bbc7fef94e1f9b482ef13a.png
 我们可以看到此函数有两个参数,第一个参数是路径,第二个参数是mode,mode是什么呢?mode_t类型又是什么呢?现实上mode_t就是一种无符号整数,我们在讲文件的时间提到过,就是一种让你控制要创建的文件初始是什么权限,我们默认将权限给成0666。
https://i-blog.csdnimg.cn/blog_migrate/afaaca4ed16c6c6ff098df8752f02ff0.png
下面我们编写服务端的代码,不知道大家还记不记得之前说过的,对于权限我们给的是0666但是经过权限掩码的影响会酿成其他的,以是我们如果不想被权限掩码所影响就将默认的权限掩码设置为0。
https://i-blog.csdnimg.cn/blog_migrate/5d32533ba49eae27495b13d98a133f5e.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;
    while (true)
    {
      buffer = 0;
      ssize_t n = read(rfd,buffer,sizeof(buffer)-1);
      if (n>0)
      {
            buffer = 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;
    while (true)
    {
      std::cout<<"请输入你的消息# ";
      char* msg = fgets(buffer,sizeof(buffer),stdin);
      assert(msg);
      (void)msg;
      buffer = 0;
      if (strcasecmp(buffer,"quit")==0)
      {
            break;
      }
      ssize_t n = write(wfd,buffer,strlen(buffer));
      assert(n>=0);
      (void)n;
    }
    close(wfd);
    return 0;
}  同样我们要先创建一个缓冲区,然后直接循环写入消息,然后将写入的消息放到我们的缓冲区中,用一个指针接收客户输入的字符串,如果成功了这个指针保存的就是字符串的起始地址,我们在接收字符串的时间并不必要考虑\0,因为fgets这是C语言函数会自动加上\0的,断言一下空指针,然后将指针强转是为了防止出现在release版本变量被界说但是却没有使用的情况。buffer(len-1)是什么意思呢?这里实在是因为我们客户端输入字符串后会按一下回车,而回车也会被放入缓冲区中,这样的话在打印的时间服务端会多一行空白,以是我们把回车删掉就没有标题了。接下来我们让客户端输入quit的时间就退出步伐,因为我们在服务端设置的是只要客户端退出服务端也就跟着退出了。然后我们向文件里写数据,把我们缓冲区的数据写到文件中,在这里同样不消考虑\0,因为只有C语言规定字符串后面必须加\0,而write是体系接口不会考虑\0的。做完这一步后我们断言一下函数返回值,不让返回值大于等于0的意思是如果是空串大概错误就不去写入了。然后同样强转一下刚刚的返回值n。最后我们将文件关闭即可,下面我们就试试可以运行吗:
运行的时间我们必须开两个窗口,当创建好两个可执行步伐后,我们先运行服务端步伐,这个时间步伐会卡着不动,因为服务端要等待客户端打开同一个文件以是我们在运行客户端:
https://i-blog.csdnimg.cn/blog_migrate/0d6e803eaa6d0c645ddc99d4ddd849e3.png
https://i-blog.csdnimg.cn/blog_migrate/42b70ac5df4ca9701672215e8e8dc0d9.png 这样我们就完成了命名管道的通信,这里另有一个标题就是当我们运行一次步伐后就有了管道文件下一次运行步伐会出现文件已经存在的报错,以是我们可以直接在服务端关闭文件后取消链接这个文件:
https://i-blog.csdnimg.cn/blog_migrate/8e2e666fa3800500fdaf6e723dda07ba.png
 这样的话我们每次运行步伐就不消先将管道文件删除再运行了。
下面我们将这个命名管道改为用户每输入一个字符,服务端就相应的输出一个字符,大家可以理解为就像我们将手机投屏到电脑上手机上做什么操作电脑屏幕就是什么操作。
因为我们前面的实现每次必要输入回车服务端才会显示消息,现在我们必要不输入回车就写入内容的函数,这里我们用的getch函数,由于getch必要用到ncurses库,以是我们先安装这个库:
(按照ncurses库这个方法我们没有搞定,如果有搞定的小同伴可以私信我哦~成功的方法在下面我用红字提醒出来了可直接找到后用体系方法)
https://i-blog.csdnimg.cn/blog_migrate/1785a53076580032067570876638d87d.png
 如果大家是平凡用户的话前面记得加上sudo提权。安装好后我们在刚刚的代码中加上这个库的头文件:
https://i-blog.csdnimg.cn/blog_migrate/c4cb99e6c04b71f946443c40d5fadb27.png
 因为我们是在客户端完成这个操作以是只必要在client.cc文件中包上这个头文件即可。然后我们修改一下client.cc文件中的代码:
https://i-blog.csdnimg.cn/blog_migrate/bdec0d119a69ae097fb0f74146770479.png
 考虑到大家大概不熟悉getch这个函数,下面我们把这个函数的文档找出来:
https://i-blog.csdnimg.cn/blog_migrate/e34e5ea55a0b8a7737072c74797c1440.png
https://i-blog.csdnimg.cn/blog_migrate/3becdcc5722b8da0544d96725488112a.png
 修改完成后下面我们把代码运行起来:
https://i-blog.csdnimg.cn/blog_migrate/b5e43f926645bc4b5d98bc17023096cd.png
 由于编译不通过是因为我们没有引入client的库,以是我们引入一下:
https://i-blog.csdnimg.cn/blog_migrate/da80b6a6ebcae9664ee015b903de86f7.png
https://i-blog.csdnimg.cn/blog_migrate/dc3abb872ede5bf11247ef0e72f7d46d.png
 引入后我们重新天生一下可执行文件:
https://i-blog.csdnimg.cn/blog_migrate/00c48862cd8413509fe870b0a528a638.png
https://i-blog.csdnimg.cn/blog_migrate/231bf0bf4cb0a92b755bf8f03c560cc8.png
 运行后我们发现服务端不能正常打印了,实在原因在于函数的返回值标题,我们刚刚看文档人家的返回值是int类型,结果我们用char类型接收了,以是就出错了,下面我们修改一下代码:
由于我们不知道返回值是什么,以是我们先在代码中直接打印一下返回值:
https://i-blog.csdnimg.cn/blog_migrate/a91f0ec127e48e83d83b3308004cc740.png
 通过打印我们发现返回值为-1:
https://i-blog.csdnimg.cn/blog_migrate/31c774f4143fb8b2e8f65c17cb7081f3.png
https://i-blog.csdnimg.cn/blog_migrate/f7b921fa9cac7888ae1130eb6445981a.png
 然后我们将代码修改为当返回值为-1我们就继承读取字符,但是在运行的过程中由于此方法有一些错误导致还是不能成功,以是我们下面直接用体系方法不消库方法:
https://i-blog.csdnimg.cn/blog_migrate/8e934e36d0d8b8c521712c79dd94b895.png
 然后我们在服务端接收的时间不打印前面的客户端名称了,直接就是接收什么打印什么(在这里记得刷新缓冲区):
https://i-blog.csdnimg.cn/blog_migrate/9466ee93756f3d06f27eb0c69849bfe7.png
 然后下面我们运行起来:
https://i-blog.csdnimg.cn/blog_migrate/813b95c96bed06c243cbd438bea1af03.png
 这样我们就实现了一开始的功能,通过以上管道的学习相信大家能明白如何创建命名管道以及使用,上面我们使用的体系方法也是从网络上搜的,如果有什么错误还请见谅。
   匿名管道与命名管道的区别:      匿名管道由   pipe   函数创建并打开。      命名管道由   mkfififo   函数创建,打开用   open      FIFO   (命名管道)与   pipe   (匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。   
命名管道的打开规则:
   如果当前打开操作是为读而打开FIFO时   O_NONBLOCK disable:阻塞直到有相应历程为写而打开该FIFO   O_NONBLOCK enable:立刻返回成功   如果当前打开操作是为写而打开FIFO时   O_NONBLOCK disable:阻塞直到有相应历程为读而打开该FIFO   O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
总结

命名管道可用于同一主机上的任意历程间通信,并且管道通信的本质是通过内核中一块缓冲区(内存)时间数据传输,而命名管道的管道文件只是一个标识符,用于让多个历程可以或许访问同一块缓冲区,并且管道是半双工通信,是可以选择方向的单向通信。命名管道和匿名有一个相同点,就是他们的本质都是内核中的一块缓冲区。同时增补一点,管道的生命周期是随历程的,本质是内核中的缓冲区,命名管道文件只是标识,用于让多个历程找到同一块缓冲区,删除后,之前已经打开管道的历程依然可以通信。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【Linux】单机版QQ之管道中的命名管道