命名管道高效的异步设计

时间:2013-07-23 09:01:53

标签: windows delphi winapi asynchronous named-pipes

问题

设计一个高效且非常快速的命名管道客户端服务器框架。

当前状态

我已经有经过验证的生产测试框架。它很快,但是每个管道连接使用一个线程,如果有很多客户端,线程数可能会很快变高。我已经使用了可以随需要扩展的智能线程池(事实上的任务池)。

我已经对管道使用OVERLAPED模式,但后来我使用WaitForSingleObject或WaitForMultipleObjects阻塞,这就是为什么我在服务器端每个连接需要一个线程

所需解决方案:

客户端很好,但在服务器端,我想只为每个客户端请求使用一个线程,而不是每个连接。因此,不是在客户端的整个生命周期中使用一个线程(连接/断开连接),而是每个任务使用一个线程。所以只有当客户请求数据时才会这样做。

我在MSDN上看到一个使用OVERLAPED结构数组的示例,然后使用WaitForMultipleObjects等待它们全部。我发现这是一个糟糕的设计。我在这里看到两个问题。首先,你必须维护一个可以变得非常大的数组,删除将是昂贵的。其次,你有很多事件,每个阵列成员一个。

我也看到了完成端口,例如CreateIoCompletionPortGetQueuedCompletionStatus,但我看不出它们有多好。

我想要的是ReadFileExWriteFileEx所做的事情,他们称之为回调例程 操作完成后。这是一种真正的异步编程风格。但问题是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请求时创建线程。

2 个答案:

答案 0 :(得分:3)

如果服务器必须处理超过64个事件(读/写),那么使用WaitForMultipleObjects的任何解决方案都变得不可行。这就是Microsoft将IO完成端口引入Windows的原因。它可以使用最合适的线程数处理非常多的IO操作(通常是处理器/核心数)。

IOCP的问题在于很难正确实施。隐藏的问题像现场的地雷一样传播:[1],[2](第3.6节)。我建议使用一些框架。有点谷歌搜索为Delphi开发人员提出了一个名为Indy的东西。可能还有其他人。

此时我会忽略对命名管道的要求,如果这意味着编码我自己的IOCP实现。这不值得悲伤。

答案 1 :(得分:1)

我认为您忽略的是,您在任何给定时间只需要一些侦听命名管道实例。管道实例连接后,您可以关闭该实例并创建一个新的侦听实例来替换它。

使用MAXIMUM_WAIT_OBJECTS(或更少)监听命名管道实例,您可以使用WaitForMultipleObjectsEx专用于监听的单个线程。同一个线程也可以使用ReadFileExWriteFileEx以及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以在同一索引处创建新的侦听管道。

关闭后可以重用现有的管道实例,但在这种情况下,我认为这不值得麻烦。