问题:
设计一个高效且非常快速的命名管道客户端服务器框架。
当前状态:
我已经有经过验证的生产测试框架。它很快,但是每个管道连接使用一个线程,如果有很多客户端,线程数可能会很快变高。我已经使用了可以随需要扩展的智能线程池(事实上的任务池)。
我已经对管道使用OVERLAPED模式,但后来我使用WaitForSingleObject或WaitForMultipleObjects阻塞,这就是为什么我在服务器端每个连接需要一个线程
所需解决方案:
客户端很好,但在服务器端,我想只为每个客户端请求使用一个线程,而不是每个连接。因此,不是在客户端的整个生命周期中使用一个线程(连接/断开连接),而是每个任务使用一个线程。所以只有当客户请求数据时才会这样做。
我在MSDN上看到一个使用OVERLAPED结构数组的示例,然后使用WaitForMultipleObjects等待它们全部。我发现这是一个糟糕的设计。我在这里看到两个问题。首先,你必须维护一个可以变得非常大的数组,删除将是昂贵的。其次,你有很多事件,每个阵列成员一个。
我也看到了完成端口,例如CreateIoCompletionPort和GetQueuedCompletionStatus,但我看不出它们有多好。
我想要的是ReadFileEx和WriteFileEx所做的事情,他们称之为回调例程 操作完成后。这是一种真正的异步编程风格。但问题是ConnectNamedPipe不支持它,而且我看到线程需要处于警报状态,你需要调用一些* Ex函数来实现它。
那么这个问题最好如何解决?
以下是MSDN的用法:http://msdn.microsoft.com/en-us/library/windows/desktop/aa365603(v=vs.85).aspx
我用这种方法看到的问题是,如果WaitForMultipleObjects的限制为64个句柄,我看不出如何同时连接100个客户端。当然,我可以在每次请求后断开管道,但我们的想法是在TCP服务器中建立永久的客户端连接,并在每个客户端具有唯一ID和客户端特定数据的整个生命周期中跟踪客户端。
理想的伪代码应该是这样的:
repeat
// wait for the connection or for one client to send data
Result = ConnectNamedPipe or ReadFile or Disconnect;
case Result of
CONNECTED: CreateNewClient; // we create a new client
DATA: AssignWorkerThread; // here we process client request in a thread
DISCONNECT: CleanupAndDeleteClient // release the client object and data
end;
until Aborted;
这样我们只有一个接受connect / disconnect / onData事件的监听器线程。线程池(工作线程)仅处理实际请求。这样,5个工作线程可以为许多连接的客户端提供服务。
P.S。 我目前的代码不应该很重要。我用Delphi编写它,但它的纯WinAPI,所以语言无关紧要。
修改
现在IOCP看起来像解决方案:
I / O完成端口为其提供了有效的线程模型 在多处理器上处理多个异步I / O请求 系统。当进程创建I / O完成端口时,系统 为唯一目的的请求创建关联的队列对象 为这些请求提供服务。处理许多并发的进程 异步I / O请求可以更快速有效地执行此操作 将I / O完成端口与预先分配的线程结合使用 池,而不是在收到I / O请求时创建线程。
答案 0 :(得分:3)
如果服务器必须处理超过64个事件(读/写),那么使用WaitForMultipleObjects的任何解决方案都变得不可行。这就是Microsoft将IO完成端口引入Windows的原因。它可以使用最合适的线程数处理非常多的IO操作(通常是处理器/核心数)。
IOCP的问题在于很难正确实施。隐藏的问题像现场的地雷一样传播:[1],[2](第3.6节)。我建议使用一些框架。有点谷歌搜索为Delphi开发人员提出了一个名为Indy的东西。可能还有其他人。
此时我会忽略对命名管道的要求,如果这意味着编码我自己的IOCP实现。这不值得悲伤。
答案 1 :(得分:1)
我认为您忽略的是,您在任何给定时间只需要一些侦听命名管道实例。管道实例连接后,您可以关闭该实例并创建一个新的侦听实例来替换它。
使用MAXIMUM_WAIT_OBJECTS
(或更少)监听命名管道实例,您可以使用WaitForMultipleObjectsEx
专用于监听的单个线程。同一个线程也可以使用ReadFileEx
和WriteFileEx
以及APC处理剩余的I / O.工作线程将APC排队到I / O线程以启动I / O,I / O线程可以使用任务池返回结果(以及让工作线程知道新连接)。 / p>
I / O线程主函数看起来像这样:
create_events();
for (index = 0; index < MAXIMUM_WAIT_OBJECTS; index++) new_pipe_instance(i);
for (;;)
{
if (service_stopping && active_instances == 0) break;
result = WaitForMultipleObjectsEx(MAXIMUM_WAIT_OBJECTS, connect_events,
FALSE, INFINITE, TRUE);
if (result == WAIT_IO_COMPLETION)
{
continue;
}
else if (result >= WAIT_OBJECT_0 &&
result < WAIT_OBJECT_0 + MAXIMUM_WAIT_OBJECTS)
{
index = result - WAIT_OBJECT_0;
ResetEvent(connect_events[index]);
if (GetOverlappedResult(
connect_handles[index], &connect_overlapped[index],
&byte_count, FALSE))
{
err = ERROR_SUCCESS;
}
else
{
err = GetLastError();
}
connect_pipe_completion(index, err);
continue;
}
else
{
fail();
}
}
唯一真正的复杂因素是,当您呼叫ConnectNamedPipe
时,如果呼叫立即失败,则可能会返回ERROR_PIPE_CONNECTED
以指示呼叫立即成功,或者返回ERROR_IO_PENDING
以外的错误。在这种情况下,您需要重置事件,然后处理连接:
void new_pipe(ULONG_PTR dwParam)
{
DWORD index = dwParam;
connect_handles[index] = CreateNamedPipe(
pipe_name,
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_MESSAGE | PIPE_WAIT | PIPE_ACCEPT_REMOTE_CLIENTS,
MAX_INSTANCES,
512,
512,
0,
NULL);
if (connect_handles[index] == INVALID_HANDLE_VALUE) fail();
ZeroMemory(&connect_overlapped[index], sizeof(OVERLAPPED));
connect_overlapped[index].hEvent = connect_events[index];
if (ConnectNamedPipe(connect_handles[index], &connect_overlapped[index]))
{
err = ERROR_SUCCESS;
}
else
{
err = GetLastError();
if (err == ERROR_SUCCESS) err = ERROR_INVALID_FUNCTION;
if (err == ERROR_PIPE_CONNECTED) err = ERROR_SUCCESS;
}
if (err != ERROR_IO_PENDING)
{
ResetEvent(connect_events[index]);
connect_pipe_completion(index, err);
}
}
connect_pipe_completion
函数将在任务池中创建一个新任务来处理新连接的管道实例,然后将APC排队以调用new_pipe
以在同一索引处创建新的侦听管道。
关闭后可以重用现有的管道实例,但在这种情况下,我认为这不值得麻烦。