RAPI2 - 第二部分:善用流
安全有效地使用 RAPI2 接口。
引言
在第一部分中,我们探讨了 RAPI2 的基础知识并实现了一个小的“dir”类应用程序。本文将回顾我们的旧应用程序,同时检查 IRAPIStream
接口的优点和陷阱。
关于 IRAPIStream
您可能曾在 IStream
中见过它,或者在 MSXML API 或 ISAPI 框架中见过。它允许我们以非常类似于 Winsock 函数的方式发送和接收数据。 IRAPIStream
接口建立在 IStream
功能的基础上,并提供了两个新方法。
根据 MSDN,这两个新方法是:
SetRapiStat
- 设置IStream::Read
方法的超时值。GetRapiStat
- 获取IStream::Read
方法的超时值。
就像 Winsock 的 recv()
函数一样,IRAPIStream::Read()
函数是阻塞的。因此,最好有某种取消调用的方法。IRAPIStream
超时方法提供了这种能力。
启用流模式
通过为 CeRapiInvoke()
的 ppIRAPIStream
参数提供一个非 NULL
值,可以简单地启用流模式。在此模式下,CeRapiInvoke()
将立即返回。pcbOutput
和 ppOutput
参数仍可以被填充,RAPI 扩展 DLL 仍将为这些值接收有效的指针。但是,当 CeRapiInvoke()
返回时,这些值中不会包含来自扩展 DLL 的任何信息。所以,不要理会它们。在流模式下,它们实际上是无用的。
HRESULT hr = S_OK;
CComPtr< IRAPIStream > stream;
hr = rapi_session->CeRapiInvoke( L"CeDirLib.dll",
L"CeDir_GetDirectoryListing",
folder.size() * sizeof( wchar_t ),
( BYTE* )folder.c_str(),
NULL,
NULL,
&stream,
0 );
if( NULL != stream )
{
// Our stream is ready for Reading and Writing!
}
写入流
我们将考虑两种将数据写入 IStream
的方法。第一种方法可能是最明显的方法。对于任何只需要将一种类型的数据写入流且仅执行一次的简单程序,我们会使用这种方法。第二种方法将 IStream
和 FindXFile
API 抽象为标准库兼容的迭代器。虽然我们的“dir”应用程序很简单,但为了演示目的,我们将实现可扩展的方法。
简单方法
直接写入流的方法如下所示。我们使用典型的模式来遍历 FindXFile
API 找到的每个文件对象,并将每个找到的项写入流。最后,我们写入一个空的 WIN32_FIND_DATA
结构到流中,以指示接收方我们已完成。
WIN32_FIND_DATA file = { 0 };
HANDLE find_file = ::FindFirstFile( folder.c_str(), &file );
if( INVALID_HANDLE_VALUE != find_file )
{
DWORD written = 0;
do
{
pStream->Write( &file, sizeof( WIN32_FIND_DATA ), &written );
} while ( SUCCEEDED( hr ) && ::FindNextFile( find_file, &file ) );
::FindClose( find_file );
// send an empty WIN32_FIND_DATA structure to indicate we're finished.
ZeroMemory( &file, sizeof( WIN32_FIND_DATA ) );
pStream->Write( &file, sizeof( WIN32_FIND_DATA ), &written );
}
可扩展方法
现在,我们考虑一个非简单的应用程序,其中我们发送多个目录的内容,或者我们还枚举所有正在运行的进程。对于每次枚举,我们需要复制粘贴那 13 行代码。每次复制粘贴操作都有可能引入重复的缺陷。如果后续测试发现其中任何一个部分存在 bug,我们必须确保在所有部分都修复它。将枚举与流通信分开封装,岂不更好?
// create a stream buffer from the IRAPIStream
RapiOStreamBuf< WIN32_FIND_DATA > sb( pStream );
// copy all files matching the search string to the stream buffer.
FindFile find( folder );
std::copy( find.begin(),
find.end(),
RapiOStreamBuf_iterator< WIN32_FIND_DATA >( &sb ) );
对于这种方法,我们必须定义三个新的可重用对象:
FindFile
RapiOstreamBuf<>
RapiOstreamBuf_iterator<>
FindFile
类为标准的 FindXFile
API 提供迭代器支持。附件代码中提供了一个示例实现,但更完整的实现,例如 WinSTL,也是可用的。
RapiOStreamBuf<>
派生自 std::basic_streambuf<>
。我们重写了 overflow
和 sync
,以便在 IStream
上发送 WIN32_FIND_DATA
结构。当流关闭时,会发送一个空的 WIN32_FIND_DATA
结构来指示 EOF。
template< typename T >
class RapiOStreamBuf : public std::basic_streambuf< byte >;
RapiOStreamBuf_iterator<>
是 RapiOstreamBuf<>
的一个简单迭代器适配器。它重写了赋值运算符,使得 stream_iterator = find_iterator
的惯用法将包装的 WIN32_FIND_DATA
结构发送到包装的 IStream
。我们指定了 std::output_iterator_tag
来指示此迭代器只能递增和写入。由于我们只实现了向此流写入,因此无法从此迭代器读取数据。
template< typename T >
class RapiOStreamBuf_iterator : public std::iterator< std::output_iterator_tag,
void, void, void, void >;
从流中读取
与之前一样,我们将考虑两种从 IStream
读取的方法。简单方法易于编写,但不适合大型、复杂的应用程序。可扩展方法将要读取的数据类型与读取数据的方法分开封装。
简单方法
在这种方法中,我们使用 IRAPIStream
接口从流中读取 CE_FIND_DATA
结构,直到出现错误或读取到空的 CE_FIND_DATA
结构(指示 EOF)为止。
// read from the IRAPIStream until we receive an empty CE_FIND_DATA
// structure or we get an error.
CE_FIND_DATA eof = { 0 };
CE_FIND_DATA file = { 0 };
while( ( hr = stream->Read( &file, sizeof( CE_FIND_DATA ), NULL ) ) >= 0 &&
( file != eof ) )
{
// use the received CE_FIND_DATA structure...
}
对于这种方法,还需要定义一个不等运算符来测试 EOF 条件。
/// return true if i != j
bool operator!=( const CE_FIND_DATA& i, const CE_FIND_DATA& j );
可扩展方法
在这种方法中,我们不再需要担心检查 EOF;流缓冲区会为我们处理。
// create a stream buffer from the IRAPIStream
RapiIStreamBuf< CE_FIND_DATA > sb( stream );
// insert each received structure in to the vector
std::vector< CE_FIND_DATA > listing;
std::copy( RapiIStreamBuf_iterator< CE_FIND_DATA >( &sb ),
RapiIStreamBuf_iterator< CE_FIND_DATA >(),
std::back_inserter( listing ) );
// use the std::vector<> collection of CE_FIND_DATA structures...
对于这种方法,我们定义了两个新的可重用对象:
RapiIStreamBuf<>
RapiIStreamBuf_iterator<>
这些对象是模板化的,以便我们轻松地将它们改编以接收其他数据结构。我们甚至可以进一步抽象,在 RAPI 和 CoreCon API 之间提供一个通用接口,允许用户选择通过 TCP/IP 连接到设备。但这要留待以后再讲。
RapiIStreamBuf<>
派生自 std::basic_streambuf
。我们重写了 underflow
,以便从 IStream
读取 CE_FIND_DATA
结构,直到读取到空的结构(指示 EOF)。
template< typename T >
class RapiIStreamBuf : public std::basic_streambuf< byte >;
RapiIStreamBuf_iterator<>
是 RapiIStreamBuf<>
的一个简单迭代器适配器。它重写了加法运算符以从流中获取另一个 CE_FIND_DATA
结构,并重写了解引用运算符以提供对已接收 CE_FIND_DATA
结构的访问。我们指定了 std::input_iterator_tag
来指示此迭代器只能递增和解引用。由于我们只实现了从流中读取,因此此迭代器是不可变的。
template< typename T >
class RapiIStreamBuf_iterator : public std::iterator< std::input_iterator_tag, T >;
关于超时的说明
像任何好的套接字一样,IRAPIStream::Read
将继续等待数据读取,直到世界末日。让我们考虑一下,如果我们的 EOF 标志(一个空的 CE_FIND_DATA
结构)从未收到会发生什么。从用户的角度来看,我们的“dir”程序将锁定,因为 IRAPISTREAM::Read
在等待永远不会来的更多数据。为了防止这种情况发生,我们将使用 IRAPIStream::SetRapiStat
为 IRAPIStream::Read
设置一个任意但合理的值。对于这个示例应用程序,我们将使用 3 秒的值。考虑到我们使用的是低延迟的 USB 连接;这似乎是确保我们的数据到达和用户不耐烦地按下 Ctrl-C 之间的公平折衷。
hr = stream->SetRapiStat( STREAM_TIMEOUT_READ, 3 );
目前,STREAM_TIMEOUT_READ
是 IRapiStream::SetRapiStat
可用的唯一选项。