1

I have an IOCP based client for which I wanted to implement HTTP redirects in a following way:

1) When encountering a redirect to a different host call DisconnectEx with TF_REUSE_SOCKET

2) Await the overlapped completion and then call ConnectEx

However my code gets a WSAEISCONN return code upon calling ConnectEx even though if I check GetLastError() when the overlapped result of DisconnectEx is returned it gives 0.

An MVCE would be quite big in this case, but if there are no experience based suggestions I'll post one.

Update

I tried to make a MVCE but encountered different symptoms:

#define VC_EXTRALEAN
#define WIN32_LEAN_AND_MEAN

#include <WinSock2.h>
#include <MSWSock.h>
#include <Windows.h>
#include <Ws2tcpip.h>

#pragma comment(lib, "ws2_32.lib")

#include <stdexcept>
#include <iostream>
#include <sstream>
#include <map>

static inline std::string ErrorMessage(const char* pErrorMessage, ...)
{
    std::string sFormattedMessage;
    va_list VariableArgumentList;
    va_start(VariableArgumentList, pErrorMessage);
    sFormattedMessage.resize(_vscprintf(pErrorMessage, VariableArgumentList) + 1);
    vsnprintf_s(const_cast<char*>(sFormattedMessage.c_str()), sFormattedMessage.size(), sFormattedMessage.size(), pErrorMessage, VariableArgumentList);
    va_end(VariableArgumentList);
    return sFormattedMessage;
}

#define CHECK(x, format, ...) { if ((x) == false)  throw std::runtime_error(ErrorMessage("%s(%d): "format, __FILE__, __LINE__, __VA_ARGS__)); }

template<typename T>
bool LoadWinsockExtensionFunction(SOCKET Socket, GUID Guid, T* pFunction)
{
    DWORD nBytesReturned = 0;
    return WSAIoctl(Socket, SIO_GET_EXTENSION_FUNCTION_POINTER, &Guid, sizeof(Guid), pFunction, sizeof(T), &nBytesReturned, NULL, NULL) == 0 && nBytesReturned == sizeof(T);
}

int main(int argc, char** argv)
{
    try
    {
        WORD nRequestedWinsockVersion = MAKEWORD(2, 2);
        WSADATA WsaData;
        CHECK(WSAStartup(nRequestedWinsockVersion, &WsaData) == 0, "WSAStartup failed");
        auto hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 0);
        CHECK(hCompletionPort != NULL, "CreateIoCompletionPort failed(%d)", GetLastError());
        auto Socket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
        CHECK(Socket != INVALID_SOCKET, "WSASocket failed(%d)", WSAGetLastError());
        CHECK(CreateIoCompletionPort(reinterpret_cast<HANDLE>(Socket), hCompletionPort, NULL, 0), "CreateIoCompletionPort failed(%d)", GetLastError());
        sockaddr_in LocalAddress;
        ZeroMemory(&LocalAddress, sizeof(LocalAddress));
        LocalAddress.sin_family = AF_INET;
        LocalAddress.sin_addr.s_addr = INADDR_ANY;
        LocalAddress.sin_port = 0;
        CHECK(bind(Socket, reinterpret_cast<SOCKADDR*>(&LocalAddress), sizeof(LocalAddress)) == 0, "bind failed(%d)", WSAGetLastError());
        LPFN_CONNECTEX pConnectEx = nullptr;
        CHECK(LoadWinsockExtensionFunction(Socket, WSAID_CONNECTEX, &pConnectEx), "WSAIoctl failed to load ConnectEx(%d)", WSAGetLastError());
        LPFN_DISCONNECTEX pDisconnectEx = nullptr;
        CHECK(LoadWinsockExtensionFunction(Socket, WSAID_DISCONNECTEX, &pDisconnectEx), "WSAIoctl failed to load DisconnectEx(%d)", WSAGetLastError());
        addrinfo Hint;
        ZeroMemory(&Hint, sizeof(Hint));
        Hint.ai_family = AF_INET;
        Hint.ai_protocol = IPPROTO_TCP;
        Hint.ai_socktype = SOCK_STREAM;
        std::map<std::string, PADDRINFOA> Hosts;
        // Scenarios:
        // one host - failure to connect on the second try to the first host with 52
        // two distinct hosts - failure to connect on the second try to the first host with 52
        Hosts.emplace("www.google.com", nullptr);
        Hosts.emplace("www.facebook.com", nullptr);
        for (auto& Host : Hosts)
        {
            auto nGetAddressInfoResult = getaddrinfo(Host.first.c_str(), "http", &Hint, &Host.second);
            CHECK(nGetAddressInfoResult == 0 && &Host.second, "getaddrinfo failed(%d)", nGetAddressInfoResult);
        }
        auto Host = Hosts.begin();
        WSAOVERLAPPED Overlapped;
        ZeroMemory(&Overlapped, sizeof(Overlapped));
        while (true)
        {
            if ((*pConnectEx)(Socket, Host->second->ai_addr, Host->second->ai_addrlen, nullptr, 0, nullptr, &Overlapped) == FALSE)
            {
                auto nWSAError = WSAGetLastError();
                CHECK(nWSAError == ERROR_IO_PENDING, "ConnectEx failed(%d)", nWSAError);
                DWORD nBytesTransferred = 0;
                ULONG_PTR pCompletionKey = 0;
                LPOVERLAPPED pOverlapped = nullptr;
                CHECK(GetQueuedCompletionStatus(hCompletionPort, &nBytesTransferred, &pCompletionKey, &pOverlapped, INFINITE), "overlapped operation failed(%d)", GetLastError());
            }
            CHECK(setsockopt(Socket, SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT, NULL, 0) == 0, "setsockopt failed(%d)", WSAGetLastError());
            CHECK(shutdown(Socket, SD_BOTH) == 0, "shutdown failed(%d)", WSAGetLastError());
            if ((*pDisconnectEx)(Socket, &Overlapped, TF_REUSE_SOCKET, 0) == FALSE)
            {
                auto nWSAError = WSAGetLastError();
                CHECK(nWSAError == ERROR_IO_PENDING, "ConnectEx failed(%d)", nWSAError);
                DWORD nBytesTransferred = 0;
                ULONG_PTR pCompletionKey = 0;
                LPOVERLAPPED pOverlapped = nullptr;
                CHECK(GetQueuedCompletionStatus(hCompletionPort, &nBytesTransferred, &pCompletionKey, &pOverlapped, INFINITE), "overlapped operation failed(%d)", GetLastError());
            }
            if (++Host == Hosts.end())
            {
                Host = Hosts.begin();
            }
        }
        closesocket(Socket);
        CloseHandle(hCompletionPort);
        for (auto& Host : Hosts)
        {
            freeaddrinfo(Host.second);
        }
        WSACleanup();
    }
    catch (std::exception& Exception)
    {
        OutputDebugStringA(Exception.what());
        std::cout << Exception.what();
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

With this snippet what happens is that I get ERROR_DUP_NAME on the second attempt to connect to the same host (TIME_WAIT possibly?). I assume that if I could rebind the socket (but I can't since a second bind call fails with WSAEINVAL which is correct since the socket is already bound) this could even work fine.

What I have in my original code is a redirect from localhost to the actual address of the interface - maybe there simply is a path which in that case gives out WSAEISCONN instead of ERROR_DUP_NAME ? I can't post an MVCE for the original code since then another piece of code that would accept the connections is needed (maybe I'll make one).

I actually found that if DisconnectEx is called by the client then yes, ERROR_DUP_NAME happens because of TIME_WAIT (see this for a detailed analysis). So the bottom line is I should simply not try to reuse sockets in this scenario.

Rudolfs Bundulis
  • 11,636
  • 6
  • 33
  • 71
  • Per the [`ConnectEx` documentation](https://msdn.microsoft.com/en-us/library/windows/desktop/ms737606.aspx): "***Any attempt to reconnect an active connection will fail with the error code WSAEISCONN**... If the `DisconnectEx` function is called with the `TF_REUSE_SOCKET` flag, **the specified socket is returned to a state in which it is not connected**, but still bound. In such cases, the handle of the socket can be passed to the `ConnectEx` function in its `s` parameter.*"... – Remy Lebeau May 11 '16 at 19:48
  • ... are you *absolutely sure* you are waiting for `DisconnectEx()` to fully finish before calling `ConnectEx()`? Are you *absolutely sure* you are passing the correct `SOCKET` from `DisconnectEx()` to `ConnectEx()`? – Remy Lebeau May 11 '16 at 19:49
  • @RemyLebeau, yes I am:) But after thorough reading of MSDN I have stumbled upon the fact that maybe I still need to call `shutdown` before `DisconnectEx` (I was not doing that since it is mentioned nowhere in the `DisconnectEx` page). Will try that tomorrow. – Rudolfs Bundulis May 11 '16 at 21:54
  • Reading the documentation, it's not entirely clear to me how `TIME_WAIT` is implemented. If `DisconnectEx` returns while the socket is still in the `TIME_WAIT` state then you won't be able to connect to a different host using that socket until the wait delay has expired. – Harry Johnston May 11 '16 at 22:32
  • @HarryJohnston that is another thing I stumbled upon while reading MSDN, but since there is no clear way to check if the TIME_WAIT period has elapsed I'll just hope `shutdown` will do the trick. – Rudolfs Bundulis May 11 '16 at 23:09
  • @HarryJohnston I tried to make an MVCE and it seems that when I use two distinct hosts (in my case the redirect was from localhost to the physical address of the interface) I get `ERROR_DUP_NAME` which is different and could be related to `TIME_WAIT`. – Rudolfs Bundulis May 12 '16 at 16:41

0 Answers0