目录
前言:
进程间要通信,如果是有血缘关系的进程的话可以使用匿名管道来进行进程间通信,但是如果是两个毫不相干的进程的话显然用匿名管道是不行的,这就需要使用我们接下来讲的命名管道了
一、什么是命名管道:
命名管道(Named Pipe),也称为 FIFO(First In, First Out),它允许无关的进程通过一个特殊的文件进行通信,就像它们通过普通文件交换数据一样。
命名管道是一种特殊的文件类型,它在文件系统中有一个路径名,但不像普通文件那样存储数据。相反,它充当一个通道,允许一个进程向其中写入数据,而另一个进程从中读取数据。
特点:
1、基于文件系统:命名管道通过文件路径访问,因此无关的进程可以通过路径名找到它。
2、面向字节流:数据以字节流的形式传输,没有消息边界。
3、阻塞行为:默认情况下,读写操作是阻塞的。如果没有数据可读,读取操作会等待;如果管道已满,写入操作也会等待。
4、命名管道和匿名管道都是内存级文件的,二者都不会将数据刷新到磁盘中,所以命名管道的大小永远为0
通过指令创建命名管道:
在命令行中,我们用指令mkfifo可以进行创建命名管道

并且可以看到最开始是以p开头的,代表着命名管道,那么在命令行中是怎么用的呢?
我们向这个文件尝试写入:

如上,我们通过echo向myfifo这个文件中写入,发现此时发生阻塞了
为什么呢?----- 这是因为这个文件是一个特殊的文件,需要作用于是两个进程进行通信的,那么我们在右边从myfifo中读呢

这个时候左边也不会阻塞了,右边也能够读到左边的消息
所以,我们了解到:myfifo是不会保存数据的,这个特殊的文件就相当于一个管道
命名管道利用路径 + 文件名 的方案,让不同的进程看到同一份文件资源,从而实现进程间通信,由于路径 + 文件名具有唯一性,因此同一个路径下的同一个文件名可以确保不同进程访问的是同一个命名管道文件
二、在代码中的命名管道:
在代码中命名管道依然是使用mkfifo,
头文件:
#include <sys/types.h>
#include <sys/stat.h>
函数原型:
int mkfifo(const char *pathname, mode_t mode);
pathname:
这是指要创建的FIFO命名管道文件的路径名
mode:
这是指指定FIFO文件的权限模式(类似于普通文件的权限)
通常使用八进制表示,例如 0664
实际权限会受到umask的影响,最终权限为mode & ~umask
返回值:
创建成功时返回0
创建失败时返回-1
如下是一个简单的命名管道的创建
cpp
#define FIFO_FILE "./myfifo"
#define MODE 0664
int main()
{
//创建管道
int n = mkfifo(FIFO_FILE,MODE);
if(n == -1)
{
perror("creat fail");
exit(FIFO_CREAT_ERR);
}
return 0;
}
和文件一样,既然开始打开了,那么在最后也最好将这个文件删除,这里使用unlink接口对其删除
cpp
//删除管道
int m = unlink(FIFO_FILE);
if(m == -1)
{
perror("delete fail");
exit(FIFO_DELETE_ERR);
}
那么,此时我们就已经能够将一个命名管道在代码中实现创建,删除操作,那么如何让两个毫不相干的进程通信起来呢?
用命名管道实现serve&client通信
这里我们创建三个文件和Makefile

其中,我们让client文件作为客服端,让server文件作为服务端,让这两个进程通信起来
首先来看看Makefile:
bash
.PHONY:all
all:server client
server:server.cpp
g++ -o $@ $^ -g -std=c++11
client:client.cpp
g++ -o $@ $^ -g -std=c++11
.PHONY:clean
clean:
rm -f client server
要生成两个进程,采用伪目标的形式来将其进行创建
接着在connect.hpp文件中引入头文件,定义宏,和错误码信息,这里错误码信息采用enum联合体,令创建错误为1,删除错误为2,打开错误为3
cpp
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#define FIFO_FILE "./myfifo"
#define MODE 0664
#define NUM 1024
enum{
FIFO_CREAT_ERR = 1,
FIFO_DELETE_ERR,
FIFO_OPEN_ERR
};
然后我们让我们的服务端进行管理(创建和删除)命名管道,
当这个命名管道创建出来后,就可以以常规的方式进行Open打开,close关闭
就可以正常进行通信了,这里我们让服务端进行读数据,客户端进行发送数据,因为我们已经在服务端进行命名管道的创建了,所以在客户端就只需进行打开,通信,关闭即可
服务端(server)代码:
cpp
#include "connect.hpp"
using namespace std;
int main()
{
//创建管道
int n = mkfifo(FIFO_FILE,MODE);
if(n == -1)
{
perror("creat fail");
exit(FIFO_CREAT_ERR);
}
//打开管道
int fd = open(FIFO_FILE,O_RDONLY);
if(fd < 0)
{
perror("open fail");
exit(FIFO_OPEN_ERR);
}
//进行通信
while(1)
{
char buffer[NUM] = {0};
int x = read(fd,buffer,sizeof(buffer));
if(x > 0)
{
buffer[x] = 0;
cout<<"client send to server@"<<buffer<<endl;
}
else if(x == 0)
{
cout<<"client quit, me too!"<<endl;
break;
}
else break;
}
//关闭管道
close(fd);
//删除管道
int m = unlink(FIFO_FILE);
if(m == -1)
{
perror("delete fail");
exit(FIFO_DELETE_ERR);
}
return 0;
}
客户端(client)代码:
cpp
#include"connect.hpp"
using namespace std;
int main()
{
//打开管道
int fd = open(FIFO_FILE,O_WRONLY);
if(fd < 0)
{
perror("open fail");
exit(FIFO_OPEN_ERR);
}
//进行通信
string line;
while(1)
{
cout<<"please enter what:";
getline(cin,line);
write(fd,line.c_str(),sizeof(line));
}
//关闭管道
close(fd);
}
注意,在命令行启动的时候要先启动服务端,因为我们是在服务端进行创建命名管道的

三、打印日志:
1、封装管理管道:
对于上述代码,已经是完成了命名管道的部分,但是,我们为了能够快速定位和解决问题,监控系统运行状态,提高系统的可维护性和可扩展性,就需要引入日志来将我们的代码进行完善
我们首先在connect.hpp文件中将我们的命名管道创建与删除封装在一个类中,这样,在服务端,其代码整体风格就和客户端整体差不多了
cpp
class Init
{
public:
Init()
{
// 创建管道
int n = mkfifo(FIFO_FILE, MODE);
if (n == -1)
{
perror("creat fail");
exit(FIFO_CREAT_ERR);
}
}
~Init()
{
// 删除管道
int m = unlink(FIFO_FILE);
if (m == -1)
{
perror("unlink fail");
exit(FIFO_DELETE_ERR);
}
}
};
2、日志文件的建立:
再创建一个log.hpp文件,在这个文件中进行日志的管理
首先定义一个日志的类来进行封装,在其里面加上若干接口来完成我们这个日志的建立
cpp
class Log
{
};
日志是有时间和级别的,接下来我们依次实现:
日志时间:
这里采用时间戳转本地时间:
1、获取本地时间戳:
这里采用time函数来获取时间戳
如下一行代码就是获取时间戳,将时间戳存储带now中(time函数需包含头文件<ctime>)
cpp
time_t now = time(nullptr);
2、将时间戳换算成年月日时分秒:
这里采用localtime来进行替换(需包含头文件<cstdarg>),在换算的时候,记得加上偏移量
cpp
struct tm *local_time = localtime(&now);
printf("%d-%d-%d %d:%d:%d\n",local_time->tm_year+1900,local_time->tm_mon+1,
local_time->tm_mday,local_time->tm_hour,local_time->tm_min,local_time->tm_sec);
这样就能够拿到本地时间,用年月日表示出来
日志级别:
日志是有级别的,部分级别如下:
INFO :常规运行状态(如服务启动、配置加载),内测版本可输出
DEBUG :调试关键路径,用于定位功能性问题(如函数调用流程)
WARNING :非关键异常(如磁盘空间不足),系统可自我修复
ERROR :严重错误(如数据库连接失败),影响部分功能但系统仍运行
FATAL:不可恢复错误(如内存耗尽),导致系统崩溃
在我们的日志文件中,采用定义宏的方式进行定义好日志等级
cpp
#define INFO 0
#define DEBUG 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
日志级别字符串化:
我们的日志级别在打印中或者写入文件中最好是字符串方便我们知道查看
所以我们在log类中定义一个接口(leveltostring)来将级别的宏转化为字符串
cpp
string leveltostring(int level)
{
switch (level)
{
case INFO:
return "INFO";
case DEBUG:
return "DEBUG";
case WARNING:
return "WARNING";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return "NON";
}
}
日志打印方式:
对于日志中,我们可以打印在屏幕上,也可以打印在单个文件中,也可以分文件进行打印,这里一共有三个方式,我们也采用定义宏的方式来进行分类
cpp
#define SCREEN 1
#define ONEFILE 2
#define MOREFILE 3
日志类构造与析构:
我们创建好了一个类,在其中有两个成员变量printstyle和path分别作为打印的方式(打印在屏幕还是输入到文件中)和如果输入到文件中,这个文件所在的路径
然后在析构中要初始化这两个,下面默认作为打印在屏幕中
cpp
Log()
:printstyle(SCREEN)
,path("./log/") //默认路径是当前路径下的log文件夹
{
mkdir(path.c_str(),0765);
}
日志的打印:
接下来就可以着手准备日志的打印了
cpp
void logmessage(int level,const char* format,...)
如上,第一个参数代表着日志的级别(这个由用户进行判断的),第二个参数format的作用是格式化模版字符串,在后面使用中用于指导将可变参数(后面的...,在下面代码中有实例化)格式化为最终的日志消息部分
那么在这个函数中,我们由两部分组成,左边部分是当前时间,右边为自定义部分,可变参数,这两个消息组合起来成为一个完整的日志消息
cpp
void logmessage(int level, const char *format, ...)
{
// 处理时间
time_t now = time(nullptr);
// 将时间戳转为本地时间
struct tm *local_time = localtime(&now);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", leveltostring(level).c_str(),
local_time->tm_year + 1900, local_time->tm_mon + 1, local_time->tm_mday,
local_time->tm_hour, local_time->tm_min, local_time->tm_sec);
// 处理可变参数
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 将两个消息组合起来成为一个完整的日志消息
// 默认部分+自定义部分
char logbuffer[SIZE * 2];
snprintf(logbuffer, sizeof(logbuffer), "%s %s", leftbuffer, rightbuffer);
printlog(level, logbuffer);
}
最后将两部分结合起来放在一个数组中,然后把这个数组通过printlog函数进行打印出来
这里为了在server函数中方便进行调用,推荐进行重载()
cpp
void operator ()(int level, const char *format, ...)
{
// 处理时间
time_t now = time(nullptr);
// 将时间戳转为本地时间
struct tm *local_time = localtime(&now);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", leveltostring(level).c_str(),
local_time->tm_year + 1900, local_time->tm_mon + 1, local_time->tm_mday,
local_time->tm_hour, local_time->tm_min, local_time->tm_sec);
// 处理可变参数
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 将两个消息组合起来成为一个完整的日志消息
// 默认部分+自定义部分
char logbuffer[SIZE * 2];
snprintf(logbuffer, sizeof(logbuffer), "%s %s", leftbuffer, rightbuffer);
printlog(level, logbuffer);
}
printlog函数的打印方式:
通过成员变量是什么打印方式来进行打印(默认为打印在屏幕上,这个在构造函数中已经进行初始化了,那么修改在之后写一个函数进行修改即可)
cpp
void change(int style)
{
printstyle = style;
}
cpp
void printlog(int level, const string &logbuffer) // 这里引用避免大型字符串的拷贝开销,优化性能
{
switch (printstyle)
{
case SCREEN:
cout << logbuffer << endl;
break;
case ONEFILE:
printonefile(logname, logbuffer);
break;
case MOREFILE:
printmorefile(level, logbuffer);
break;
}
}
接下来只需要实现往一个文件中进行打印,往多个文件中进行打印的函数即可
cpp
void printonefile(const string &_logname, const string &logbuffer)
{
string __logname = path + _logname;
int fd = open(__logname.c_str(),O_WRONLY|O_CREAT|O_APPEND,0666);
if(fd < 0)
return ;
write(fd,logbuffer.c_str(),logbuffer.size());
close(fd);
}
cpp
void printmorefile(int level, const string &logbuffer)
{
//思路:通过不同的文件名进行区分
string _logname = logname;
_logname += ".";
_logname += leveltostring(level);
printonefile(_logname,logbuffer);
}
这样就完成了一个简单基础的日志系统
成果展示:
如下,这次日志是往显示器上打印的


如上,当我们将代码中的打印方式进行修改


相同的,如果我们想要分文件查看,就只需要修改打印方式即可

这样,就能够在log路径下看到分好类的文件了


四、全部代码:
server.cpp
cpp
#include "connect.hpp"
#include "log.hpp"
using namespace std;
int main()
{
Init init;
Log log;
// log.change(SCREEN);
//log.change(ONEFILE);
log.change(MOREFILE);
//打开管道
int fd = open(FIFO_FILE,O_RDONLY);
if(fd < 0)
{
// perror("open fail");
log(FATAL,"error code : %d error string : %s",errno,strerror(errno));
exit(FIFO_OPEN_ERR);
}
log(INFO,"error code : %d error string : %s\n",errno,strerror(errno));
log(DEBUG,"error code : %d error string : %s\n",errno,strerror(errno));
log(WARNING,"error code : %d error string : %s\n",errno,strerror(errno));
log(ERROR,"error code : %d error string : %s\n",errno,strerror(errno));
//进行通信
while(1)
{
char buffer[NUM] = {0};
int x = read(fd,buffer,sizeof(buffer));
if(x > 0)
{
buffer[x] = 0;
cout<<"client send to server@"<<buffer<<endl;
}
else if(x == 0)
{
cout<<"client quit, me too!"<<endl;
break;
}
else break;
}
//关闭管道
close(fd);
return 0;
}
client.cpp
cpp
#include"connect.hpp"
using namespace std;
int main()
{
//打开管道
int fd = open(FIFO_FILE,O_WRONLY);
if(fd < 0)
{
perror("open fail");
exit(FIFO_OPEN_ERR);
}
//进行通信
string line;
while(1)
{
cout<<"please enter what:";
getline(cin,line);
write(fd,line.c_str(),sizeof(line));
}
//关闭管道
close(fd);
}
connect.hpp
cpp
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#define FIFO_FILE "./myfifo"
#define MODE 0664
#define NUM 1024
enum
{
FIFO_CREAT_ERR = 1,
FIFO_DELETE_ERR,
FIFO_OPEN_ERR
};
class Init
{
public:
Init()
{
// 创建管道
int n = mkfifo(FIFO_FILE, MODE);
if (n == -1)
{
perror("creat fail");
exit(FIFO_CREAT_ERR);
}
}
~Init()
{
// 删除管道
int m = unlink(FIFO_FILE);
if (m == -1)
{
perror("unlink fail");
exit(FIFO_DELETE_ERR);
}
}
};
log.hpp
cpp
#pragma once
#include <iostream>
#include <ctime>
#include <cstdarg>
#include <errno.h>
#define INFO 0
#define DEBUG 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define SCREEN 1
#define ONEFILE 2
#define MOREFILE 3
#define SIZE 1024
#define logname "log.txt"
using namespace std;
class Log
{
public:
Log()
:printstyle(SCREEN)
,path("./log/")// 默认路径是当前路径下的log文件夹
{
mkdir(path.c_str(),0765);
}
void change(int style)
{
printstyle = style;
}
string leveltostring(int level)
{
switch (level)
{
case INFO:
return "INFO";
case DEBUG:
return "DEBUG";
case WARNING:
return "WARNING";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return "NON";
}
}
void operator()(int level, const char *format, ...)
{
// 处理时间
time_t now = time(nullptr);
// 将时间戳转为本地时间
struct tm *local_time = localtime(&now);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", leveltostring(level).c_str(),
local_time->tm_year + 1900, local_time->tm_mon + 1, local_time->tm_mday,
local_time->tm_hour, local_time->tm_min, local_time->tm_sec);
// 处理可变参数
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 将两个消息组合起来成为一个完整的日志消息
// 默认部分+自定义部分
char logbuffer[SIZE * 2];
snprintf(logbuffer, sizeof(logbuffer), "%s %s", leftbuffer, rightbuffer);
printlog(level, logbuffer);
}
// void logmessage(int level, const char *format, ...)
// {
// // 处理时间
// time_t now = time(nullptr);
// // 将时间戳转为本地时间
// struct tm *local_time = localtime(&now);
// char leftbuffer[SIZE];
// snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", leveltostring(level).c_str(),
// local_time->tm_year + 1900, local_time->tm_mon + 1, local_time->tm_mday,
// local_time->tm_hour, local_time->tm_min, local_time->tm_sec);
// // 处理可变参数
// va_list s;
// va_start(s, format);
// char rightbuffer[SIZE];
// vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
// va_end(s);
// // 将两个消息组合起来成为一个完整的日志消息
// // 默认部分+自定义部分
// char logbuffer[SIZE * 2];
// snprintf(logbuffer, sizeof(logbuffer), "%s %s", leftbuffer, rightbuffer);
// printlog(level, logbuffer);
// }
void printlog(int level, const string &logbuffer) // 这里引用避免大型字符串的拷贝开销,优化性能
{
switch (printstyle)
{
case SCREEN:
cout << logbuffer << endl;
break;
case ONEFILE:
printonefile(logname, logbuffer);
break;
case MOREFILE:
printmorefile(level, logbuffer);
break;
}
}
void printonefile(const string &_logname, const string &logbuffer)
{
string __logname = path + _logname;
int fd = open(__logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0)
return;
write(fd, logbuffer.c_str(), logbuffer.size());
close(fd);
}
void printmorefile(int level, const string &logbuffer)
{
// 思路:通过不同的文件名进行区分
string _logname = logname;
_logname += ".";
_logname += leveltostring(level);
printonefile(_logname, logbuffer);
}
~Log()
{
}
private:
int printstyle;
string path;
};