【问题标题】:How to detect WinSock TCP timeout with BindIoCompletionCallback如何使用 BindIoCompletionCallback 检测 WinSock TCP 超时
【发布时间】:2012-01-30 23:46:20
【问题描述】:

我正在使用 BindIoCompletionCallback 构建 Visual C++ WinSock TCP 服务器,它可以正常接收和发送数据,但我找不到检测超时的好方法:SetSockOpt/SO_RCVTIMEO/SO_SNDTIMEO 对非阻塞套接字没有影响,如果peer 没有发送任何数据,CompletionRoutine 根本没有被调用。

我正在考虑将 RegisterWaitForSingleObject 与 OVERLAPPED 的 hEvent 字段一起使用,这可能会起作用,但是根本不需要 CompletionRoutine,我还在使用 IOCP 吗?如果我只使用 RegisterWaitForSingleObject 而不使用 BindIoCompletionCallback 会有性能问题吗?

更新:代码示例:

我的第一次尝试:

    bool CServer::Startup() {
        SOCKET ServerSocket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
        WSAEVENT ServerEvent = WSACreateEvent();
        WSAEventSelect(ServerSocket, ServerEvent, FD_ACCEPT);
        ......
        bind(ServerSocket......);
        listen(ServerSocket......);
        _beginthread(ListeningThread, 128 * 1024, (void*) this);
        ......
        ......
    }

    void __cdecl CServer::ListeningThread( void* param ) // static
    {
        CServer* server = (CServer*) param;
        while (true) {
            if (WSAWaitForMultipleEvents(1, &server->ServerEvent, FALSE, 100, FALSE) == WSA_WAIT_EVENT_0) {
                WSANETWORKEVENTS events = {};
                if (WSAEnumNetworkEvents(server->ServerSocket, server->ServerEvent, &events) != SOCKET_ERROR) {
                    if ((events.lNetworkEvents & FD_ACCEPT) && (events.iErrorCode[FD_ACCEPT_BIT] == 0)) {
                        SOCKET socket = accept(server->ServerSocket, NULL, NULL);
                        if (socket != SOCKET_ERROR) {
                            BindIoCompletionCallback((HANDLE) socket, CompletionRoutine, 0);
                            ......
                        }
                    }
                }
            }
        }
    }

    VOID CALLBACK CServer::CompletionRoutine( __in DWORD dwErrorCode, __in DWORD dwNumberOfBytesTransfered, __in LPOVERLAPPED lpOverlapped ) // static
    {
        ......
        BOOL res = GetOverlappedResult(......, TRUE);
        ......
    }

    class CIoOperation {
    public:
        OVERLAPPED Overlapped;
        ......
        ......
    };

    bool CServer::Receive(SOCKET socket, PBYTE buffer, DWORD length, void* context)
    {
        if (connection != NULL) {
            CIoOperation* io = new CIoOperation();
            WSABUF buf = {length, (PCHAR) buffer}; 
            DWORD flags = 0;
            if ((WSARecv(Socket, &buf, 1, NULL, &flags, &io->Overlapped, NULL) != 0) && (GetLastError() != WSA_IO_PENDING)) {
                delete io;
                return false;
            } else return true;
        }
        return false;
    }

正如我所说,如果客户端实际上正在向我发送数据,“接收”没有阻塞,调用 CompletionRoutine,接收到数据,但这是一个问题,如果客户端没有向我发送任何数据,则它工作正常,超时后如何放弃?

由于 SetSockOpt/SO_RCVTIMEO/SO_SNDTIMEO 在这里没有帮助,我认为我应该使用 OVERLAPPED 结构中的 hEvent 字段,该字段将在 IO 完成时发出信号,但是 WaitForSingleObject / WSAWaitForMultipleEvents 会阻止接收调用,我想要Receive 总是立即返回,所以我使用了 RegisterWaitForSingleObject 和 WAITORTIMERCALLBACK。它起作用了,在超时后调用回调,或者 IO 完成,但现在我有两个回调用于任何单个 IO 操作,CompletionRoutine 和 WaitOrTimerCallback:

如果 IO 完成,它们将被同时调用,如果 IO 没有完成,WaitOrTimerCallback 将被调用,然后我调用 CancelIoEx,这导致 CompletionRoutine 被调用并出现一些 ABORTED 错误,但这是一个竞争条件,也许 IO 会在我取消之前完成,然后...... blahblah,总而言之相当复杂。

然后我意识到我实际上根本不需要 BindIoCompletionCallback 和 CompletionRoutine,并且从 WaitOrTimerCallback 做所有事情,它可能会工作,但这里有一个有趣的问题,我想首先构建一个基于 IOCP 的 Winsock 服务器,并且认为 BindIoCompletionCallback 是最简单的方法,使用 Windows 本身提供的线程池,现在我最终得到一个没有 IOCP 代码的服务器?还是 IOCP 吗?还是我应该忘记 BindIoCompletionCallback 并构建自己的 IOCP 线程池实现?为什么?

【问题讨论】:

  • 您使用什么语言工作?你能提供一个有限的代码示例吗?
  • 你可能想看看这段代码:codeproject.com/KB/IP/iocp_server_client.aspx?msg=1133926 我的 Jeffrey Richter 的“Microsoft Windows 2000 的编程服务器端应用程序”的旧副本目前在别处,所以我不能给你任何更多帮助:(
  • 好吧,它的Visual C++,我会更新问题

标签: winapi sockets visual-c++ winsock io-completion-ports


【解决方案1】:

我所做的是强制超时/完成通知进入套接字对象中的关键部分。一旦进入,获胜者可以设置一个套接字状态变量并执行它的操作,无论它可能是什么。如果 I/O 完成首先进入,则以正常方式处理 I/O 缓冲区数组,并且任何超时都将由状态机指示重新启动。类似地,如果超时首先出现,I/O 将获得 CancelIOEx'd,并且任何稍后排队的完成通知都会被状态引擎丢弃。由于这些可能的“延迟”通知,我将释放的套接字放入超时队列,并仅在五分钟后将它们回收到套接字对象池中,这与 TCP 堆栈本身将其套接字放入“TIME_WAIT”的方式类似。

为了进行超时,我有一个线程在超时对象的 FIFO 增量队列上运行,每个超时限制一个队列。线程在输入队列中等待新对象,超时时间根据队列头部对象的最小超时到期时间计算得出。

服务器中只使用了几个超时,所以我使用了在编译时固定的队列。通过向线程输入队列发送适当的“命令”消息并与新套接字混合来添加新队列或修改超时将相当容易,但我没有做到这一点。

在超时时,线程调用对象中的一个事件,在套接字的情况下,该事件将进入套接字对象 CS 保护的状态机,(这是一个 TimeoutObject 类,该类是套接字的后代) .

更多:

我等待控制超时线程输入队列的信号量。如果发出信号,我会从输入队列中获取新的 TimeoutObject 并将其添加到它要求的任何超时队列的末尾。如果信号量等待超时,我会检查超时 FIFO 队列头部的项目,并通过从它们的超时时间中减去当前时间来重新计算它们的剩余间隔。如果间隔为 0 或负数,则调用超时事件。在迭代队列及其头部时,我将下一次超时前的最小剩余间隔保存在本地。 Hwn 所有队列中的所有头项都有非零剩余间隔,我使用我累积的最小剩余间隔返回等待队列信号量。

事件调用返回一个枚举。此枚举指示超时线程如何处理刚刚触发其事件的对象。一种选择是通过重新计算超时时间并在最后将对象推回其超时队列来重新启动超时。

我没有使用 RegisterWaitForSingleObject(),因为它需要 .NET 并且我的 Delphi 服务器都是非托管的(我很久以前就编写了我的服务器!)。

因为 IIRC,它有 64 个句柄的限制,例如 WaitForMultipleObjects()。我的服务器有超过 23000 个客户端超时。我发现单个超时线程和多个 FIFO 队列更加灵活——任何旧对象都可以在其上超时,只要它是 TimeoutObject 的后代——不需要额外的操作系统调用/句柄。

【讨论】:

  • 嗯......所以你在某些事件上使用专用线程和 WaitForSingleObject 顺序?这就是 RegisterWaitForSingleObject 的设计目的吗?
  • 我只等待过一个同步对象——保持超时线程输入队列计数的信号量。我编辑了我的答案以添加更多细节。
  • 嗯...稍后我会仔细阅读您的更新,但 RegisterWaitForSingleObject 是 Win32 API,不依赖于 .NET :)
  • 嗯。如果您使用 RegisterWaitForSingleObject,它会为每个等待句柄创建一个线程吗?
【解决方案2】:

基本思想是,由于您在系统线程池中使用异步 I/O,因此您不需要通过事件检查超时,因为您没有阻塞任何线程。

检查陈旧连接的recommended way 是使用SO_CONNECT_TIME 选项调用getsockopt。这将返回套接字已连接的秒数。我知道这是一个轮询操作,但是如果您对查询该值的方式和时间很了解,那么它实际上是一种非常好的管理连接的机制。我在下面解释这是如何完成的。

通常我会在两个地方调用getsockopt:一个是在我的完成回调期间(这样我就有了该套接字上最后一次发生 I/O 完成的时间戳),一个在我的接受中线程。

accept 线程通过WSAEventSelectFD_ACCEPT 参数监视我的套接字积压。这意味着接受线程仅在 Windows 确定存在需要接受的传入连接时执行。此时我枚举了我接受的套接字并再次查询SO_CONNECT_TIME 以获取每个套接字。我从该值中减去连接的最后一次 I/O 完成的时间戳,如果差值高于指定阈值,我的代码将认为连接已超时。

【讨论】:

  • 是的,您没有阻塞任何线程,但套接字仍会使用系统资源,直到您注意到时间延迟,对吧?这真的足以防止您耗尽连接吗?
猜你喜欢
  • 2011-10-04
  • 2016-06-18
  • 1970-01-01
  • 2019-01-30
  • 1970-01-01
  • 2023-01-29
  • 1970-01-01
  • 2011-06-02
  • 1970-01-01
相关资源
最近更新 更多