进程间命名管道通信小例程

背景

公司是做自动化设备的,PLC和其他一些Modbus、Ethernet设备组成下位机。上位机方面,我们基于每一个物理设备写一个win32服务程序,将各种设备接口格式统一,再由一个负责流程控制与命令分发的调度程序来操控所有的服务进程,这里面就涉及到进程间通信。进程间通信有7种,我目前只对管道和socket有所了解,这里就简单说说管道。

相关知识

本篇内包括两方面,一个是ATL服务的写法,另一个是管道的写法。前者是一个模板库,后者是直接基于win32-api。这两方面,微软官方文档讲得挺不错的:

ATL服务:https://docs.microsoft.com/zh-cn/cpp/atl/reference/catlservicemodulet-class
命名管道:https://docs.microsoft.com/en-us/windows/win32/ipc/pipe-functions

服务端程序

服务端程序基于ATL服务类开发,只重写了PreMessageLoop这一个方法,目的就是创建一个命名管道,然后读取客户端程序发来的字符串并打印到文件。下面是程序主体:

class CATLServiceRepoModule : public ATL::CAtlServiceModuleT< CATLServiceRepoModule, IDS_SERVICENAME >
{
public:
	DECLARE_LIBID(LIBID_ATLServiceRepoLib)
	DECLARE_REGISTRY_APPID_RESOURCEID(IDR_ATLSERVICEREPO, "{BB68417D-AAA5-45B6-8EAC-2CAE3662CB46}")
	HRESULT InitializeSecurity() throw()
	{
		// TODO : 调用 CoInitializeSecurity 并为服务提供适当的安全设置
		// 建议 - PKT 级别的身份验证、
		// RPC_C_IMP_LEVEL_IDENTIFY 的模拟级别
		// 以及适当的非 NULL 安全描述符。

		return S_OK;
	}

	HRESULT PreMessageLoop(int nShowCmd) throw()
	{
		HRESULT hr = CAtlServiceModuleT::PreMessageLoop(nShowCmd);
		th = thread(&CATLServiceRepoModule::OnServiceStart, this);
		SetServiceStatus(SERVICE_RUNNING);
		return hr;
	}

	void OnServiceStart()
	{
		//Sleep(10 * 1000);
		ofstream file_out;
		char PathName[256];
		char *FileName = "test.txt";
		GetModuleFileNameA(NULL, (LPSTR)PathName, sizeof(PathName));
		for (int i = sizeof(PathName) - 1; i >= 0; i--)
		{
			if (PathName[i] == '\\')
			{
				for (int j = 0; j < sizeof(FileName); j++)
				{
					PathName[i + j + 1] = FileName[j];
				}
				break;
			}
			PathName[i] = '\0';
		}

		HANDLE hPipe = CreateNamedPipe(
			TEXT("\\\\.\\Pipe\\mypipe"),
			PIPE_ACCESS_DUPLEX,
			PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
			PIPE_UNLIMITED_INSTANCES,
			0,
			0,
			NMPWAIT_WAIT_FOREVER,
			NULL);
		ConnectNamedPipe(hPipe, NULL);

		char buffer[256];
		DWORD rlen;

		memset(buffer, 0, 256);
		while (true)
		{
			if (ReadFile(hPipe, buffer, 256, &rlen, NULL) && rlen > 0)
			{
				file_out.open(PathName, ios_base::app);
				if (!file_out.is_open())
				{
					break;
				}
				file_out << buffer << endl;
				file_out.close();
				memset(buffer, 0, 256);
			}
			else
			{
				DisconnectNamedPipe(hPipe);
				ConnectNamedPipe(hPipe, NULL);
			}
		}
		DisconnectNamedPipe(hPipe);
		CloseHandle(hPipe);
	}

private:
	thread th;
};
  • 这里只贴出了自定义类CATLServiceRepoModule的内容,其他都是自动创建的,没必要贴。
  • ATL服务启动后会依次调用PreMessageLoop RunMessageLoop PostMessageLoop。如果想加点自己的东西,可以重写前两个方法,并在进入消息循环之前添加。本篇重写了第一个方法,开了一个线程,线程内创建命名管道来等待消息。
  • 使用GetModuleFileNameA来获取当前路径,并在同一路径创建test.txt文件来写内容。
  • CreateNamedPipe函数配置了管道参数并返回句柄。
  • 需要注意的是ConnectNamedPipe函数,如果采用堵塞模式,他会等待WaitNamedPipe的连接。
  • ReadFile也可以是堵塞的,直到管道内出现写入的数据。当然,如果没有任何客户端连接到管道,他会立即返回失败(零值),可以用它做标志来重启管道(GetNamedPipeHandleState看起来可以获取状态,但他获取的应该是保存在内部的状态而不是真实状态,因为即使客户端断开连接,他显示的连接数也是1,所以不能用它做标志。)

客户端程序

直接贴出代码:

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

int main(int argc, char *argv[])
{
	char buffer[256];
	memset(buffer, 0, sizeof(buffer));
	DWORD wlen = 0;

	if (WaitNamedPipe(L"\\\\.\\Pipe\\mypipe", NMPWAIT_WAIT_FOREVER))
	{
		HANDLE hPipe = CreateFile(
			L"\\\\.\\Pipe\\mypipe",
			GENERIC_READ | GENERIC_WRITE,
			0,
			NULL,
			OPEN_EXISTING,
			FILE_ATTRIBUTE_NORMAL,
			NULL);
		do 
		{
			printf_s("please enter a string(Ctrl+D to quit): ");
			scanf_s("%s", buffer, sizeof(buffer));
			if (buffer[0] == 4) { break; }
			if (WriteFile(hPipe, buffer, sizeof(buffer), &wlen, 0))
			{
				printf("write to pipe success!\n");
			}
			else
			{
				printf("write to pipe failed!\n");
				break;
			}
		} while (true);
		
		CloseHandle(hPipe);
	}
	system("pause");
}
  • 这里用WaitNamedPipe连接管道,用CreateFile创建读写句柄,程序启动后会循环读取,直到读到了Ctrl+D的输入值(调试时发现是4,就用了它)。

效果图

image

posted @ 2022-02-24 00:46  mrzono  阅读(79)  评论(0编辑  收藏  举报