管道模板类
一个模板类库,以最小的程序员工作量来支持管道开发。
1. 引言
这个管道模板库的编写目的是为了最大程度地减少程序员在创建管道服务器和客户端时的工作量。使用这个库,您只需要创建数据类并实现相应数据类的服务器端函数,就可以得到一个工作的管道客户端和服务器。这个项目的第二个优先任务是收集关于管道错误和关键情况的所有信息。当然,您也可以将管道库代码作为Windows下管道的技术文档使用。
这个库是为MSVC 5.0编写的,但它也适用于MSVC的后续版本。如果您在后续版本的MSVC中运行此库,最好使用ATL的atlsecurity.h文件,而不是本库自带的文件,因为本软件包中的security.h文件是为了与MSVC 5.0编译器协同工作而重写的。这个建议仅适用于服务器端使用DACL来设置管道安全性的代码。任何DACL包装类的实现都可以用于此目的。
2. 背景
该库使用了Alexandrescu引入的类型列表(有关更多信息,请参阅他的著作《Modern C++ Design》,或直接查看文件sources\utils\typelist.h)。类型列表的typedef
在服务器基类的声明中使用。
3. 使用库
3.1. 快速入门
3.1.1. 功能
本库支持传输以下消息:
客户端:
- 创建管道客户端;
- 打开它;
- 测试它,确定它是否已连接到服务器端;
- 创建数据对象并发送给服务器;
- 测试到服务器的连接,并在需要时重新连接;
- 关闭客户端管道。
服务器端:
- 创建服务器对象;
- 启动服务器对象(创建工作线程时,等待管道客户端注册,该客户端又为每个客户端通信通道创建一个工作线程);
- 创建通信通道后,开始从客户端传输数据;
- 来自每个客户端的数据被组合成消息;
- 服务器决定接收到哪条消息,并调用服务器相应的函数;
- 通信持续进行直到服务器停止。
3.1.2. 消息
数据传输的单元是消息。
消息是:
- 一个从
CPipeDataBaseImpl
类模板派生的类的对象; - 一个声明了GUID,用于标识该类在服务器处理的所有其他消息中的唯一性;
- 实现了用于保存/加载数据的虚函数。
让我们看一下消息的类声明。以测试服务器中的声明为例(test_server\Messages.h文件)
//first of all declare GUID
//{E67A8D9E-EEB7-42d5-A1F9-25BDEE214A08}
static const GUID PipeMsgGUID_State =
{0xe67a8d9e, 0xeeb7, 0x42d5, { 0xa1, 0xf9, 0x25, 0xbd, 0xee, 0x21, 0x4a, 0x8 }};
/*
state information to registry/unregistry clients on server and verify there activity
*/
struct CPipeMsg_State : public CPipeDataBaseImpl<CPipeMsg_State>
{
enum StateEn {S_Null=0,S_Started,S_Running,S_Stoped};
CPipeMsg_State(const CString& _sName = _T(""),StateEn _state = S_Null)
:m_sName(_sName),m_state(_state)
{
}
DECLARE_PIPEDATAGUID(PipeMsgGUID_State);
virtual bool save(IPipeCommunicator* _ppipe) const
{
VERIFY_EXIT1(NOT_NULL(_ppipe),false);
if(!_ppipe->write(m_sName)) return false;
if(!_ppipe->write((long)m_state)) return false;
return true;
}
virtual bool load(const IPipeCommunicator* _ppipe)
{
VERIFY_EXIT1(NOT_NULL(_ppipe),false);
long nstate = 0;
if(!_ppipe->read(m_sName)) return false;
if(!_ppipe->read(nstate)) return false;
m_state = (StateEn)nstate;
return true;
}
const CString& get_ClientName() const {return m_sName;}
StateEn get_state() const {return m_state;}
protected:
CString m_sName; // name of the client
StateEn m_state; // state inforamtion
};//struct CPipeMsg_State
每种消息类型都在其自己的类中声明。在这种情况下,您可以看到一个用于传输状态信息的类(CPipeMsg_State
)。如您在代码片段中看到的,数据类派生自CPipeDataBaseImpl
模板,该模板接收创建的类作为模板参数。此外,您应该使用宏DECLARE_PIPEDATAGUID
为消息类设置GUID
。此外,代码片段还演示了如何实现虚函数save()
和load()
,以便将声明类的元素保存到管道或从管道加载。
在save()
和load()
函数中实现与此处所示完全相同的调用顺序以将数据写入或从管道读取至关重要。并且在两个函数中保持相同的顺序也很重要。
IPipeCommunicator
接口提供了某些基类数据类型的实现,这些数据类型用于将类元素写入管道或从管道读取。您应该使用此接口(基类实现)的函数来保存和加载您的消息类数据。
从架构角度来看,消息类描述了要保存到管道以及从管道加载的数据,以及在操作管道时如何保存和加载数据。在本库的管道通道的客户端和服务器端都使用了消息类。因此,消息声明文件应该包含在管道实现的两个部分中。
3.1.3. 客户端
让我们看一下本库的客户端(项目test_client的文件test_client\test_client.cpp)。
客户端只需创建一个CClientPipeComm
类的对象,并使用它来向服务器传输数据。如果我们忽略实现通信接口的细节,管道的本质可以用几行简单的代码来表达:
CPipeMsg_Text txt(sName,str);
pipe.save(&txt);
bFail = pipe.nead_toReconnect();
和
CPipeMsg_State si(_sName,_state);
_pipe.save(&si);
//...
bFail = pipe.nead_toReconnect();
如您所见,要将一些数据发送到服务器端,您只需创建一个消息类对象,然后调用CClientPipeComm
类对象的save()
方法。此函数将返回true
或false
来指示操作是否成功。此外,如果管道状态发生变化(即与服务器断开连接),函数nead_toReconnect()
将返回true
,表示您需要重新连接才能发送数据。
CClientPipeComm
类描述
bool open(const CString& _sServerName
,const CString& _sPipeName
,DWORD _dwDesiredAccess = GENERIC_WRITE
,DWORD _nWaitTime = NMPWAIT_USE_DEFAULT_WAIT
,bool _bWriteThroughMode = false
,DWORD _dwPipeMode = PIPE_READMODE_BYTE
,LPSECURITY_ATTRIBUTES _psa = NULL
);
此函数在客户端打开管道。
参数
_sServerName
- 服务器启动的计算机名称,客户端连接到该服务器(如果是本地机器,则应将此参数设置为"."字符串值)。_sPipeName
-- 要打开的管道通道名称。_dwDesiredAccess
- 对于本库的当前版本,这应始终设置为GENERIC_WRITE
。_nWaitTime
- 设置管道打开的最大时间(如果服务器存在,但暂时无法响应)。_bWriteThroughMode
- 设置标志以使用“write through”模式处理写入操作,并等待服务器返回读取状态以保证数据传输(如果设置为true
),或者使用数据缓冲,此时写入函数在不等待数据传输的情况下返回)。_dwPipeMode
- 设置管道的模式(最好使用PIPE_READMODE_BYTE
)。_psa
- 设置此管道的安全属性。
有关详细描述和错误信息,请参阅文件pipe\namepipebase.h。其中定义了实现低级管道操作的CNamedPipeWrap
类(有关详细信息,请参阅open()
函数)。
void close()
此函数关闭客户端的管道。
operator bool() const
bool operator ! () const
以上是用于确定管道是否有效的运算符。如果连接成功创建,则管道有效。
bool save(IPipeDataBase* _data)
将数据发送到服务器。如果操作失败,客户端将尝试重新连接,然后重试发送数据一次。如果重连失败,则返回false
。如果数据发送成功,则返回true
。
bool nead_toReconnect() const
如果保存操作后需要重新连接以发送或重发数据,此函数返回true
。
bool reconnect_client(DWORD _nWaitTime = NMPWAIT_USE_DEFAULT_WAIT)
重新连接管道的客户端。调用此函数后,如果连接已恢复,则管道对象将变为有效;否则,它将无效。(您可以使用operator bool()
或bool operator!()
来测试管道。)
参数
_nWaitTime
- 设置重新连接的最大时间。
void async_reconnect_client(DWORD _nWaitTime = NMPWAIT_USE_DEFAULT_WAIT)
异步重新连接管道的客户端。创建工作线程尝试重新连接。
bool reconnecting() const
如果异步重新连接正在进行,则返回true
。如果调用void async_reconnect_client()
函数之前该函数返回false
,您可以测试管道是否已连接。
void flush()
刷新客户端管道的缓冲区。
客户端代码示例可以在test_client项目中找到。
3.1.4. 服务器端
服务器类创建
服务器示例可以在test_server项目中找到。
// declare type list of messages to receive by pipe server
typedef TYPELIST_2(CPipeMsg_State,CPipeMsg_Text) PipeServerMsgsList;
// declare pipe server class
struct CTestPipeServer : public CServerPipeCommImpl<CTestPipeServer,PipeServerMsgsList>
{
CTestPipeServer(const CString& _sPipeName,CStatistics& _stat)
:CServerPipeCommImpl<CTestPipeServer,PipeServerMsgsList>(_sPipeName)
,m_stat(_stat)
{
m_psa = &m_sa;
}
bool process(CPipeMsg_State* _pstate)
{
VERIFY_EXIT1(NOT_NULL(_pstate),false);
m_stat.on_state(_pstate->get_ClientName(),_pstate->get_state());
CAutoLock __al(g_output);
g_output << _T("\"")
<< _pstate->get_ClientName()
<< _T("\" state:")
<< _pstate->get_state();
g_output.endline();
return true;
}
bool process(CPipeMsg_Text* _ptext)
{
VERIFY_EXIT1(NOT_NULL(_ptext),false);
m_stat.on_text(_ptext->get_ClientName());
CAutoLock __al(g_output);
g_output << _T("\"")
<< _ptext->get_ClientName()
<< _T("\" :")
<< _ptext->get_Text();
g_output.endline();
return true;
}
CSecurityAttributes& get_security() {return m_sa;}
protected:
CSecurityAttributes m_sa;
CStatistics& m_stat;
};//struct CTestPipeServer
该示例显示了一个用于接收消息(CPipeMsg_State
和CPipeMsg_Text
)的管道通信器的服务器类。首先,如您所见,您需要创建一个typelist
类,作为所有接收消息的列表,将您的服务器类派生自CServerPipeCommImpl
,并将之前创建的typelist
类传递给它。然后,您只需要为每种消息类型实现相应的功能;换句话说,您需要实现process()
函数,并将相应的消息类型类作为参数。(在本例中,这些是bool process(CPipeMsg_State* _pstate)
和bool process(CPipeMsg_Text* _ptext)
函数。)这就足够创建一个管道服务器了,因为基类CServerPipeCommImpl
实现了服务器所需的所有其他功能:从管道通道读取数据,从数据块创建消息类,以及调用相应的函数来处理从客户端接收到的消息。
该项目还说明了服务器类的初始化和创建。让我们看一下相应的代码片段:
CStatistics g_stat; // statistics
//...
void run_server()
{
CTestPipeServer* pserver = new CTestPipeServer(_T("TestPipeLib"),g_stat);
//activate security
CSid everyone = security::Sids::World();
CSid owner = security::Sids::Self();
CDacl dacl;
dacl.AddDeniedAce(everyone,security::AccessRights_WriteDac);
dacl.AddAllowedAce(everyone,security::AccessRights_GenericWrite
|security::AccessRights_GenericRead);
dacl.AddAllowedAce(owner,security::AccessRights_GenericAll);
CSecurityDesc secrdesc;
secrdesc.SetDacl(dacl);
pserver->get_security().Set(secrdesc);
// run pipe server
pserver->start();
cout << _T("Type 'q' -- to exit and 'i' -- for inforamtion") << endl;
char ch = 0;
while((ch=toupper(getch()))!='Q')
{
if(ch=='I') g_stat.print_all();
}
pserver->stop();
delete pserver;
cout << _T("pipe server was stoped");
}
此函数展示了服务器类的创建、对创建的管道的访问权限初始化以及服务器的启动。从工作线程开始,将为相应消息调用process()
函数。该函数还演示了如何通过行pserver->stop();
来关闭管道通道。
4. 库的架构和内部描述
4.1. 类简述
class CAutoLock
代码片段的同步。
struct CClientPipeComm
管道客户端(向服务器发送消息)。
struct CNamedPipeWrap
Windows管道WinAPI的包装类。该类为客户端和服务器管道类提供了(与WinAPI协同工作的)函数。
template<typename _FromType,typename _ToType,long _bufsize = 1024*4>
struct convert_string
内存使用优化的字符串转换模板类。(该类使用堆栈上的内存块,或者如果字符串转换的内存大于堆栈上预分配的内存,则使用堆内存。)
struct COSVersion
封装系统信息的类,特别是Windows版本。
struct CPipeBufferChunk
单个管道数据块的数据缓冲区。
template<typename _Type>
struct CPipeCommunicatorImpl
实现了IPipeCommunicator
接口的模板类,用于向管道通道读写基本数据类型。
template<typename _PipeDataClass>
struct CPipeDataBaseImpl
实现通过管道通道传输消息的基本操作的模板类。
struct CPipeReadedBuffer
管道通道的数据缓冲区(用于接收数据)。
template<typename _Server,typename _TypeList>
struct CServerPipeCommImpl
实现管道服务器基础的类。
template<typename _Server,typename _TypeList>
struct CServerPipeReadThread
实现与具体客户端通过管道通道进行通信的类。process()
函数是从该类的线程调用的。
struct DateInfo
用于通过管道通道传输CTime
信息的辅助类。
template<typename _TypeList>
struct FindDataTypeAndProcess
template<>
struct FindDataTypeAndProcess<NullType>
辅助模板类,通过从管道通道读取GUID
来确定具体的类。它在服务器类加载具体消息信息之前使用。
interface IPipeCommunicator
实现基本数据类型操作的接口(用于从管道通道读写)。
然而,如果库需要使用更多基本类型,我将使用模板类而不是带虚函数的类。
interface IPipeDataBase
消息数据接口。
template<typename _FromType,typename _ToType>
struct string_converter
辅助模板类。它被convert_string
类使用,并实现字符串转换(Unicode <=> ASCII)。
4.2. 库功能
4.2.1. 客户端
(有关信息,请参阅第3.1.1节“功能”。)
客户端类的open
、close
和信息获取函数仅调用包装了管道WinAPI的CNamedPipeWrap
类的相应函数。
save()
函数将首先将消息GUID
保存到管道流中,然后保存消息类对象的(通过调用消息类的save()
函数)数据。它还会测试是否需要重新连接,如果由于断开连接问题导致写入数据失败,它将尝试重新连接一次。
4.2.2. 服务器端
DWORD thread_main()
函数实现了并行线程函数,该函数创建一个新的通信对象并创建一个新的管道对象(preadthread->create(m_sPipeName,m_psa)
),然后等待客户端连接(preadthread->connect(bstoped)
)并启动创建的线程。如果调用了服务器类函数stop()
(直接或间接从析构函数调用),则服务器将停止等待客户端连接并停止并行线程。(析构函数还等待读取线程停止,向它们发送停止传输数据或处理的事件。)
与具体管道客户端通信的服务器线程(类CServerPipeReadThread
)从管道通道读取并行线程数据。所有管道数据都以小块(独立于管道通道类型(PIPE_TYPE_MESSAGE
))传输。
数据读取方式如下:
- 形成一个数据块(最大数据块大小有限制;请参阅
CNamedPipeWrap::read_pipe()
常量sec_criticalbuffersize
)。 - 客户端应至少传输一个数据块。这是元块(后面的消息类的
GUID
),它应该在任何数据块之前。当服务器接收到元块时,它会准备接收与GUID
对应的消息。服务器使用模板类FindDataTypeAndProcess
来查找GUID
对应的消息类。 - 如果找到消息类,则:
- 服务器调用消息类的
load()
函数来加载该消息的数据。 - 服务器调用
process()
函数来处理加载的消息对象。 - 跳转到步骤1。
- 如果找不到
GUID
或消息,则跳转到步骤1(并忽略无效的数据块)。
4.2.4. 数据缓冲区
有些人可能认为为管道创建数据缓冲类成本太高,但这是有原因的。每个管道通道的系统缓冲区由系统分配,其大小应该很小(512字节几乎是最优尺寸)。另外,请注意消息大小几乎可以是任意的,所以显然我们需要创建一个数据缓冲区。
5. 关于库的补充
5.1. 编译器信息
这是一个模板库,所以您不需要链接任何额外的库来使用此代码。包含的DLL模块也包含单元测试。有关运行单元测试的更多信息,请参阅MonitorTestSuite项目。该项目使用了一个简单的单元测试接口,test_pipe.dll导入用于在管道库中执行单元测试的函数。(有关单元测试文件,请参阅sources\pipe\pipe_tests.cpp。)
extern "C"
void _declspec(dllexport) test_suite(ITestSuiteHelper* _ts)
// pipe\pipe_tests.cpp
上述导出函数由MonitorTestSuite用于执行单元测试,因此test_pipe.dll对于执行单元测试是必需的,但您无需将此库链接到使用管道库的代码中。
5.2. 可能的重新设计
全双工模式。目前,客户端只发送数据,服务器只接收数据,但Windows系统上的管道API存在一些“特性”。作为一种替代方案,最好在两端都创建服务器,以便能够从任何发送者(无论是客户端还是服务器)发送和接收数据。