命名管道原理

linux进程间通信——学习与应用命名管道, 日志程序的使用与实现-LMLPHP

        但是, 我们如果向这个管道之中写入数据, 就会阻塞住:

linux进程间通信——学习与应用命名管道, 日志程序的使用与实现-LMLPHP

        所以呢,我们如果再打开一个终端, 在我们的管道中去读取, 就会看到读取到了内容, 并且不会再被阻塞住了!!! mkfifo管道文件不会保存数据, 他只相当于一个中转层。 

linux进程间通信——学习与应用命名管道, 日志程序的使用与实现-LMLPHP

        所以, 我们的两个不同进程打开同一个文件本质上也是这一张图:

linux进程间通信——学习与应用命名管道, 日志程序的使用与实现-LMLPHP

如何理解mkfifo

        那么, 我们怎么保证, 我们打开的是同一个文件呢? 以及, 我们为什么要打开同一个文件呢? ——看到同一个路径下面的同一个文件名 = 路径 + 文件名(路径 + 文件名具有唯一性!!!) ——也就是说, 命名管道, 是利用使用路径 + 文件名的方案, 让不同的进程, 看到同一份文件资源, 进而实现不同文件之间的通信的!!!

        那么问题来了, 命名管道能不能也设计成我们之前写的进程池的样子呢?——可以的!只是不需要创建子进程,直接使用mkfifo创建就好了!!!

        命名管道和普通管道是一样的——都会面向字节流, 进程同步与互斥, 生命周期随进程, 当使用时需要打开一个写端, 一个读端, 单向通信等等。 不同点就是命名管道可以作用于没有血缘关系的进程之间进行通信。 

日志

        对于日志, 博主也不太熟悉, 也没有查过相关资料。 这里只是谈一些本篇内容需要用到的东西:

        日志包括: 日志的内容包括日志的时间日志的等级日志的内容文件的名称行号等。

        其中, 日志时间, 日志等级, 日志内容一般的日志都会有。 并且日志的等级一般有以下几个等级:

创建文件

makefile

Com.hpp头文件准备

        以下是整个Com.hpp的文件代码

#pragma once
#include <iostream>
using namespace std;
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include<unistd.h>

#define FIFO_FILE "./myfifo"
#define MODE 0660

enum
{
    FIFO_CREAT_ERR = 1,
    FIFO_DELE_ERR = 2,
    FIFO_OPEN_ERR = 3
};

class Init
{
public:
    Init()
    {
        int n = mkfifo(FIFO_FILE, MODE);
        if (n == -1) 
        {
            perror("mkfifo");
            exit(FIFO_CREAT_ERR);
        }

    }

    ~Init()
    {
        int m = unlink(FIFO_FILE);
        if (m == -1)
        {
            perror("unlink");
            exit(FIFO_DELE_ERR);
        }
    }

};

日志文件的准备

         日志文件是本节很重要的一块。 我们需要注意:

#define部分

        先定义好日志等级

#define Info 0
#define Debug 1
#define Waring 2
#define Error 3
#define Fatal 4

        定义打印方式——分为在显示器上打印, 在一个文件里面打印,在多个文件里面打印:

#define Info 0
#define Debug 1
#define Waring 2
#define Error 3
#define Fatal 4

        定义打印到目标文件的文件名

#define LogFile "log.txt"

        我们打印日志文件到管道里面, 所以一定要有日志文件的缓冲区, 所以这里要定义一个缓冲区的最大大小:

#define SIZE 1024

定义类 

LevelToString 

operator()

printLog

printOneFile

        向一个文件里面写就是很标准的文件写入, 先打开文件获取fd, 然后判断文件是否打开成功。 如果成功就像里面写入数据。最后关闭文件。 ——值得一提的是, 这里的logname是打印到的目标文件的文件名。 我们在前面加上path(这个path是./log/)是为了创建一个文件夹, 以后打印数据都向当前目录的log目录下创建文件进行打印数据。


    void printOneFile(const string& logname, const string& logtxt)
    {
        string _logname = path + logname;   //打印到的文件路径
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0660);
        if (fd < 0) return;
        write(fd, logtxt.c_str(), logtxt.size()); //文件描述符,文件源, 文件大小
        close(fd);
    }

printClassFile

        打印到多个文件夹我们就可以根据level转换成字符串的不同, 创建出不同的文件名。 再将数据打印到这些文件名的文件夹中:

    void printClassFile(int level, const string& logtxt)
    {
        //创建文件夹的文件名
        string filename = LogFile;
        filename += ".";
        filename += LevelToString(level);

        printOneFile(filename, logtxt);  //然后去这个文件里打印数据
    }

Enable

        最后我们再加一个用来修改打印方式的函数, 日志系统就完成了

    void Enable(int method)
    {
        printStyle = method;
    }

下面是真个日志系统全部代码
全部代码:

#pragma once
#include<iostream>
using namespace std;
#include<stdarg.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

#define SIZE 1024

#define Info 0
#define Debug 1
#define Waring 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        printStyle = Screen;    //默认打印到屏幕上
        path = "./log/";        //默认的路径是./log/
    }

    void Enable(int method)
    {
        printStyle = method;
    }

    void printOneFile(const string& logname, const string& logtxt)
    {
        string _logname = path + logname;   //打印到的文件路径
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0660);
        if (fd < 0) return;
        write(fd, logtxt.c_str(), logtxt.size()); //文件描述符,文件源, 文件大小
        close(fd);
    }

    void printClassFile(int level, const string& logtxt)
    {
        //创建文件夹的文件名
        string filename = LogFile;
        filename += ".";
        filename += LevelToString(level);

        printOneFile(filename, logtxt);  //然后去这个文件里打印数据
    }


    void printLog(int level, const string logtxt)
    {
        switch(printStyle)
        {
            case Screen:
                cout << logtxt << endl;
                break;
            case Onefile:
                printOneFile(LogFile, logtxt); //向文件里面打, 这个文件名定义的是LogFile            
                break;
            case Classfile:
                printClassFile(level, logtxt);
                break;
            default:
                break;
        }
    }


    void operator()(int level, const char* format, ...)
    {
        //获取时间
        time_t t = time(nullptr); 
        struct tm* ctime = localtime(&t); 
        char leftbuffer[SIZE];   

        snprintf(leftbuffer, sizeof(leftbuffer), 
        "[%s][%d-%d-%d-%d-%d-%d]", LevelToString(level).c_str(), ctime->tm_year
        ,ctime->tm_mon, ctime->tm_mday, ctime->tm_hour, ctime->tm_min
        , ctime->tm_sec);    //向缓冲区中打印数据

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];

        vsnprintf(rightbuffer, sizeof(rightbuffer), format,s); //
        va_end(s);

        char logtxt[SIZE * 2]; //文本缓冲区, 用来将两个文本文件合起来;
        snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer
        , rightbuffer);

        //将要打印的数据放到缓冲区里面后就可以打印数据了
        printLog(level, logtxt);
    }

    string LevelToString(int level)
    {
        switch(level)
        {
            case Info:
                return "Info";
            case Debug:
                return "Debug";
            case Waring:
                return "Waring";
            case Error:
                return "Error";
            default:
                return "None";
        }
    }


public:
    int printStyle; //打印风格
    string path; //打印到那个路径下面
};

Sever端

接下来准备我们的Sever端, 这里我们写日志, 我们是向不同的文件中打印(因为测试向不同的文件中打印成功的话, 向显示器, 向单个文件中打印一定可以成功)

#include"Com.hpp"
#include"Log.hpp"

int main()
{
    Init init; //创建管道和删除管道
    Log log;   //日志系统
    log.Enable(Classfile);  //向不同的文件中打印

    return 0;
}

        然后我们打开信道——这个打开信道和创建管道是不同的, 创建管道只是将我们的命名管道文件给创建出来。 而我们的打开信道就是打开这个管道文件, 并且我们现在写的是服务端, 所以打开管道的方式是只读, 也就是O_RDONLY;然后报错信息等等都通过日志系统的()重载打印到日志系统之中。

#include"Com.hpp"
#include"Log.hpp"

int main()
{
    Init init; //创建管道和删除管道
    Log log;   //日志系统
    log.Enable(Classfile);  //向不同的文件中打印

//打开信道
    int fd = open(FIFO_FILE, O_RDONLY); //Sever端是读取
    if (fd < 0)
    {
        log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
        exit(FIFO_OPEN_ERR);
    }



//开始通信
    log(Info, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
    log(Waring, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
    log(Error, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
    while (true)
    {
        char buffer[1024];
        int x = read(fd, buffer, sizeof(buffer));
        if (x > 0)
        {
            buffer[x] = 0;
            cout << "client say#: " << buffer << endl;
        } 
        else if (x == 0) 
        {
            log(Fatal, "client quit, me too!!! error string: %s, error code: %d", strerror(errno), errno);
            break;
        }
        else break;
    }
    close(fd);

    return 0;
}

Client端

我们的客户端同样是打开管道文件, 同时以写的方式打开。 然后我们创建缓冲区,将数据写到缓冲区中, 再从缓冲区通过write函数写到文件里面

#include"Com.hpp"
#include"Log.hpp"

int main()
{
    int fd = open(FIFO_FILE, O_WRONLY);
    if (fd < 0) 
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }

    //
    cout << "client open file done" << endl;//成功打开客户端
    
    string line; //创建缓冲区
    
    while (true)
    {
        cout << "Please Entering#: ";
        getline(cin, line); //向缓冲去中写入数据

        write(fd, line.c_str(), line.size());
    }

    close(fd);
    return 0;
}

运行测试

        写完所有的代码之后我们就可以运行测试一下了。 首先我们打开两个终端:

        先在目录下面创建一个log文件, 用来给日志系统打印数据。

linux进程间通信——学习与应用命名管道, 日志程序的使用与实现-LMLPHP

        然后运行先运行我们的服务端, 再运行客户端——一定先运行服务端。 因为如果先运行客户端, 我们的客户端打开的管道文件和我们的服务端打开的管道文件就不是同一个管道文件——打开后如下:

linux进程间通信——学习与应用命名管道, 日志程序的使用与实现-LMLPHP

然后我们每向客户端输入一条数据, 服务端就会打印一条数据

linux进程间通信——学习与应用命名管道, 日志程序的使用与实现-LMLPHP

并且我们在客户端ctrl + c后,服务端读取到0, 那么久else if判断, 程序就退出了!

然后我们打开我们的日志文件, 就写入了许多数据: 

linux进程间通信——学习与应用命名管道, 日志程序的使用与实现-LMLPHP

——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!

09-16 11:39