4

我正在使用 BindIoCompletionCallback 构建一个 Visual C++ WinSock TCP 服务器,它可以正常接收和发送数据,但我找不到检测超时的好方法:SetSockOpt/SO_RCVTIMEO/SO_SNDTIMEO 对非阻塞套接字没有影响,如果对等方不是发送任何数据时,根本不会调用 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 将阻止接收调用,我希望接收总是立即返回,所以我使用了 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 线程池实现?为什么 ?

4

2 回答 2

0

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

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

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

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

于 2012-01-10T22:41:15.177 回答
0

我所做的是强制超时/完成通知进入套接字对象中的关键部分。一旦进入,获胜者可以设置一个套接字状态变量并执行它的操作,无论它可能是什么。如果 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 的后代——不需要额外的操作系统调用/句柄。

于 2012-01-04T13:12:12.713 回答