Search code examples
cwinapinetwork-programmingwinsock

WSAConnectByName timeout


I'm trying to use WSAConnectByName() to connect to an address. However, it seems like it disregards the timeout parameter.

Here is the (only slightly modified) code from MS's example:

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;       
}

When I use an address that doesn't exist (like a local IP address), instead of failing with a timeout, the code stops until some other timeout occurs.

Any idea what's going on here?


Solution

  • This is due to bad documentation.

    The timeout parameter is only partially used. WSAConnectByName is a complex function, which performs many operations internally.

    At the beginning exists code like this:

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

    and then several times it calls code like this:

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

    but the heart of the function calls to ConnectEx like this:

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

    Both ConnectEx (which is an asynchronous function) and GetOverlappedResult have no parameter for specifying a timeout. You end up waiting in GetOverlappedResult after ConnectEx exits, but you can not set a timeout for it.

    There exists only one nice solution - use ConnectEx directly and not use any timeouts.

    else one problem point - GetAddrInfoEx used to translate nodename to ip address. this function called with timeout=0, lpOverlapped=0, lpCompletionRoutine = 0 - so here also can wait on DNS request. asynchronous query supported only begin from Windows 8

    EDIT

    if direct use ConnectEx we can use timeout (thank Remy Lebeau for idea) - create/use OVERLAPPED.hEvent and

    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();
        }
    }
    

    or for alternative use GetOverlappedResultEx with our timeout (but need windows 8+)

    or the best choice (for my view) - use BindIoCompletionCallback((HANDLE)s,) or direct bind self IOCP for socket and not wait after call at all.