写在前面
今天主要的任务就是知道什么是进程通信?进程通信是如何实现的?前面我们学习了基础IO,再往前看又学习进程的相关的概念,那么今天我们通过进程的通信来把他们用起来.这个话题挺重要的,但是没有前面的大.
进程通信
"通信"这个单词很好理解,就是两个或者多个事物之间相互交谈.那么进程之间是如何交谈的呢?进程通信有什么实际应用呢?今天这个博客就会深入浅出的带大家了解.
通信背景
我们在进程那里就一直强调,进程具有独立性,甚至父子进程一旦父进程或者子进程修改原本的数据都会发生写时拷贝.这个就造成了进程与进程之间是不能够相互交流的或者交流的成本非常高.那么他们是真的完全独立吗?要知道藕断还会丝连,这里独立性不是彻底的独立,当然独立的程度也不会很少,这里我们理解要把握一个度.想一想如果现实世界我们人与人无法交流是多么令人绝望的一件事啊.现实中声音通过介质传递,那么进程于进程直接是不是可以通过某些事物来帮助我们完成通信呢?这里是可以的,也就是我们今天的内容.
通信目的
在一个大型项目中,我们是多个程序员相互协作完成工作的.例如程序员张三只需要负责数据处理模块,李四只需要负责缓存模块...这个相互协作肯定会涉及到人与人之间的交流,否则张三闷头一直干,遇到了缓存相关的任务也不和李四交流,自己查资料解决.如果一个人连自己负责的任务都不了解,就可能会造成总体进度进展缓慢.同理进程之间也是需要交流的,我们可以通过进程通信让不同的进程处理不同的内容.
下面我简单的说一下为何要是进程通信:
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变
这里简单谈一下进程控制,前面我们在调试代码的时候就是进程控制,我们可以通过debug这个进程来使程序走一步,是否进入函数等操作.
通信发展
这里就不再阐述通信技术是如何发展的,总而言之就是一大堆大佬在一起去研究,最终大浪淘沙现在剩下下面的东西.下面的内容我们都会谈的,不过有的要放在线程中.
- 管道
- System V进程间通信 主要是本地通信,现在很少使用,流行分布式,跨主机
- POSIX进程间通信
注意,上面我写的是三个,实际上是两套标准,管道这个可以理解为天然存在的.关于这两套标准我们再后面都会学习,即使System V有些老,这里我们也会认识认识,都不要着急.
通信本质
我们可以很容易想到,我们进程通信是前提是我们需要先看到通一份资源,也就是让不同的进程看到同一份资源,这一份资源可以是文件,内存,甚至是我们后面的网络.我们可以把这个资源理解成声音传播的介质,它是什么不重要,重要的是它可以帮助我们传输数据.后面的所有的学习都是围绕这个展开的.
注意,我们一般很少把数据刷到外设中,也就是我们这个资源只是挂个名,这个我在后面会演示到.
管道
我们先来学习管道的通信方式.里面涉及到的内容很多,但是很简单.大家要好好学,后面我们他来制作一个进程池,这也为后面的其他的通信方式及打下基础.管道分为两类,我们后面都会用到.
- 匿名管道
- 命名管道
管道原理
我们先把管道的原理给大家说一下.前面我们知道进程的task_struct,这里我们还要借助前面的基础IO.在创建子进程的时候我们还要不要把打开的文件被拷贝一份呢?不需要.但是子进程的大多数据都来自父进程,其中文件描述符就是要被拷贝的.
此时我们父子进程就指向了同一资源了,这同样解释了我们之前父子进程在打印的时候都打印在显示器上的原因.如果我们的资源是管道,我们在内存中直接创建一个文件,这个文件是不需要刷新到磁盘.我们有一个问题,它是如何知道我打开这个文件是管道文件还是普通文件呢?我们看看源码,这个在inode里面有关于文件的分类,其中一个就是管道文件,我们不需要把文件刷新到磁盘,只需要接借助文件的缓冲区就可以了.
管道特点
大家在生活中见过水龙头,天然气管道...,生活中的管道是随处可以想到的.我想问问大家大部分的管道是可以同时从右向左和从左向右传输事物的吗?不太可能,也就是管道一般都是单向的,这也是管道文件的特点.数据是计算机最重要的资源,是信息领域的石油,也就是管道里面流动的是数据.Linux下我们可以通过文件来传输数据,此时管道就是一个内存级文件,它的数据不需要刷新到磁盘.此时我们就可以描述管道的特点了.
- 单项的 管道本身具有的
- 传送数据的 这是管道的功能
请问那么我们是如何把管道成单项的呢?内核是如何实现的.这里很简单.我们创建一个文件,时的两个fd分别指向管道的写端和读端,注意读端和写端是一个抽象概念,不需要细究.
这个时候我们创建一个子进程,子进程的大部分数据来自父进程,也就是两个fd都被继承了.
我们让父进程关闭一个写端子进程关闭一个读端(或者反过来),此时我们就可以父子进程发生进程通信了.
上面的所有操作我们都可以通过内核提供的接口来实现,所以不用着急.这里有几个问题,为何父进程要打开读写两个端口,这是由于子进程看到的完全都是继承父进程的,我们开始的时候父子进程不知道哪个写哪个读.我们打开两个,这样子进程不需要自己打开了.父子进程为何都要关闭一段?这是由于管道是单项的,我们必须保证这一点.是谁决定父进程读还是写?这是需求决定的,当客户提出需求,我们按需求来写.
匿名管道
内核提供了创建管道文件的接口,这是一个输入输出性的参数.
[bit@Qkj 12_09]$ man 2 pipe
这个接口如果打开文件成功,返回0,如果打开失败,此时返回-1,并且错误码被设置.我们先用一下这个接口,看看效果.
#include <unistd.h>
#include <stdio.h>
int main()
{
int pipefd[2] = {0};
pipe(pipefd);
printf("pipefd[0] %d\n", pipefd[0]);
printf("pipefd[1] %d\n", pipefd[1]);
return 0;
}
这个接口,下标为0的表示读端,1代表写端,这个1是不是很像一根笔,我们可以这么记忆.当我们知道了客户要求的时候,就可以决定父子进程关闭哪一端了.此时我们让父进程进行写,让子进程进行读.我们开始创建子进程.关于创建子进程的相关知识我就不说了,这里在之前都谈过,下面直接开始吧.
int main()
{
int pipefd[2] = {0};
if (pipe(pipefd) != 0)
{
cerr << "pipe open fail" << errno << endl;
}
// 创建子进程
pid_t id = fork();
if (id < 0)
{
// 失败
}
else if (id == 0)
{
// child 关闭 写
close(pipefd[1]);
}
else
{
// parent 关闭 读
close(pipefd[0]);
}
return 0;
}
这个时候我们们就可以实现父子进程之间的通信了,我们把管道当作普通的文件进行读写,这一点基础IO的我们也是谈过的.我们先来观察一下现象.
int main()
{
int pipefd[2] = {0};
if (pipe(pipefd) != 0)
{
cerr << "pipe open fail" << errno << endl;
}
// 创建子进程
pid_t id = fork();
if (id < 0)
{
// 失败
}
else if (id == 0)
{
// child 关闭写
close(pipefd[1]);
#define NUM 100
char buffer[NUM];
while (1)
{
memset(buffer, '\0', sizeof(buffer));
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (s == 0)
{
cout << "读端关闭,我也退出了" << endl;
break;
}
else if (s < 0)
{
cout << "读取错误" << endl;
break;
}
else
{
buffer[s] = '\0';
cout << buffer << endl;
}
}
close(pipefd[0]);
return 0; // 子进程在这里推虎
}
else
{
// parent 关闭 读
close(pipefd[0]);
string msg = "你好子进程.我是父进程";
int cnt = 0;
while (cnt < 5)
{
write(pipefd[1], msg.c_str(), msg.size());
cnt++;
sleep(1);
}
cout << "父进程已经接完了" << endl;
close(pipefd[1]);
}
// 等待子进程
pid_t ret = waitpid(id, NULL, 0);
if (ret > 0)
{
cout << "等待成功" << endl;
}
return 0;
}
这里我有几个问题想和大家讨论一下.为何当我们关闭写段的时候,读端读到文件结尾后就认为这个已经结束了?这是由于在文件中存在一个引用计数,当只有一个进程连着这个文件,读到文件的末尾就是可以认为是文件读取结束了.
不知道你看到一个现象没有,我是让父进程带了个sleep,那么为何子进程也会等待?这是由于管道设计的模式是阻塞,当文件中没有数据的时候,读端就会进行阻塞等待,我们把时间戳打出来,便于我们观察.
int main()
{
int pipefd[2] = {0};
pipe(pipefd);
// 创建子进程
pid_t id = fork();
if (id == 0)
{
// child 关闭写
close(pipefd[1]);
#define NUM 100
char buffer[NUM];
while (1)
{
memset(buffer, '\0', sizeof(buffer));
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (s == 0)
{
cout << "读端关闭,我也退出了" << endl;
break;
}
else if (s < 0)
{
cout << "读取错误" << endl;
break;
}
else
{
buffer[s] = '\0';
cout << buffer << endl;
cout << "时间戳是 : " << (uint64_t)time(nullptr) << endl;
}
}
close(pipefd[0]);
return 0; // 子进程在这里推虎
}
else
{
// parent 关闭 读
close(pipefd[0]);
string msg = "你好子进程.我是父进程";
int cnt = 0;
while (cnt < 5)
{
write(pipefd[1], msg.c_str(), msg.size());
cnt++;
sleep(2);
}
cout << "父进程已经接完了" << endl;
close(pipefd[1]);
}
// 等待子进程
pid_t ret = waitpid(id, NULL, 0);
if (ret > 0)
{
cout << "等待成功" << endl;
}
return 0;
}
这里我们就可以发现子进程的读取是以父进程为主的,这就是管道的阻塞等待.子进程等待父进程写入完成后,才会进行读取.子进程要以父进程的节奏为主.那么我们可以这么说父子进程在读写的时候是有一定的顺序经行的,我做了你才能能做.这个就称之为自带访问控制机制.以前我们父子进程往显示器上打印数据,那是争先恐后的,这叫缺乏访问控制.
在来解释最后一个疑问,阻塞等待的本质就是将当前进程的pcb放入等待队列中.请问我放在了哪一个等待队列中?这个等待队列是管道自带的,在管道资源中等待
上面我们都是简单的看一下管道文件是如何用的,这里给大家添加一些内容,让子进程处理不同的任务.
typedef void (*functor)(); // 函数指针
vector<functor> functors; // 方法集合
unordered_map<uint32_t, string> info; // 任务描述
void f1()
{
cout << "这是一个处理日志的任务, 执行的进程 ID [" << getpid() << "]"
<< "执行时间是[" << time(nullptr) << "]\n"
<< endl;
}
void f2()
{
cout << "这是一个备份数据任务, 执行的进程 ID [" << getpid() << "]"
<< "执行时间是[" << time(nullptr) << "]\n"
<< endl;
}
void f3()
{
cout << "这是一个处理网络连接的任务, 执行的进程 ID [" << getpid() << "]"
<< "执行时间是[" << time(nullptr) << "]\n"
<< endl;
}
void loadFunctor()
{
info.insert({functors.size(), "处理日志的任务"});
functors.push_back(f1);
info.insert({functors.size(), "备份数据任务"});
functors.push_back(f2);
info.insert({functors.size(), "处理网络连接的任务"});
functors.push_back(f3);
}
void work(int blockFd)
{
cout << "进程[" << getpid() << "]"
<< " 开始工作" << endl;
// 子进程核心工作的代码
while (true)
{
// a.阻塞等待 b. 获取任务信息
uint32_t operatorCode = 0;
ssize_t s = read(blockFd, &operatorCode, sizeof(uint32_t));
if (s == 0)
break;
assert(s == sizeof(uint32_t));
(void)s;
// c. 处理任务
if (operatorCode < functors.size())
functors[operatorCode]();
}
cout << "进程[" << getpid() << "]"
<< " 结束工作" << endl;
}
int main()
{
// 0. 加载任务列表
loadFunctor();
// 1. 创建管道
int pipefd[2] = {0};
if (pipe(pipefd) != 0)
{
cerr << "pipe error" << endl;
return 1;
}
// 2. 创建子进程
pid_t id = fork();
if (id < 0)
{
cerr << " fork error " << endl;
return 2;
}
else if (id == 0)
{
// 3. 关闭不需要的文件fd
// child,read
close(pipefd[1]);
// 4. 业务处理
while (true)
{
uint32_t operatorType = 0;
// 如果有数据,就读取,如果没有数据,就阻塞等待, 等待任务的到来
ssize_t s = read(pipefd[0], &operatorType, sizeof(uint32_t));
if (s == 0)
{
cout << "我要退出啦,我是给人打工的,老板都走了...." << endl;
break;
}
assert(s == sizeof(uint32_t));
(void)s;
if (operatorType < functors.size())
{
// 处理任务
functors[operatorType]();
}
else
{
cerr << "bug? operatorType = " << operatorType << std::endl;
}
}
close(pipefd[0]);
exit(0);
}
else
{
srand((long long)time(nullptr));
// parent,write - 操作
// 3. 关闭不需要的文件fd
close(pipefd[0]);
// 4. 指派任务
int num = functors.size();
int cnt = 10;
while (cnt--)
{
// 5. 形成任务码
uint32_t commandCode = rand() % num;
std::cout << "父进程指派任务完成,任务是: " << info[commandCode] << " 任务的编号是: " << cnt << std::endl;
// 向指定的进程下达执行任务的操作
write(pipefd[1], &commandCode, sizeof(uint32_t));
sleep(1);
}
close(pipefd[1]);
pid_t res = waitpid(id, nullptr, 0);
if (res)
cout << "wait success" << endl;
}
return 0;
}
进程池
上面我们都是用管道控制一个进程,这里我想控制一批进程怎么样?可以的.我们对每一个子进程都创建一个管道文件,让每一个进程都处理不通的事物.同样的,前面在进程那里我们没有谈太多一个父进程是如何控制多个子进程的,这里算是补上去一点.
首先第一点,我们创建一批进程,这就要利用的到循环.那么请问,我们父进程是如何记住这些子进程的?我们还是让父进程去写,让子进程去读.那么我们应该记录什么?这里主要记录的就是每一个子进程对应的额写端,因为一会我们要往里面写入内容,顺带的把子进程的pid也保存一下吧,所以我们用map.
// int32_t: 进程pid, int32_t: 该进程对应的管道写端fd
typedef std::pair<int32_t, int32_t> elem;
int processNum = 5;
int main()
{
loadFunctor();
vector<elem> assignMap;
// 创建processNum个进程
for (int i = 0; i < processNum; i++)
{
// 定义保存管道fd的对象
int pipefd[2] = {0};
// 创建管道
pipe(pipefd);
// 创建子进程
pid_t id = fork();
if (id == 0)
{
// 子进程读取, r -> pipefd[0]
close(pipefd[1]);
// 子进程执行
work(pipefd[0]);
close(pipefd[0]);
exit(0);
}
//父进程做的事情, pipefd[1]
close(pipefd[0]);
elem e(id, pipefd[1]);
assignMap.push_back(e);
}
// 到这里一定是 父进程
// 派发任务
blanceSendTask(assignMap);
// 回收资源
}
现在我们就可以通过父进程给各个的子进程派发任务了,我们把这个派发任务写成一个函数,这里先不谈.主要是要看看先把子进程退出后资源给回收了.首先我们先把逻辑理通讯了.我们看看什么时候走到回收资源,肯定是父进程任务派发结束了,也就是写端关闭了,此时当写端关闭了,子进程读不到数据,我们自动让他退出,也就是子进程就剩下资源了,此时父进程就可以回收资源了.
// 回收资源
for (int i = 0; i < processNum; i++)
{
if (waitpid(assignMap[i].first, nullptr, 0) > 0)
cout << "wait for: pid=" << assignMap[i].first << " wait success!"
<< "number: " << i << "\n";
close(assignMap[i].second);
}
我们先来看看一个简单的函数,也就是子进程工作的函数.我们在work函数里面是传入读端的,所以这里很容易,直接读就可以了.
void work(int blockFd)
{
cout << "进程[" << getpid() << "]" << " 开始工作" << endl;
// 子进程核心工作的代码
while (true)
{
// a.阻塞等待 b. 获取任务信息
uint32_t operatorCode = 0;
ssize_t s = read(blockFd, &operatorCode, sizeof(uint32_t));
if(s == 0) break;
assert(s == sizeof(uint32_t));
(void)s;
// c. 处理任务
if(operatorCode < functors.size()) functors[operatorCode]();
}
cout << "进程[" << getpid() << "]" << " 结束工作" << endl;
}
派发任务更加简单,这里所谓的派发任务,实际上就是给某一个子进程的管道写入数据,由于所有的子进程都在read那里阻塞等待,一旦父进程把数据写入了进去,此时子进程就可以经行读取了,这里就是原理.
void blanceSendTask(const vector<elem> &processFds)
{
srand((long long)time(nullptr));
while(true)
{
sleep(1);
// 选择一个进程, 选择进程是随机的,没有压着一个进程给任务
// 较为均匀的将任务给所有的子进程 --- 负载均衡
uint32_t pick = rand() % processFds.size();
// 选择一个任务
uint32_t task = rand() % functors.size();
// 把任务给一个指定的进程
write(processFds[pick].second, &task, sizeof(task));
// 打印对应的提示信息
cout << "父进程指派任务->" << info[task] << "给进程: " << processFds[pick].first << " 编号: " << pick << endl;
}
}
我们先把所有的代码给大家展示一下,这里我感觉还是有点问题的.
typedef void (*functor)();
vector<functor> functors; // 方法集合
// for debug
unordered_map<uint32_t, string> info;
// int32_t: 进程pid, int32_t: 该进程对应的管道写端fd
typedef std::pair<int32_t, int32_t> elem;
int processNum = 5;
void f1()
{
cout << "这是一个处理日志的任务, 执行的进程 ID [" << getpid() << "]"
<< "执行时间是[" << time(nullptr) << "]\n" << endl;
}
void f2()
{
cout << "这是一个备份数据任务, 执行的进程 ID [" << getpid() << "]"
<< "执行时间是[" << time(nullptr) << "]\n" << endl;
}
void f3()
{
cout << "这是一个处理网络连接的任务, 执行的进程 ID [" << getpid() << "]"
<< "执行时间是[" << time(nullptr) << "]\n" << endl;
}
void loadFunctor()
{
info.insert({functors.size(), "处理日志的任务"});
functors.push_back(f1);
info.insert({functors.size(), "备份数据任务"});
functors.push_back(f2);
info.insert({functors.size(), "处理网络连接的任务"});
functors.push_back(f3);
}
void work(int blockFd)
{
cout << "进程[" << getpid() << "]" << " 开始工作" << endl;
// 子进程核心工作的代码
while (true)
{
// a.阻塞等待 b. 获取任务信息
uint32_t operatorCode = 0;
ssize_t s = read(blockFd, &operatorCode, sizeof(uint32_t));
if(s == 0) break;
assert(s == sizeof(uint32_t));
(void)s;
// c. 处理任务
if(operatorCode < functors.size()) functors[operatorCode]();
}
cout << "进程[" << getpid() << "]" << " 结束工作" << endl;
}
// [子进程的pid, 子进程的管道fd]
void blanceSendTask(const vector<elem> &processFds)
{
srand((long long)time(nullptr));
while(true)
{
sleep(1);
// 选择一个进程, 选择进程是随机的,没有压着一个进程给任务
// 较为均匀的将任务给所有的子进程 --- 负载均衡
uint32_t pick = rand() % processFds.size();
// 选择一个任务
uint32_t task = rand() % functors.size();
// 把任务给一个指定的进程
write(processFds[pick].second, &task, sizeof(task));
// 打印对应的提示信息
cout << "父进程指派任务->" << info[task] << "给进程: " << processFds[pick].first << " 编号: " << pick << endl;
}
}
int main()
{
loadFunctor();
vector<elem> assignMap;
// 创建processNum个进程
for (int i = 0; i < processNum; i++)
{
// 定义保存管道fd的对象
int pipefd[2] = {0};
// 创建管道
pipe(pipefd);
// 创建子进程
pid_t id = fork();
if (id == 0)
{
// 子进程读取, r -> pipefd[0]
close(pipefd[1]);
// 子进程执行
work(pipefd[0]);
close(pipefd[0]);
exit(0);
}
//父进程做的事情, pipefd[1]
close(pipefd[0]);
elem e(id, pipefd[1]);
assignMap.push_back(e);
}
cout << "create all process success!" << std::endl;
// 父进程, 派发任务
blanceSendTask(assignMap);
// 回收资源
for (int i = 0; i < processNum; i++)
{
if (waitpid(assignMap[i].first, nullptr, 0) > 0)
cout << "wait for: pid=" << assignMap[i].first << " wait success!"
<< "number: " << i << "\n";
close(assignMap[i].second);
}
}
这里会有一个问题的,我们在派发任务结束后没有关闭子进程的写端,此时子进程根本不会退出,我们这里加上一个函数,它的功能就是关闭文件描述符.
void closeFd(const vector<elem> &processFds)
{
for(int i = 0; i< processFds.size();i++)
{
close(processFds[i].second);
}
}
我们这里看看指令的相关的管道,你会发现我们指令中的**|**也是管道,此时他们是两个兄弟进程进程进行通信,我们也是可以设计出来的,这里就不浪费大家的时间了.
这个时候我们就可以总结一下管道的特点了.
- 管道只能在具有血缘关系的进程之间通信,常用于父子之间
- 管道只能单项通信,这是由于管道的特性决定的,半双工的特殊性情况
- 管道自带同步机制,也就是访问机制
- 管道是面向字节流的,这里大家可能还感觉不到,等到网络那里再说
- 管道的声明周期是随进程的,进程退出后,文件也会关闭
上面我们谈到了一个词语,半双工,这里先和大家提出来,等到我们网络的时候在处理它.
- 半双工 -- 两个人说话,你说的时候我在听,我说的时候你在听,不相互干扰
- 全双工 -- 吵架,你说你的,我说我的,只管自己
命名管道
只能父子进程或者血缘关系的人通信是不是有点挫了.如果想让两个互不相关的进程通信是不是这里搞不定了?是的,我们匿名管道是不行的.这里我们用命名管道,关于命名管道,它的特性和上面是一样的,这里我们只看如何使用.这里我们先用指令给大家演示一下在命令行上这个的表示
在Linux中,内核给我们提供一个接口可以让互不相关的两个进程进行相互通信.我们先来看一看.
[bit@Qkj pipe]$ man 3 mkfifo
那么我们该如何通过代码实现呢?这里很简单,我们创建两个可执行程序,其中一个创建这个命名管道,要知道这个文件是具有有唯一的标识符也就是绝对路径的,我们让两个可执行程序一个写,一个读,此时就可以了.
我们先来创建一个头文件,这里保存我们等会要用的标准库文件,顺便我们也把管文件的路径带进去,这里为了方便我们使用相对路径.
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstdio>
#include <unistd.h>
#include <errno.h>
#include <assert.h>
#define IPC_PATH "./fifo"
那么我们应该如何做呢,我们创建两个源文件,其中一个作为服务端,一个作为客户端.我们先来写服务端的代码,这个服务端它是创建命名管道,它主要的功能是读取内容,这里没有什么可以说的,都是我们前面的内容.
#include "comm.h"
int main()
{
umask(0);
if (mkfifo(IPC_PATH, 0666) < 0)
{
std::cerr << "mkfifo errno " << errno << std::endl;
return 1;
}
int fd = open(IPC_PATH, O_RDONLY);
if (fd < 0)
{
std::cerr << "openerrno " << errno << std::endl;
return 2;
}
// 正常的通信过程
#define NUM 1024
char buffer[NUM];
while (true)
{
memset(buffer, '\0', sizeof(buffer));
// 开始读
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
if (s == 0)
{
std::cout << "客户端推出了,我也推出了" << std::endl;
break;
}
else if (s > 0)
{
buffer[s] = '\0';
std::cout << "客户端发送了一条信息 -> " << buffer
<< std::endl;
}
else
{
assert(0);
}
}
close(fd);
std::cout << "服务端退出了" << std::endl;
return 0;
}
那么客户端,我们这里就是写文件,如果文件不存在吗,这里就直接报错.为了方便,我们把要写的内容手动输入.
#include "comm.h"
int main()
{
int fd = open(IPC_PATH, O_WRONLY);
if (fd < 0)
{
std::cerr << "open : " << errno << std::endl;
return 1;
}
#define NUM 1024
char line[NUM];
while (true)
{
std::cout << "请输入你的消息: ";
fflush(stdout);
memset(line, '\0', sizeof(line));
if (fgets(line, sizeof(line) - 1, stdin) != NULL)
{
// fgets 读取是 abc\n\0 他会把 '\n' 带上也会自动添加'\0' -- C语言接口
line[strlen(line) - 1] = '\0';
write(fd, line, strlen(line));
}
else
{
break;
}
}
close(fd);
std::cout << "客户端退出了" << std::endl;
return 0;
}
这样短短的百十行代码就完成了我们想要的,我们这里编译运行,注意,一定要先让服务端开始运行,他还负责创建命名管道的功能.