目录
[4.C++ 实战:编写一个简单的 Server-Client 聊天程序](#4.C++ 实战:编写一个简单的 Server-Client 聊天程序)
引言:为什么需要命名管道?
在学习进程间通信(IPC)时,我们通常首先接触的是匿名管道 (Anonymous Pipe) 。匿名管道简单好用,但它有一个致命的限制:只能在具有共同祖先(具有亲缘关系,如父子进程)的进程间通信。
那么,如果我们在系统中运行了两个完全不相关的进程,它们也想通过管道交换数据,该怎么办呢?这时候,命名管道(Named Pipe,也叫 FIFO)就闪亮登场了!
命名管道是一种特殊类型的文件,它在文件系统中有一个真实存在的路径名。因此,任何进程只要有权限访问该路径,就可以通过它进行通信。
1.创建一个命名管道
命名管道可以通过命令行创建,也可以在代码中动态创建。
1.1命令行创建
在终端中,我们可以使用 mkfifo 命令来创建一个命名管道:
$mkfifo my_fifo$ ls -l my_fifo
prw-rw-r-- 1 user user 0 Oct 24 10:00 my_fifo
注意观察 ls -l 的输出,文件类型是 p,代表 Pipe(管道文件)。
1.2代码中创建
在 C/C++ 程序中,我们使用 mkfifo 函数:
cpp
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
-
pathname: 管道文件的路径/名字。
-
mode : 文件的权限(例如
0666),实际权限会受系统的umask影响。 -
返回值 : 成功返回 0,失败返回 -1 并设置
errno。
创建命名管道:
cpp
int main(int argc, char *argv[])
{
mkfifo("p2", 0644);
return 0;
}
2.匿名管道与命名管道的区别
很多初学者会疑惑,命名管道既然是个"文件",那通信时是不是把数据写到磁盘上了?速度会不会很慢?
答案是:绝对不会! 它们之间的区别仅仅在于"打开方式",底层通信介质其实是一样的。
| 对比维度 | 匿名管道 (Pipe) | 命名管道 (FIFO) |
|---|---|---|
| 创建方式 | pipe() 函数 |
mkfifo 命令或函数 |
| 打开方式 | 创建时自动打开 | 使用 open() 函数像普通文件一样打开 |
| 适用范围 | 仅限有亲缘关系的进程 | 任意两个进程(只要能访问同一路径) |
| 底层原理 | 内存级别的缓冲区,不写磁盘 | 文件系统中存在路径节点,但数据仍然只存在于内存缓冲区,不写磁盘 |
3.命名管道的打开规则与同步机制
命名管道自带同步机制,什么是同步?简单来说,就是"互相等待"。在使用open()函数时打开命名管道,有非常严格的阻塞规则:
-
如果你是为了"读"而打开 (O_RDONLY):
-
阻塞模式(默认) :程序会卡在
open()这里阻塞住,直到有另一个进程为了"写"打开了这个管道,程序才会继续往下走。 -
非阻塞模式 (O_NONBLOCK):立刻返回成功。
-
-
如果你是为了"写"而打开 (O_WRONLY):
-
阻塞模式(默认) :程序会卡在
open()这里阻塞住,直到有另一个进程为了"读"打开了这个管道。 -
非阻塞模式 (O_NONBLOCK) :立刻返回失败,错误码为
ENXIO(因为没人读,你写了也没意义)。
-
总结一句话:写端必须等读端,读端必须等写端。双方都准备好(都 open 成功)了,才能愉快地通信!
4.C++ 实战:编写一个简单的 Server-Client 聊天程序
接下来我们用代码实现一个单向通信:Client 发送消息,Server 接收并打印。
结果:

代码:
Makefile:
cpp
all:client server
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client server
comm.h:
定义管道的名称,方便两边统一。
cpp
#pragma once
#include <string>
const std::string fifoname = "fifo";
服务器/读端,服务器负责创建管道、读取数据,并在结束后清理管道文件。
cpp
#include <iostream>
#include <cstdio>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include "comm.h"
int main()
{
// 1.创建管道文件
umask(0);
int n = mkfifo(fifoname.c_str(), 0666);
if(n < 0)
{
perror("mkfifo");
return 1;
}
// 2.打开管道文件
std::cout << "open begin" << std::endl;
int rfd = open(fifoname.c_str(), O_RDONLY);
if(rfd < 0)
{
perror("open");
return 2;
}
std::cout << "open end" << std::endl;
char inbuffer[1024];
// 3.进行通信
while(true)
{
ssize_t n = read(rfd, inbuffer, sizeof(inbuffer)-1);
if(n > 0)
{
inbuffer[n] = 0;
std::cout << "client say# " << inbuffer << std::endl;
}
else if(n == 0)
{
// 写端关闭
std::cout << "client quit, me too" << std::endl;
break;
}
else
{
perror("read");
break;
}
}
// 关闭
close(rfd);
// 删除管道文件
unlink(fifoname.c_str());
return 0;
}
客户端,直接打开以存在的管道文件,并发送终端输入的数据。
cpp
#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.h"
int main()
{
std::cout << "open begin" << std::endl;
int wfd = open(fifoname.c_str(), O_WRONLY);
if(wfd < 0)
{
perror("open");
return 1;
}
std::cout << "open end" << std::endl;
std::string outstring;
while(true)
{
std::cout << "Please Enter@ ";
std::cin >> outstring;
write(wfd, outstring.c_str(), outstring.size()); // 不需要写\0
}
close(wfd);
return 0;
}
总结
通过这篇文章,我们了解到命名管道(FIFO)打破了匿名管道必须具有亲缘关系的限制 。虽然它以"文件"的形式存在于文件系统中,但其本质依然是内核中的一块内存缓冲区。掌握了命名管道的阻塞规则和使用方法,我们就掌握了 Linux 下最经典、最基础的进程间解耦通信方式之一。
如果你觉得这篇文章对你有帮助,欢迎点赞收藏!如有疑问,也欢迎在评论区留言讨论。