IOCP 模式下 ConnectEx() 超时?

Timeout for ConnectEx() in IOCP mode?

在 IOCP Winsock2 客户端中,ConnectEx() 连接尝试超时后,会发生以下情况:

  1. "IO completion" 排队到关联的 IO 完成端口。

  2. GetQueuedCompletionStatus() returns 错误。

  3. WSAGetOverlappedResult() returns WSAETIMEDOUT.

什么决定了调用ConnectEx()和上面的1之间的超时时间?我怎样才能缩短这个超时时间?

我知道可以等待 ConnectEx() 传递给它一个完整的结构 OVERLAPPED.hEvent = WSACreateEvent() 然后等待这个事件,例如WaitForSingleObject(Overlapped.hEvent, millisec)millisec 时间段内未建立连接后超时。但是,该解决方案超出了这个问题的范围,因为它没有引用 IOCP 通知模型。

不幸的是,似乎没有用于设置套接字连接超时的内置选项。最低限度我不看这个和基于这个问题 - How to configure socket connect timeout - 没有人不看。

一个可能的解决方案将事件句柄传递给 I/O 请求,如果我们得到 ERROR_IO_PENDING - 调用 RegisterWaitForSingleObject for this event. if this call will be successful - our WaitOrTimerCallback callback function will be called - or because I/O will be complete (with any final status) and at this moment event (which we pass both to I/O request and RegisterWaitForSingleObject) will be set or because timeout (dwMilliseconds) expired - in this case we need call CancelIoEx 函数。

所以假设我们有 class IO_IRP : public OVERLAPPED 有引用计数(我们需要保存指向 I/O 请求中使用的 OVERLAPPED 的指针以将其传递给 CancelIoEx。并且需要确保这个 OVERLAPPED 还没有在另一个新的 I/O 中使用——所以还不是免费的)。在这种情况下可能的实施方式:

class WaitTimeout
{
    IO_IRP* _Irp;
    HANDLE _hEvent, _WaitHandle, _hObject;

    static VOID CALLBACK WaitOrTimerCallback(
        __in  WaitTimeout* lpParameter,
        __in  BOOLEAN TimerOrWaitFired
        )
    {
        UnregisterWaitEx(lpParameter->_WaitHandle, NULL);

        if (TimerOrWaitFired)
        {
            // the lpOverlapped unique here (because we hold reference on it) - not used in any another I/O
            CancelIoEx(lpParameter->_hObject, lpParameter->_Irp);
        }

        delete lpParameter;
    }

    ~WaitTimeout()
    {
        if (_hEvent) CloseHandle(_hEvent);
        _Irp->Release();
    }

    WaitTimeout(IO_IRP* Irp, HANDLE hObject) : _hEvent(0), _Irp(Irp), _hObject(hObject)
    {
        Irp->AddRef();
    }

    BOOL Create(PHANDLE phEvent)
    {
        if (HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL))
        {
            *phEvent = hEvent;
            _hEvent = hEvent;

            return TRUE;
        }

        return FALSE;
    }
public:

    static WaitTimeout* Create(PHANDLE phEvent, IO_IRP* Irp, HANDLE hObject)
    {
        if (WaitTimeout* p = new WaitTimeout(Irp, hObject))
        {
            if (p->Create(phEvent))
            {
                return p;
            }

            delete p;
        }

        return NULL;
    }

    void Destroy()
    {
        delete this;
    }

    // can not access object after this call
    void SetTimeout(ULONG dwMilliseconds)
    {
        if (RegisterWaitForSingleObject(&_WaitHandle, _hEvent, 
            (WAITORTIMERCALLBACK)WaitOrTimerCallback, this, 
            dwMilliseconds, WT_EXECUTEONLYONCE|WT_EXECUTEINWAITTHREAD))
        {
            // WaitOrTimerCallback will be called
            // delete self here
            return ;
        }

        // fail register wait
        // just cancel i/o and delete self
        CancelIoEx(_hObject, _Irp);
        delete this;
    }
};

并使用类似

的东西
if (IO_IRP* Irp = new IO_IRP(...))
{
    WaitTimeout* p = 0;

    if (dwMilliseconds)
    {
        if (!(p = WaitTimeout::Create(&Irp->hEvent, Irp, (HANDLE)socket)))
        {
            err = ERROR_NO_SYSTEM_RESOURCES;
        }
    }

    if (err == NOERROR)
    {
        DWORD dwBytes;

        err = ConnectEx(socket, RemoteAddress, RemoteAddressLength, 
            lpSendBuffer, dwSendDataLength, &dwBytes, Irp)) ?
                NOERROR : WSAGetLastError();
    }

    if (p)
    {
        if (err == ERROR_IO_PENDING)
        {
            p->SetTimeout(dwMilliseconds);
        }
        else
        {
            p->Destroy();
        }
    }

    Irp->CheckErrorCode(err);
}

另一种可能的解决方案是通过 CreateTimerQueueTimer and if timer expired - call CancellIoEx or close I/O handle from here. difference with event solution - if I/O will be completed before timer expired - the WaitOrTimerCallback callback function will be not automatically called. in case event - I/O subsystem set event when I/O complete (after intial pending status) and thanks to that (event in signal state) callback will be called. but in case timer - no way pass it to io request as parameter (I/O accept only event handle). as result we need save pointer to timer object by self and manually free it when I/O complete. so here will be 2 pointer to timer object - one from pool (saved by CreateTimerQueueTimer 设置计时器)和一个来自我们的对象(套接字)class(当 I/O 完成时我们需要它来取消引用对象)。这也需要对封装计时器的对象进行引用计数。从另一方面来说,我们可以使用计时器,而不是用于单个 I/O 操作,而是用于多个 I/O (因为它不直接绑定到某些 I/O)