从零开始:基于命名管道实现客户端与服务器的实时通信
- 一. 命名管道
-
- 1.1 基本概念
- 1.2 创建命名管道
-
- 1.2.1 创建方法
- 1.2.2 示例代码:
- 1.2.3 注意事项:
- 1.3 与匿名管道区别
- 1.4 打开原则
-
- 1.4.1 管道打开顺序
- 1.4.2 阻塞行为
- 1.4.3 管道的关闭
- 1.4.4 关闭读写端的规则
- 1.5 特点
- 二. 客户端与服务器的实现
-
- 2.1 简介
- 2.2 基本工作原理
- 2.3 实现通信
-
- 2.3.1 创建并设置命名管道
- 2.3.2 服务端实现(读取命名管道中的消息)
- 2.3.3 客户端实现(向命名管道写入消息)
- 2.3.4 定义常量和文件路径
- 2.3.5 总结:
- 三. 最后
命名管道是一种用于进程间通信(IPC)的机制,它通过特定的文件路径进行数据传输。客户端和服务器可以通过打开命名管道进行双向数据交换。服务器通常会创建一个命名管道并等待客户端连接,而客户端则需要访问这个管道进行通信。通信的过程基于FIFO(先进先出)原则,即先写入的数据会先被读取。通过这种方式,客户端和服务器能够实现异步通信,且数据传输简单高效。命名管道的优势在于它不需要显式的网络连接,使得在同一台机器上的进程间通信变得更加便捷。
💬 欢迎讨论:如果你在学习过程中有任何问题或想法,欢迎在评论区留言,我们一起交流学习。你的支持是我继续创作的动力! 👍点赞、收藏与分享:觉得这篇文章对你有帮助吗?别忘了点赞、收藏并分享给更多的小伙伴哦!你们的支持是我不断进步的动力! 🚀分享给更多人:如果你觉得这篇文章对你有帮助,欢迎分享给更多对Linux OS感兴趣的朋友,让我们一起进步!
一. 命名管道
1.1 基本概念
命名管道(Named Pipe)是一种用于进程间通信(IPC)的机制,它允许不同进程之间通过一个命名的通道交换数据。与匿名管道不同,命名管道是通过一个系统级的文件路径来标识和访问,允许不同的进程在不同的时间、甚至不同的机器上进行通信。
允许不具有血缘关系的进程之间进行通信,数据使用FIFO原则进行数据传输。
1.2 创建命名管道
1.2.1 创建方法
- 方法一:在命令行创建语法:
mkfifo filename
该指令将会创建命名管道filename(管道名称)。
- 方法二: 使用相关的函数创建,语法如下:
int mkfifo(const char *pathname, mode_t mode);
参数说明:
- pathname:这是要创建的命名管道的路径。这个路径需要是一个有效的文件路径,且在文件系统中应该是一个文件名。
- mode:指定管道的权限,类似于文件的权限。它通常是一个由数字组成的权限值,类似于文件的权限位,例如 S_IRUSR | S_IWUSR表示用户可读可写。权限的设置遵循Linux的文件权限规则。
返回值:
- 如果命名管道创建成功,返回 0。
- 如果失败,返回 -1 并设置 errno 以指示错误原因。
1.2.2 示例代码:
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
const char *fifo_path = "/tmp/my_fifo";//命名管道路径
// 创建命名管道
if (mkfifo(fifo_path, 0666) == –1) {
perror("mkfifo failed");
return 1;
}
printf("FIFO created successfully\\n");//成功是打印该内容
return 0;
}
在这个示例中,mkfifo 创建了一个名为 /tmp/my_fifo 的命名管道,权限设置为 0666,即文件的所有者、用户组和其他用户均可读写。
1.2.3 注意事项:
1.3 与匿名管道区别
标识方式 | 有文件路径,可以通过路径访问 | 没有文件路径,仅通过管道描述符访问 |
作用范围 | 支持不同进程间通信,甚至跨系统通信 | |
创建方式 | 使用mkfifo()创建 | 使用pipe()创建 |
生命周期 | 用户控制,管道文件存在于文件系统中 | 进程结束后自动销毁 |
阻塞行为 | 支持阻塞,写操作阻塞直到有读进程 | 支持阻塞,写操作阻塞直到有读进程 |
使用场景 | 不同进程间通信 | 父子进程之间通信 |
命名管道具有更强的灵活性,适用于跨进程、跨系统的复杂场景;而匿名管道则更简单,适用于父子进程间的简单数据传输。
- 如何理解用户控制:
"用户控制”则表示,管道的生命周期由用户管理,用户可以决定管道的创建、删除时机,并且管道不会自动销毁,直到用户删除它。
1.4 打开原则
1.4.1 管道打开顺序
- 写进程:可以在没有读进程存在的情况下打开管道,数据被放入管道缓冲区,直至有读进程打开并读取读取数据。如果写进程在管道缓冲区已满时继续写入,写端会发生阻塞,直到有数据可以读取。如果没有任何进程写入数据,读进程将永久阻塞。
- 读进程:写进程必须存在,没有时读端将发生阻塞,直至有数据可以读取。如果没有任何进程写入数据,读进程将会发生永久性阻塞。
1.4.2 阻塞行为
- 阻塞读取:如果读进程尝试从命名管道读取数据,但管道中没有数据,它会被阻塞,直到有写进程向管道写入数据。
- 阻塞写入:如果写进程向管道写入数据,而没有任何读进程准备好读取数据(或者管道缓冲区已满),写进程将被阻塞,直到有读进程读取数据或者管道中有空闲空间。
1.4.3 管道的关闭
- 进程关闭管道:进程在使用完管道后应该调用 close()来关闭管道。如果管道被完全关闭(即读写端都关闭),则其他试图读取或写入的进程会遇到 EOF 或 EPIPE 错误。
- 当管道中的数据被全部读取后,读进程会收到一个“文件结束标志(EOF)”。如果继续尝试读取,将会返回 0,表示没有更多数据可读取。
1.4.4 关闭读写端的规则
- 写端关闭时:如果写端关闭而读端仍在读取数据,读端会得到一个 EOF(文件结束符)信号,表明没有更多数据。
- 读端关闭时:如果读端关闭而写端继续写入数据,写进程会得到一个 EPIPE 错误,表明管道的另一端已经关闭。
1.5 特点
特点与匿名管道相似,唯一不同的是它可以让毫不相干的进程之间进行通信。
二. 客户端与服务器的实现
2.1 简介
用命名管道(Named Pipe)实现客户端与服务端通信是一种常见的进程间通信(IPC)方式。在这种模式下,服务端和客户端通过一个共享的命名管道进行数据交换。命名管道提供了一个通信通道,使得它们可以在不同的进程间进行双向数据传输。由于命名管道是一种通过文件系统实现的通信机制,因此客户端和服务端可以通过文件路径访问该管道。
2.2 基本工作原理
命名管道的工作原理基于先进先出规则,即数据写入管道的顺序会按照写入的顺序被读取。它在进程间提供一个缓冲区,允许数据从一个进程流向另一个进程。
在客户端和服务端通信中,服务端通常是管道的创建者,而客户端则是通过管道进行读取和写入操作。
2.3 实现通信
2.3.1 创建并设置命名管道
为什么要使用命名管道(FIFO)?
创建命名管道:
- 我们在 服务端 中创建命名管道。
int n = mkfifo(FIFO_FILE, 0666); // 创建管道,文件名为 FIFO_FILE
if (n != 0) {
std::cerr << "mkdir fifo error" << std::endl;
return 1;
}
2.3.2 服务端实现(读取命名管道中的消息)
- 服务端的主要功能:
服务端的工作是打开命名管道文件,读取客户端发送过来的消息并显示。
int fd = open(FIFO_FILE, O_RDONLY); // 以只读方式打开命名管道
if (fd < 0) {
std::cerr << "open fifo error" << std::endl;
return 2;
}
- 解释:
open() 用于打开已经创建的命名管道文件。这里我们以 O_RDONLY(只读)方式打开它,意味着服务端只是从管道中读取数据。
如果管道无法打开,输出错误信息并返回 2。
- 读取信息
char buffer[1024];
while (true) {
int n = read(fd, buffer, sizeof(buffer) – 1); // 从管道读取数据
if (n > 0) {
buffer[n] = 0; // 确保字符串结束
std::cout << "client say# " << buffer << std::endl; // 打印客户端消息
}
}
- 解释:
read() 函数从命名管道中读取数据,直到管道中没有数据可读。buffer 存储读取到的数据。
如果读取成功,将数据输出到控制台。
通过 n 判断读取的字节数,如果大于 0,说明读取成功。
- 关闭管道
close(fd); // 关闭管道
- 解释:
完成数据读取后,关闭管道文件描述符。
2.3.3 客户端实现(向命名管道写入消息)
- 客户端的主要功能:
客户端需要向命名管道写入消息。在每次输入后,客户端会将用户输入的消息写入管道,供服务端读取。
int fd = open(FIFO_FILE, O_WRONLY); // 以只写方式打开命名管道
if (fd < 0) {
std::cerr << "open fifo error" << std::endl;
return 2;
}
- 解释:
open() 用于打开管道文件,O_WRONLY 表示只写打开管道。客户端仅通过该管道写入数据。
如果打开失败,输出错误并返回 2。
发送信息:
while (true) {
std::cout << "Please Enter# ";
std::string message;
std::cin >> message; // 从用户输入读取消息
int n = write(fd, message.c_str(), message.size()); // 将消息写入管道
if (n > 0) {
// 写入成功
}
}
- 解释:
std::cin >> message 用于从用户输入获取消息。
write() 用于将消息写入管道。如果消息成功写入,n 返回写入的字节数。
关闭管道:
close(fd); // 关闭管道
- 解释:
客户端发送完消息后,关闭管道。
2.3.4 定义常量和文件路径
#pragma once
#define FIFO_FILE "fifo" // 定义命名管道的文件路径
- 解释:
在 common.hpp 文件中定义一个宏 FIFO_FILE,用于存储命名管道的路径。在客户端和服务端中都使用这个路径,确保一致性。
2.3.5 总结:
三. 最后
本文介绍了基于命名管道(FIFO)实现客户端与服务器实时通信的过程。命名管道是一种通过文件路径进行进程间通信的机制,支持不同进程之间的数据传输,基于FIFO(先进先出)原则。通过 mkfifo() 函数可以创建命名管道,客户端和服务端通过管道文件进行数据交换。服务端创建并打开管道以读取数据,客户端通过管道向服务端写入消息。命名管道的优势在于不需要网络连接,适用于同一台机器上的进程通信。通过合适的同步与阻塞机制,客户端与服务端能够顺利实现数据传输。
完整代码(如下):
server.cc
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#include <unistd.h>
#include "common.hpp"
int main()
{
umask(0);
int n = mkfifo(FIFO_FILE, 0666);
if(n != 0)
{
std::cerr << "mkdir fifo error" << std::endl;
return 1;
}
int fd = open(FIFO_FILE,O_RDONLY);
if(fd < 0)
{
std::cerr << "open fifo error" << std::endl;
return 2;
}
char buffer[1024];
while(true)
{
int n = read(fd,buffer,sizeof(buffer) –1);
if(n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
}
}
close(fd);
return 0;
}
client.cc
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#include <unistd.h>
#include "common.hpp"
int main()
{
int fd = open(FIFO_FILE,O_WRONLY);
if(fd < 0)
{
std::cerr << "open fifo error" << std::endl;
return 2;
}
while(true)
{
std::cout << "Please Enter# ";
std::string message;
std::cin >> message;
int n = write(fd, message.c_str(),message.size());
if(n > 0)
{
}
}
close(fd);
return 0;
}
comm.hpp
#pragma once
#define FIFO_FILE "fifo"
makefile
.PHONY:all
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
上述代码可以实现两者进行通信。
演示图片(如下图):
评论前必须登录!
注册