65.9K
CodeProject 正在变化。 阅读更多。
Home

引入基于 Windows 消息的 IPC 组件

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.55/5 (3投票s)

2019年8月12日

MIT

4分钟阅读

viewsIcon

7581

downloadIcon

201

基于Windows消息的简单而可靠的IPC模块,由于Windows的限制,最多支持19轮递归调用。

select mode

server

client

引言

在大多数情况下,我们使用管道进行IPC。 使用管道时,通常会使用额外的线程来监视数据的到达,这会导致UI线程和监视线程之间的同步工作。 此外,使用管道,递归调用将变得出乎意料地复杂。

在这里,我提出了一个新的IPC模块,我将其命名为SIPC。 使用SIPC,可以轻松地进行IPC调用,就像两个进程在同一线程中一样。

背景

我深入研究了构建自己的输入法编辑器(IME)。 我将IME分为两个部分:IME接口和UI模块。 IME接口由系统加载并在宿主进程中运行。 UI模块是一个单独的可执行模块。 为了尽可能轻松地连接这两个模块,我设计了SIPC。

Using the Code

构建演示完成后,运行该演示,系统会要求您选择运行模式。 选择以服务器身份运行,选择以客户端身份运行。 请注意,服务器必须首先运行。 服务器和客户端都有UI。 在服务器UI中,日志窗口记录客户端信息。 在客户端UI中,可以从UI调用三个IPC函数。“add int”和“add string”演示了intstring参数类型如何在演示中工作。“sum”演示了如何进行递归调用。

该项目的基本思想是使用Windows消息在两个进程之间进行通信。 使用Windows消息,可以调用IPC函数,而无需担心线程同步问题。 尽管WM_COPYDATA可以在大多数情况下做到这一点,但在某种程度上使用WM_COPYDATA会更复杂。 例如,必须手动序列化输入参数,并且只能接收整数返回值。 实际上,可以通过将参数包装到一个类中来简化序列化和反序列化过程。

为了自动序列化和反序列化参数,这里使用了三个类

	struct IShareBuffer {
		enum SEEK {
			seek_set= 0,            /* seek to an absolute position */
			seek_cur,               /* seek relative to current position */
			seek_end                /* seek relative to end of file */
		};
		virtual int Write(const void * data, UINT nLen) = 0;
		virtual int Read(void * buf, UINT nLen) = 0;
		virtual UINT Tell() const = 0;
		virtual UINT Seek(SEEK mode, int nOffset) = 0;
		virtual void SetTail(UINT uPos) = 0;
	};

	class SParamStream
	{
	public:
		SParamStream(IShareBuffer *pBuf) :m_pBuffer(pBuf)
		{
		}

		IShareBuffer * GetBuffer() {
			return m_pBuffer;
		}

		template<typename T>
		SParamStream & operator<<(const T & data)
		{
			Write((const void*)&data, sizeof(data));
			return *this;
		}

		template<typename T>
		SParamStream & operator >> (T &data)
		{
			Read((void*)&data, sizeof(data));
			return *this;
		}

	public:
		int Write(const void * data, int nLen)
		{
			return m_pBuffer->Write(data, nLen);
		}
		int Read(void * buf, int nLen) 
		{
			return m_pBuffer->Read(buf, nLen);
		}

	protected:
		IShareBuffer * m_pBuffer;
	};

	struct  IFunParams
	{
		virtual UINT GetID() = 0;
		virtual void ToStream4Input(SParamStream &  ps) = 0;
		virtual void ToStream4Output(SParamStream &  ps) = 0;
		virtual void FromStream4Input(SParamStream &  ps) = 0;
		virtual void FromStream4Output(SParamStream &  ps) = 0;
	};

IShareBuffer用于包装共享内存支持。 SParamStream用于将任何类型的参数写入或读取到共享内存。 IFunParams是一个接口,用于包装IPC调用的实际参数。

此外,我们设计了一组辅助宏,以简化IFunParams接口的实现。

#pragma once

#define FUNID(id) \
enum{FUN_ID=id};\
UINT GetID() {return FUN_ID;}

#define FUN_BEGIN \
bool HandleFun(UINT uMsg, SOUI::SParamStream &ps){ \
	bool bHandled = false; \

#define FUN_HANDLER(x,fun) \
	if(!bHandled && uMsg == x::FUN_ID) \
	{\
		x param; \
		GetIpcHandle()->FromStream4Input(¶m,ps.GetBuffer());\
		ps.GetBuffer()->Seek(SOUI::IShareBuffer::seek_cur,sizeof(int));\
		fun(param); \
		GetIpcHandle()->ToStream4Output(¶m,ps.GetBuffer());\
		bHandled = true;\
	}

#define FUN_END \
	return bHandled; \
}

#define CHAIN_MSG_MAP_2_IPC(ipc) \
		if(ipc)\
		{\
			BOOL bHandled = FALSE;\
			lResult = (ipc)->OnMessage((ULONG_PTR)hWnd,uMsg,wParam,lParam,bHandled);\
			if(bHandled)\
			{\
				return true;\
			}\
		}

/////////////////////////////////////////////////////////////////////
template<typename P1>
void toParamStream(SOUI::SParamStream &  ps, P1 &p1)
{
	ps << p1;
}
template<typename P1>
void fromParamStream(SOUI::SParamStream &  ps, P1 & p1)
{
	ps >> p1;
}

#define PARAMS1(type,p1) \
void ToStream4##type(SOUI::SParamStream &  ps){ toParamStream(ps,p1);}\
void FromStream4##type(SOUI::SParamStream &  ps){fromParamStream(ps,p1);}\

/////////////////////////////////////////////////////////////
template<typename P1, typename P2>
void toParamStream(SOUI::SParamStream &  ps, P1 &p1, P2 & p2)
{
	ps << p1 << p2;
}
template<typename P1, typename P2>
void fromParamStream(SOUI::SParamStream &  ps, P1 & p1, P2 &p2)
{
	ps >> p1 >> p2;
}

#define PARAMS2(type,p1,p2) \
void ToStream4##type(SOUI::SParamStream &  ps){ toParamStream(ps,p1,p2);}\
void FromStream4##type(SOUI::SParamStream &  ps){fromParamStream(ps,p1,p2);}\

////////////////////////////////////////////////////////////////////
template<typename P1, typename P2, typename P3>
void toParamStream(SOUI::SParamStream &  ps, P1 &p1, P2 & p2, P3 & p3)
{
	ps << p1 << p2 << p3;
}
template<typename P1, typename P2, typename P3>
void fromParamStream(SOUI::SParamStream &  ps, P1 & p1, P2 &p2, P3 & p3)
{
	ps >> p1 >> p2 >> p3;
}

#define PARAMS3(type,p1,p2,p3) \
void ToStream4##type(SOUI::SParamStream &  ps){ toParamStream(ps,p1,p2,p3);}\
void FromStream4##type(SOUI::SParamStream &  ps){fromParamStream(ps,p1,p2,p3);}\

///////////////////////////////////////////////////////////////////
template<typename P1, typename P2, typename P3, typename P4>
void toParamStream(SOUI::SParamStream &  ps, P1 &p1, P2 & p2, P3 & p3, P4 & p4)
{
	ps << p1 << p2 << p3<<p4;
}
template<typename P1, typename P2, typename P3, typename P4>
void fromParamStream(SOUI::SParamStream &  ps, P1 & p1, P2 &p2, P3 & p3, P4 & p4)
{
	ps >> p1 >> p2 >> p3>>p4;
}

#define PARAMS4(type,p1,p2,p3,p4) \
void ToStream4##type(SOUI::SParamStream &  ps){ toParamStream(ps,p1,p2,p3,p4);}\
void FromStream4##type(SOUI::SParamStream &  ps){fromParamStream(ps,p1,p2,p3,p4);}\

/////////////////////////////////////////////////////////////////////////
template<typename P1, typename P2, typename P3, typename P4, typename P5>
void toParamStream(SOUI::SParamStream &  ps, P1 &p1, P2 & p2, P3 & p3, P4 & p4, P5 &p5)
{
	ps << p1 << p2 << p3 << p4 <<p5;
}
template<typename P1, typename P2, typename P3, typename P4, typename P5>
void fromParamStream(SOUI::SParamStream &  ps, P1 & p1, P2 &p2, P3 & p3, P4 & p4, P5 &p5)
{
	ps >> p1 >> p2 >> p3 >> p4>>p5;
}

#define PARAMS5(type,p1,p2,p3,p4,p5) \
void ToStream4##type(SOUI::SParamStream &  ps){ toParamStream(ps,p1,p2,p3,p4,p5);}\
void FromStream4##type(SOUI::SParamStream &  ps){fromParamStream(ps,p1,p2,p3,p4,p5);}\

如您所见,以上助手最多可以支持5个输入和输出参数。

使用宏,要定义一个IPC调用,例如int add(int a, int b),可以通过定义一个类Param_AddInt来完成序列化和反序列化,该类看起来像

struct Param_AddInt : FunParams_Base
{
	int a, b;
	int ret;
	FUNID(CID_AddInt)
		PARAMS2(Input, a,b)
		PARAMS1(Output,ret)
};

FUNID(CID_AddInt)定义IPC调用ID。 PARAMS2(Input, a,b)定义如何将ab序列化到共享内存,PARAMS1(Output,ret)定义如何反序列化返回值。

要调用IntAdd IPC,伪代码可能如下所示

int CClientConnect::Add(int a, int b)
{
	Param_AddInt params;
	params.a = a;
	params.b = b;
	m_ipcHandle->CallFun(¶ms);
	return params.ret;
}

现在,让我们解释一下此演示的工作方式。 在这里,让我们关注服务器端点如何响应IPC调用。

首先,我们定义了Param_AddInt。 在向服务器发送消息之前,我们将Param_AddInt序列化到共享内存。

然后,我们使用SendMessage向服务器发送消息。 服务器收到消息后,它读取函数ID和参数,然后调用响应过程并将输出参数写回共享缓冲区并返回。

客户端收到服务器的返回后,客户端从共享缓冲区读取输出参数。 这样就完成了一个IPC调用。

	LRESULT SIpcHandle::OnMessage
    (ULONG_PTR idLocal, UINT uMsg, WPARAM wp, LPARAM lp, BOOL &bHandled)
	{
		bHandled = FALSE;
		if ((HWND)idLocal != m_hLocalId)
			return 0;
		if (UM_CALL_FUN != uMsg)
			return 0;
		bHandled = TRUE;
		IShareBuffer *pBuf = GetRecvBuffer();
		assert(pBuf->Tell()>= 4); //4=sizeof(int)
		pBuf->Seek(IShareBuffer::seek_cur,-4);
		int nLen=0;
		pBuf->Read(&nLen, 4);
		assert(pBuf->Tell()>=(UINT)(nLen+ 4));
		pBuf->Seek(IShareBuffer::seek_cur,-(nLen+ 4));
		int nCallSeq = 0;
		pBuf->Read(&nCallSeq,4);
		UINT uFunId = 0;
		pBuf->Read(&uFunId,4);
		SParamStream ps(pBuf);

		bool bReqHandled = m_pConn->HandleFun(uFunId, ps);
		return  bReqHandled?1:0;
	}

收到IPC调用请求后,SIpcHandler调用IConnection::HandleFun(uFunId,ps)。 在辅助宏的帮助下,HandleFun函数可以分解为一组宏,并将不同的调用映射到不同的处理函数。 例如

	void OnAddInt(Param_AddInt & param);
	void OnAddStr(Param_AddString & param);
	void OnSum(Param_Sum & param);
	FUN_BEGIN
		FUN_HANDLER(Param_AddInt, OnAddInt)
		FUN_HANDLER(Param_AddString, OnAddStr)
		FUN_HANDLER(Param_Sum,OnSum)
	FUN_END

FUN_BEGINFUN_END是函数HandleFun的主体,FUN_HANDLER将由其第一个参数标识的IPC调用映射到其第二个参数。

请注意,在尝试发送IPC请求之前,在消息队列中,可能存在来自另一端的一些待处理请求正在等待处理。 为了确保所有IPC调用都按顺序处理,IIpcHande::CallFunc将如下所示

	bool SIpcHandle::CallFun(IFunParams * pParam) const
	{
		if (m_hRemoteId == NULL)
			return false;

		//pay attention, here we need to make sure msg queue empty.
		MSG msg;
		while(::PeekMessage(&msg, NULL, UM_CALL_FUN, UM_CALL_FUN, PM_REMOVE))
		{
			if(msg.message == WM_QUIT)
			{
				PostQuitMessage(msg.wParam);
				return false;
			}
			DispatchMessage(&msg);
		}

		int nCallSeq = m_uCallSeq ++;
		if(m_uCallSeq>100000) m_uCallSeq=0;

		IShareBuffer *pBuf = &m_sendBuf;
		DWORD dwPos = pBuf->Tell();
		pBuf->Write(&nCallSeq,4);             //write call seq first.
		UINT uFunId = pParam->GetID();
		pBuf->Write(&uFunId,4);
		if(!ToStream4Input(pParam, pBuf))
		{
			pBuf->Seek(IShareBuffer::seek_set, dwPos);
			m_sendBuf.SetTail(dwPos);
			assert(false);
			return false;
		}
		int nLen = m_sendBuf.Tell()-dwPos;
		m_sendBuf.Write(&nLen,sizeof(int));//write a length of params to stream, 
                                           //which will be used to locate param header
		LRESULT lRet = SendMessage(m_hRemoteId, UM_CALL_FUN, pParam->GetID(), 
                                  (LPARAM)m_hLocalId);
		if (lRet != 0)
		{
			m_sendBuf.Seek(IShareBuffer::seek_set,
               dwPos+nLen+sizeof(int));    //output param must follow input params
			BOOL bRet = FromStream4Output(pParam,&m_sendBuf);
			assert(bRet);
		}
		//clear params.
		m_sendBuf.Seek(IShareBuffer::seek_set, dwPos);
		m_sendBuf.SetTail(dwPos);

		return lRet!=0;
	}	

以上提到的都是关键点。 借助SIPC,可以轻松进行IPC调用,而无需考虑线程同步问题等。

实际上,SIPC是来自我的另一个开源项目SOUI的组件之一。 SOUI是一个直接的UI框架,它借鉴了包括WTL,QT,flash,Android,Chrome和CEGUI等在内的思想。 可以从https://github.com/soui3/soui克隆源代码。 如果您对SOUI感兴趣,请随时与我联系。

关注点

  1. 通过使用宏构造IFunParams,SIPC调用会自动执行参数序列化。
  2. 通过使用宏构造HandleFun,SIPC调用会自动将来自参数类型的IPC调用映射到处理函数。
  3. 所有IPC调用都在同一线程中完成。

历史

  • 1.0 2019.8.12: 初始版本
© . All rights reserved.