【IPC 进程间通信】有名管道的简单实现

一、前言

管道(Pipe)实际是用于进程间通信的一段共享内存,创建管道的进程称为管道服务器,连接到一个管道的进程为管道客户端。一个进程在向管道写入数据后,另一进程就可以从管道的另一端将其读取出来。

在介绍命名管道之前首先要区分匿名管道与命名管道的区别:

  • 匿名管道(Anonymous Pipes)是在父进程和子进程间单向传输数据的一种未命名的管道,只能在本地计算机中使用,而不可用于网络间的通信。
  • 命名管道(Named Pipes)顾名思义,这个管道肯定是有名字的。通过管道的名字来确保多个进程访问同一个管道。事实上,命名管道不仅可在同一台计算机的不同进程之间传输数据,甚至能在跨越一个网络的不同计算机的不同进程之间,支持可靠的、单向或双向的数据通信。(只是局域网)

二、命名管道介绍

命名管道(Named Pipes)是一种简单的进程间通信(IPC)机制,Microsoft Windows 大都提供了对它的支持(但不包括 Windows CE)。

用命名管道来设计跨计算机应用程序实际非常简单,并不需要事先深入掌握底层网络传送协议(如 TCP、UDP、IP、IPX)的知识。这是由于命名管道利用了微软网络提供者(MSNP)重定向器通过同一个网络在各进程间建立通信,这样一来,应用程序便不必关心网络协议的细节。

命名管道是一个具有名称,可以单向或双面在一个服务器和一个或多个客户端之间进行通讯的管道。命名管道的所有实例拥有相同的名称,但是每个实例都有其自己的缓冲区和句柄,用来为不同客户端通许提供独立的管道。使用实例可使多个管道客户端同时使用相同的命名管道。另外:

  • 命名管道的名称在本系统中是唯一的。
  • 命名管道可以被任意符合权限要求的进程访问。
  • 命名管道的客户端可以是本地进程(本地访问:\.\pipe\PipeName)或者是远程进程(访问远程:\ServerName\pipe\PipeName)。

命名管道服务器和客户端的区别在于:服务器是唯一一个有权创建命名管道的进程,也只有它能接受管道客户端的连接请求,而客户端只能同一个现成的命名管道服务器建立连接。

命名管道提供了两种通信模式:字节模式和消息模式。

  • 在字节模式下,数据以一个连续的字节流的形式在客户端和服务器之间流动。

  • 在消息模式下,客户端和服务器则通过一系列的不连续的数据单位,进行数据的收发,每次在管道上发出一个消息后,它必须作为一个完整的消息读入。


三、效果演示

先运行服务器工程代码,再运行客户端工程代码,如下图所示:

IPC_NamedPipe_A.png


四、命名管道实现流程

服务器端命名管道实现的步骤:

1. 创建命名管道 CreateNamedPipe
2. 等待客户端连接 ConnectNamedPipe
3. 接收客户端发送数据 ReadFile & 向客户端发送数据 WriteFile
4. 关闭管道 CloseHandle

客户端命名管道实现的步骤:

1. 判断是否有可以用的命名管道 WaitNamedPipe
2. 打开管道 CreateFile
3. 接收服务器发送数据 ReadFile & 向服务器发送数据 WriteFile
4. 关闭管道 CloseHandle

五、服务器

首先是服务器的代码:

#include <tchar.h>
#include "windows.h"
#include <iostream>

using namespace std;

#define BUF_SIZE 1024
// 定义管道名 , 如果是跨网络通信 , 则在圆点处指定服务器端程序所在的主机名
#define PIPE_NAME   L"\\\\.\\pipe\\pipeTest"   

int main()
{
	char buf[BUF_SIZE] = "";
	DWORD rLen = 0;
	DWORD wLen = 0;

	// 1.创建命名管道
	HANDLE hPipe = NULL;
	hPipe = CreateNamedPipe(
		PIPE_NAME,											    // 管道名  
		PIPE_ACCESS_DUPLEX,										// 管道类型,双向通信  
		PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,  // 管道参数  
		PIPE_UNLIMITED_INSTANCES,								// 管道能创建的最大实例数量  
		0,														// 输出缓冲区长度 0表示默认  
		0,														// 输入缓冲区长度 0表示默认  
		NMPWAIT_WAIT_FOREVER,									// 超时时间,NMPWAIT_WAIT_FOREVER为不限时等待
		NULL);													// 指定一个SECURITY_ATTRIBUTES结构,或者传递零值.
	// 判断命名管道是否创建成功
	if (INVALID_HANDLE_VALUE == hPipe)
		cout << "创建管道失败: " << GetLastError() << endl;
	else // 创建成功
	{
		cout << "这是命名管道测试程序中的服务器端" << endl;

		// 2.阻塞等待客户端连接
		cout << "现在等待客户端连接..." << endl;
		if (!ConnectNamedPipe(hPipe, NULL))						
		{
			cout << "连接失败!" << endl;
			return 1;
		}
		else
			cout << "连接成功!" << endl;

		// 3.阻塞等待接受客户端发送数据
		if (!ReadFile(hPipe, buf, BUF_SIZE, &rLen, NULL))
		{
			cout << "从客户端接收并读取数据!" << endl;
			return 2;
		}
		else
			cout << "客户端接收的数据为 : " << buf << endl << "数据长度为 " << rLen << endl;

		// & 3.向客户端发送数据
		char strMessage[] = "命名管道测试程序";
		WriteFile(hPipe, strMessage, sizeof(strMessage), &wLen, 0); 

		// 4. 关闭管道句柄 
		CloseHandle(hPipe);									
	}

	system("pause");
	return 0;
}

我们介绍一下上面几个重要函数:

CreateNamedPipe

// 创建命名管道的函数的使用s
CreateNamedPipe("\\\\.\\Pipe\\Test",PIPE_ACCESS_DUPLEX,PIPE_NOWAIT,10,1024,1024,100,NULL)
1、为创建的管道命名
2、指定管道的访问方式、重叠方式、写直通方式以及管道句柄的安全访问方式(PIPE_ACCESS_DUPLEX这里指双向模式)
3、指定管道句柄的类型、读取和等待方式(PIPE_NOWAIT指允许非阻塞方式)
4、指定管道能够创建的实例的最大数目
5、指定为输出缓冲区所保留的字节数
6、指定为输入缓冲区所保留的字节数
7、指定默认超时时间,单位ms,同一管道的不同实例指定值需要相同
8、指向SECURITY_ATTRIBUTES结构的指针,该结构指定了命名管道的安全描述符

ConnectNamedPipe

// 服务器等待客户端的连接请求的到来(并非连接服务器端的命名管道!)
ConnectNamedPipe(hPipe, NULL)	
1、指向一个命名管道实例的服务器的句柄,该句柄由CreateNamedPipe函数返回
2、指向OVERLAPPED结构体的指针,默认为NULL,表明使用默认的同步IO方式

ReadFile & WriteFile

// 文件的写入(向客户端发送数据)
WriteFile(hPipe, strMessage, sizeof(strMessage), &wLen, 0)
1、指定要写入数据的文件的句柄
2、指向包含将要将要写入文件的数据的缓冲区的指针
3、指明要向文件中写入的字节数
4、用来接收实际写入到文件中的字节数
5、指向OVERLAPPED结构体的指针,默认为NULL,表明使用默认的同步IO方式
 
// 文件的读取(接收客户端发送数据)
ReadFile(hPipe, buf, BUF_SIZE, &rLen, NULL)
1、指定要读取数据的文件的句柄
2、指向包含将要将要接收的文件中读取数据的缓冲区的指针
3、指明要向文件中读取的字节数
4、用来接收实际读取到的字节数
5、指向OVERLAPPED结构体的指针,默认为NULL,表明使用默认的同步IO方式

ReadFile & WriteFile

CloseHandle(hPipe);											// 关闭管道句柄 

六、客户端

客户端的代码:

#include <tchar.h>
#include "windows.h"
#include <iostream>
using namespace std;

#define BUF_SIZE 1024
// 定义管道名 , 如果是跨网络通信 , 则在圆点处指定服务器端程序所在的主机名
#define PIPE_NAME   L"\\\\.\\pipe\\pipeTest"

int main()
{
	char buf[BUF_SIZE] = "";
	DWORD rLen = 0;
	DWORD wLen = 0;

	cout << "这是命名管道测试程序的客户端" << endl;

	// 1.等待管道创建成功  
	if (!WaitNamedPipe(PIPE_NAME, NMPWAIT_WAIT_FOREVER))
	{
		cout << "connect the namedPipe failed!" << endl;
		return 1;
	}

	// 2.创建管道文件,即打开管道 
	HANDLE hPipe = CreateFile(           
		PIPE_NAME,						// 管道名称  
		GENERIC_READ | GENERIC_WRITE,   // 文件模式  
		0,                              // 是否共享  
		NULL,                           // 指向一个SECURITY_ATTRIBUTES结构的指针  
		OPEN_EXISTING,                  // 创建参数  
		FILE_ATTRIBUTE_NORMAL,          // 文件属性,NORMAL为默认属性  
		NULL);                          // 模板创建文件的句柄  
	// 判断命名管道是否打开成功
	if (INVALID_HANDLE_VALUE == hPipe)
	{
		cout << "打开通道失败!" << endl;
		return 2;
	}

	// 3.向服务器发送数据
	char strMessage[] = "命名管道测试程序";
	if (!WriteFile(hPipe, strMessage, sizeof(strMessage), &wLen, 0))   
	{
		cout << "向通道写数据失败!" << endl;
		return 3;
	}

	// & 3.阻塞等待接受服务器发送数据
	if (!ReadFile(hPipe, buf, BUF_SIZE, &rLen, NULL))
	{
		cout << "从通道读数据失败!" << endl;
		return 4;
	}
	else
		cout << "从服务器端接收数据 : " << buf << endl << "数据长度为:" << rLen << endl;
	Sleep(1000); // 执行挂起一段时间

	// 4. 关闭管道句柄 
	CloseHandle(hPipe); 

	system("pause");
	return 0;
}

客户端的程序与服务器端相同的函数就不再介绍了,这里只说一下客户端单独用到的两个函数:

WaitNamedPipe

WaitNamedPipe(TEXT("\\\\.\\Pipe\\pipeTest"), NMPWAIT_WAIT_FOREVER)
1、指定命名管道的名称
2、指定超时间隔,NMPWAIT_WAIT_FOREVER表示一直等待,直到出现了一个可用的命名管道的实例

CreateFile

CreateFile("\\\\.\\Pipe\\Test",GENERIC_WRITE,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
1、指定用于创建或打开的对象的名称
2、指定对象的访问方式,GENERIC_WRITE表示指定对象具有写访问
3、指定共享方式,如果此参数为0,表示对象不能被共享
4、指向SECURITY_ATTRIBUTES结构的指针,该结构指定了命名管道的安全描述符,如果没有特殊的需求,默认值为NULL
5、指定如何创建文件(OPEN_EXISTING表示打开文件,如果文件不存在,则函数调用失败)
6、设置文件属性和标志(FILE_ATTRIBUTE_NORMAL表示该文件没有其他属性设置)
7、指定具有GENERIC_READ访问方式的模板文件的句柄

七、服务器与客户端的详细实现流程

服务端:

  • 服务端进程调用 CreateNamedPipe 函数来创建一个有名称的命名管道,在创建命名管道的时候必须指定一个本地的命名管道名称(不然就不叫命名管道了),Windows 允许同一个本地的命名管道名称有多个命名管道实例,所以,服务器进程在调用 CreateNamedPipe 函数时必须指定最大允许的实例数(0 -255)。

  • 如果 CreateNamedPipe 函数成功返回后,服务器进程得到一个指向一个命名管道实例的句柄,然后,服务器进程就可以调用 ConnectNamedPipe 来等待客户的连接请求。

  • 这个 ConnectNamedPipe 既支持同步形式,又支持异步形式,若服务器进程以同步形式调用 ConnectNamedPipe 函数,(同步方式也就是如果没有得到客户端的连接请求,则会一直等到)那么,当该函数返回时,客户端与服务器之间的命名管道连接也就已经建立起来了。

  • 在已经建立了连接的命名管道实例中,服务端进程就会得到一个指向该管道实例的句柄,这个句柄称之为服务端句柄。

  • 此时服务端进程可以使用 ReadFile 接受来自客户端的数据,或者使用 WriteFile 向客户端发送数据。

  • 同时,服务端进程可以调用 DisconnectNamedPipe 函数,将一个管道实例与当前建立连接的客户端进程断开,从而可以重新连接到新的客户进程。当然在服务端也是可以调用 CloseHandle 来关闭一个已经建立连接的命名管道实例。

客户端

  • 客户端进程调用 CreateFile 函数连接到一个正在等待连接的命名管道上,在这里客户端需要指定将要连接的命名管道的名称,当 CreateFile 成功返回后,客户进程就得到了一个指向已经建立连接的命名管道实例的句柄,到这里,服务器进程的 ConnectNamedPipe 也就完成了其建立连接的任务。

  • 客户端进程除了调用 CreateFile 函数来建立管道连接以外,还可以调用 WaitNamedPipe 函数来测试指定名称的管道实例是否可用。在已经建立了连接的命名管道实例中,客户端进程就会得到一个指向该管道实例的句柄,这个句柄称之为客户端句柄。

  • 此时客户端进程也可以使用 WriteFile 向服务端发送数据,或者使用 ReadFile 接受来自服务端的数据。

  • 在客户端可以调用 CloseHandle 来关闭一个已经建立连接的命名管道实例。


八、扩展:cmd列出计算机内所有的命名管道

在 powershell3 以上的版本中,我们可以使用:

[System.IO.Directory]::GetFiles("\\.\\pipe\\")

来查看本机上所有的存在的命名管道,或者使用微软提供的专业任务资源管理器:process explorer 来进行查看,其图文介绍可参考:Process Explorer使用图文教程

可以利用有名管道来绕过防火墙,具体请参考:windows安全初探之命名管道


参考:

C++命名管道详解及简单案例(基于VS2013)

进程间通信 - 命名管道实现


posted @ 2021-03-24 21:00  fengMisaka  阅读(353)  评论(0编辑  收藏  举报