【问题标题】:Timeout for ConnectEx() in IOCP mode?IOCP 模式下 ConnectEx() 超时?
【发布时间】:2019-07-24 22:53:01
【问题描述】:

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

  1. “IO 完成”排队到关联的 IO 完成端口。

  2. GetQueuedCompletionStatus() 返回 FALSE。

  3. WSAGetOverlappedResult() 返回WSAETIMEDOUT

什么决定了调用ConnectEx()和上面的1之间的超时时间?如何缩短此超时时间?

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

【问题讨论】:

  • iocp 这不是模式。和超时值独立于你的套接字是否与 iocp 相关联
  • IOCP 是一种完成对 Winsock 的连接请求的模式。无论如何,是什么决定了套接字的“连接超时”值,无论它是否与 IOCP 相关联?
  • 如果是准确的 - iocp 这不是模式,而是在 io 操作完成时获得通知的唯一方式。我不知道/查看让设置连接超时的套接字选项,但让我们问一下 - 你需要什么?
  • 对于快速 TCP 连接扫描器,它只检测快速连接的服务器。
  • 或者继续使用 Event 对象,使用 RegisterWaitForSingleObject() 等待它,如果在它过去之前连接,则取消等待,否则在 OVERLAPPED 上调用 CancelIoEx

标签: windows sockets winapi winsock2


【解决方案1】:

不幸的是,似乎没有设置套接字连接超时的内置选项。基于这个问题,我看不到这一点有多少 - How to configure socket connect timeout - 没有人也看不到。

一种可能的解决方案将事件句柄传递给 I/O 请求,如果我们得到 ERROR_IO_PENDING - 请为此事件调用 RegisterWaitForSingleObject。如果此调用成功 - 我们的 WaitOrTimerCallback 回调函数将被调用 - 或者因为 I/O 将完成(具有任何最终状态)并且此时事件(我们将其传递给 I/O 请求和 RegisterWaitForSingleObject ) 将被设置或因为超时 (dwMilliseconds) 已过期 - 在这种情况下,我们需要调用 CancelIoEx 函数。

假设我们有 class IO_IRP : public OVERLAPPED,它有引用计数(我们需要保存指向 OVERLAPPED 的指针,用于 I/O 请求以将其传递给 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 设置计时器,如果计时器到期 - 调用CancellIoEx 或从此处关闭 I/O 句柄。与事件解决方案的区别 - 如果 I/O 将在计时器到期之前完成 - 将不会自动调用 WaitOrTimerCallback 回调函数。万一事件 - 当 I/O 完成时(在初始挂起状态之后),I/O 子系统设置事件,并且由于该事件(信号状态中的事件),将调用回调。但万一计时器 - 无法将其作为参数传递给 io 请求(I/O 仅接受事件句柄)。因此,我们需要自己保存指向计时器对象的指针,并在 I/O 完成时手动释放它。所以这里将有 2 个指向计时器对象的指针 - 一个来自池(由 CreateTimerQueueTimer 保存)和一个来自我们的对象(套接字)类(当 I/O 完成时我们需要它来取消引用对象)。这也需要对包含计时器的对象进行引用计数。另一方面,我们可以使用定时器,而不是单个 I/O 操作,而是多个 I/O(因为它不直接绑定到某些 I/O)

【讨论】:

  • 您写道:“不幸的是,似乎没有用于设置套接字连接超时的内置选项”......这是对我的第二个问题的严峻但可以接受的答案。至于提出的解决方法,我想知道哪种方法消耗更少的系统资源:使用 CreateTimerQueueTimer() 的方法还是使用 RegisterWaitForSingleObject() 的方法?
  • @GeorgeRobinson - 更少的系统资源很难说,这也取决于 Windows 版本 - 非常不同的实现。对于计时器,我使用this。您也可以将所有套接字对象链接到列表并定期(例如每秒 1 次)检查此列表是否超时。开始连接时间记住每个对象。这需要更少的资源比较这两种方式
猜你喜欢
  • 1970-01-01
  • 2021-10-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-09
  • 1970-01-01
相关资源
最近更新 更多