WSAConnectByName 超时

WSAConnectByName timeout

我正在尝试使用 WSAConnectByName() 连接到一个地址。但是,它似乎忽略了 timeout 参数。

这是来自 MS 示例的(仅稍作修改的)代码:

SOCKET ConnSocket = INVALID_SOCKET;
int iResult;
BOOL bSuccess;
SOCKADDR_STORAGE LocalAddr = {0};
SOCKADDR_STORAGE RemoteAddr = {0};
DWORD dwLocalAddr = sizeof(LocalAddr);
DWORD dwRemoteAddr = sizeof(RemoteAddr);

ConnSocket = socket(AF_INET, SOCK_STREAM, 0);
if (ConnSocket == INVALID_SOCKET){
    wprintf(L"socket failed with error: %d\n", WSAGetLastError());
    return INVALID_SOCKET;
}

struct timeval tv;
tv.tv_sec = 1;
tv.tv_usec = 0;

bSuccess = WSAConnectByName(ConnSocket, NodeName, 
        PortName, &dwLocalAddr,
        (SOCKADDR*)&LocalAddr,
        &dwRemoteAddr,
        (SOCKADDR*)&RemoteAddr,
        (struct timeval *)&tv,
        NULL);
if (!bSuccess){
    wprintf(L"WsaConnectByName failed with error: %d\n", WSAGetLastError());
    closesocket(ConnSocket);
    return INVALID_SOCKET;       
}

当我使用不存在的地址(如本地 IP 地址)时,代码不会因超时而失败,而是会停止,直到发生其他超时。

知道这里发生了什么吗?

这是由于错误的文档。

timeout 参数仅部分使用WSAConnectByName 是一个复杂的函数,它在内部执行许多操作。

开头有这样的代码:

ULONG time, ms;
if (timeout)
{
  time = GetTickCount();
  ms = timeout->tv_sec * 1000 + timeout->tv_usec/1000;
}

然后它多次调用这样的代码:

if (timeout && GetTickCount() - time > ms) return WSAETIMEDOUT;

但是函数的核心调用 ConnectEx 是这样的:

if (!ConnectEx(*))
{
  if (GetLastError() == ERROR_IO_PENDING)
  {
    WSAGetOverlappedResult(*); // you wait here and the timeout is not used!
  }
}

ConnectEx(这是一个异步函数)和GetOverlappedResult都没有指定参数暂停。在 ConnectEx 退出后,您最终在 GetOverlappedResult 中等待,但您无法为其设置超时。

只有一个好的解决方案 - 直接使用 ConnectEx 并且不使用任何超时。

另外一个问题点 - GetAddrInfoEx 用于将 nodename 转换为 ip 地址。这个函数用 timeout=0, lpOverlapped=0, lpCompletionRoutine = 0 调用 - 所以这里也可以等待 DNS 请求。只支持从 Windows 8

开始的异步查询

编辑

如果直接使用ConnectEx我们可以使用超时(感谢Remy Lebeau的想法)-create/useOVERLAPPED.hEvent

OVERLAPPED Overlapped = {};
Overlapped.hEvent = CreateEvent(0, 0, 0, 0);
//... ConnectEx ...
ULONG NumberOfBytesTransferred = 0;
ULONG err = GetLastError();
if (err == ERROR_IO_PENDING)
{
    switch (WaitForSingleObject(Overlapped.hEvent, ms))
    {
    case STATUS_TIMEOUT:
        CancelIoEx((HANDLE)s, &Overlapped);
        err = WSAETIMEDOUT;
        break;
    case WAIT_OBJECT_0:
        // really final NT status of operation is Internal and NumberOfBytesTransferred == InternalHigh
        // GetOverlappedResult set LastError by translating (NTSTATUS)Internal to win32 error
        // with the loss of accuracy, especially if  status > 0
        GetOverlappedResult((HANDLE)s, &Overlapped, &NumberOfBytesTransferred, TRUE);
        err = GetLastError();

        // code of GetOverlappedResult in this case for clarity
        //if ((NTSTATUS)Overlapped.InternalHigh == STATUS_PENDING)
        //{
        //  WaitForSingleObject(Overlapped.hEvent, INFINITE);
        //}
        //NumberOfBytesTransferred = (ULONG)Overlapped.InternalHigh;
        //if (0 > (NTSTATUS)Overlapped.Internal) 
        //{
        //  SetLastError(RtlNtStatusToDosError((NTSTATUS)Overlapped.Internal));
        //}

        break;
    default: __debugbreak();
    }
}

或替代使用 GetOverlappedResultEx 我们的超时(但需要 windows 8+)

或最佳选择(以我的观点)- 使用 BindIoCompletionCallback((HANDLE)s,) 或直接为套接字绑定自身 IOCP,根本不在调用后等待。

我在工作线程中使用过它。超时似乎仅在连接实际超时(10060)时才适用。如果连接被拒绝 (10061),调用可能会在几秒钟内完成。其他错误也是如此。我没有经历过任何挂起(运行 on Windows 10)。

1秒对于WSAConnectByName

来说可能有点短