【Linux系统】进程间通信:实现命名管道通信
认识命名管道通信
命名管道通信的结构图示:
图中的 Server
和 Client
是不同的进程, 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()
,如在 server
端 的 OpenPipeForRead()
函数中调用 OpenPipe(gForRead)
server
端
这里的 gForRead
是 open
函数的选项,定义成了全局数据,后面有具体讲解
// 打开文件
bool OpenPipeForRead()
{
_fd = OpenPipe(gForRead);
if(_fd < 0) return false;
return true;
}
// 关闭文件
void ClosePipeRead()
{
ClosePipe(_fd);
}
client
端
这里的 gForWrite
是 open
函数的选项,定义成了全局数据,后面有具体讲解
// 打开文件
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)