Preliminary. ThinkMeta ConcurrentTasks and this documentation are preliminary; everything is subject to change.

TCP sockets

AsyncTcpSocket4 and AsyncTcpSocket6 are TCP sockets for overlapped I/O through the core's completion port. Every operation comes in two forms: a waiting form (Recv, Send, Accept, Connect, ...) that suspends the calling task until the operation has completed, and a starting form (RecvAsync, SendAsync, ...) that never suspends and leaves the waiting to you. Because a waiting task only suspends, a server can afford one task per connection. The examples come from guide-tcp-sockets.cpp: an echo server on 127.0.0.1 with an ephemeral port, and clients in the same process that check the echo.

Startup

if (WinsockError::Failed(TC::Windows::Socket::Startup())) // WSAStartup and the
                                                          // extension functions
    return 1;
if (!TC::AsyncIo::EnableAsyncIo() || !TC::EnableTimers())
    return 1;

Socket::Startup calls WSAStartup for Winsock 2.2 and loads the extension functions (AcceptEx, ConnectEx, DisconnectEx, ...) that the asynchronous operations call through. Call it once per process before any socket is created, and balance each successful call with Socket::Cleanup. The sockets also need EnableAsyncIo, and EnableTimers if you use timeouts. Link with ws2_32.lib.

Important

Create asynchronous sockets only through AsyncSocket::Create, which the TCP classes' Create() calls: it creates an overlapped socket and associates it with the core's completion port. A socket created any other way never sees its completions.

Listening

The listening socket is created, bound and put into listening state with ordinary Winsock calls:

sockaddr_in address{};
address.sin_family      = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
address.sin_port        = 0; // any free port
if (WinsockError::Failed(server.listener.Create())
    || WinsockError::Failed(server.listener.Bind(address))
    || WinsockError::Failed(server.listener.GetSockName(address))
    || WinsockError::Failed(server.listener.Listen()))
    return 1;

Create() without arguments creates a TCP socket of the version's address family. Bind with port 0 picks a free port, which GetSockName reports. Listen uses SOMAXCONN as the default backlog. These plain calls do not suspend; a blocking Winsock call would block the OS thread, so use the asynchronous forms for anything that waits.

An accept loop with a task per connection

The server keeps a fixed number of connection tasks, and all tasks of the program join one task group:

constexpr int MaxConnections = 16; // connection tasks the server keeps

TC::Synchronization::TaskGroup_s
    tasks; // all tasks of the program: server, connections, clients

The server task accepts a connection, hands the socket to a connection task and accepts the next one, until no client has connected for half a second:

// The accept loop: accept, hand the socket to a connection task, accept the next
// one. The server stops when all connection tasks are used or when no client
// connects for half a second.
class Server : public TC::Tasks::Task
{
public:
    AsyncTcpSocket4 listener; // created, bound and listening before the task starts
    Connection      connections[MaxConnections];

    void Run() noexcept override
    {
        for (Connection& connection : connections) {
            const WinsockErrorCode error = listener.Accept(
                connection.socket, nullptr, TC::Duration::Milliseconds(500));
            if (WinsockError::Failed(error))
                break;                  // WinsockError::TimedOut, or another error
            tasks.AddTask(&connection); // a task of the group may add tasks to it
            connection.Schedule();
        }
        listener.Close();
    }
};

Accept takes an object that does not hold a socket yet, creates the socket for the connection, waits for a connection with AcceptEx and then sets SO_UPDATE_ACCEPT_CONTEXT, so that getpeername, shutdown and the other calls work on the new socket. An optional pointer receives the peer's address. On failure the new socket is closed again.

Each connection task echoes until the peer shuts down its sending side:

// One task per connection: echo everything until the peer shuts down its side.
class Connection : public TC::Tasks::Task
{
public:
    AsyncTcpSocket4 socket; // filled by Accept

    void Run() noexcept override
    {
        char buffer[16 * 1024];
        for (;;) {
            DWORD            received = 0;
            WinsockErrorCode error = socket.Recv(buffer, sizeof(buffer), &received);
            if (WinsockError::Failed(error) || received == 0)
                break; // error, or 0 bytes: the peer has shut down sending
            DWORD sent = 0;
            error      = socket.Send(buffer, received, &sent);
            if (WinsockError::Failed(error) || sent != received)
                break;
        }
        socket.Shutdown(SD_SEND); // no more data from us
        socket.Close(); // nothing is pending: both waiting calls have returned
    }
};

A Recv that succeeds with 0 bytes means that the peer has closed or shut down its side of the connection. Each connection task owns its socket; the socket objects are movable, so you can also move an accepted socket into a task object.

Important

Connection tasks are ordinary tasks: the object must live from scheduling until its OnTaskExited. The example keeps a fixed array of them and uses each once; a long-running server needs a pool or another owner that reuses or frees a task object only after it has exited. See Lifetimes and sizing.

Connecting, sending and receiving

Connect uses ConnectEx, which needs a bound socket: bind it first, for example to port 0. It sets SO_UPDATE_CONNECT_CONTEXT after the connection.

The waiting forms Send and Recv keep one operation in flight. The client below needs two at once: if it sent its whole message before reading, the server's sends could fill the client's receive buffer and both sides would wait for each other. So it starts the send with SendAsync and receives the echo while the send is in flight:

// Sends a message and receives the echo at the same time: the send runs as an
// overlapped operation with its own request while the task waits in Recv.
bool Echo(const sockaddr_in& server, std::span<const char> message,
          std::span<char> echo) noexcept
{
    AsyncTcpSocket4 socket;
    sockaddr_in     local{};
    local.sin_family      = AF_INET;
    local.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    if (WinsockError::Failed(socket.Create())
        || WinsockError::Failed(
            socket.Bind(local))) // ConnectEx needs a bound socket
        return false;
    if (WinsockError::Failed(socket.Connect(server, TC::Duration::Seconds(5))))
        return false;

    TC::AsyncIo::WaitableAsyncSocketIoRequest_s sendRequest;
    DWORD                                       sent  = 0;
    const WinsockErrorCode                      start = socket.SendAsync(
        message.data(), static_cast<std::uint32_t>(message.size()), &sent,
        sendRequest);
    if (WinsockError::Failed(start) && !WinsockError::IsPending(start))
        return false; // not started: never wait for this request

    std::size_t received = 0;
    while (received < echo.size()) {
        DWORD n = 0;
        if (WinsockError::Failed(
                socket.Recv(echo.data() + received,
                            static_cast<std::uint32_t>(echo.size() - received), &n,
                            TC::Duration::Seconds(5)))
            || n == 0)
            break;
        received += n;
    }
    if (received < echo.size())
        socket
            .CancelAllAsyncRequests(); // giving up: the send must not stay pending
    sendRequest.Wait(); // the request must not go away while the send is pending

    socket.Shutdown(SD_SEND); // the server's Recv returns 0 bytes now
    return WinsockError::Succeeded(sendRequest.GetError())
        && received == echo.size();
}

If the echo does not arrive completely, Echo cancels the send before it waits for the request, so that the wait cannot hang on a send that the server no longer reads.

main starts the server task like any other task; the clients are tasks that call Echo with the address the listener reported:

tasks.AddTask(&server);
scheduler->AddTask(&server);

The result of a starting call decides what happens next:

Result Meaning
WinsockError::Success Completed synchronously; the request has already been completed on the calling thread.
WinsockError::Pending In flight; the request completes later on the core's I/O thread.
any other code Failed synchronously; the request is not completed. Do not wait for it.

Important

The request and every buffer passed to a starting call belong to you and must stay valid, and in place, until the request has completed. Reuse a request only after it has completed and after Reset. Several operations may be outstanding on one socket at once, each with its own request, started from any tasks or threads.

WaitableAsyncSocketIoRequest_s allows one waiting task and Wait(Duration); once its Wait has returned it may be destroyed at once. WaitableAsyncSocketIoRequest allows several waiting tasks, and the Recv variants (WaitableAsyncSocketRecvIoRequest_s) also capture the receive flags.

Timeouts

Every waiting form takes an optional timeout (default Duration::Infinite). When it expires, the operation is cancelled with CancelIoEx, the call waits until the cancellation has completed, and it returns WinsockError::TimedOut (WSAETIMEDOUT). If the operation completed before the cancellation took effect, its own result stands. The server above uses this to stop once no client has connected for half a second.

Important

After a cancelled operation a stream socket is fit for closing only: data may have been lost. A finite timeout needs EnableTimers; without it the wait has no limit.

The Wait(Duration) of a request, by contrast, does not cancel anything: after it returns false the operation is still pending and the request must stay alive.

Shutting down and closing

  • Shutdown(SD_SEND) tells the peer that no more data follows; its Recv then returns 0 bytes. The clients and the connection tasks both use it.
  • Close closes the socket; the destructor does the same. Closing aborts pending operations, which then complete with WinsockError::OperationAborted.
  • Disconnect uses DisconnectEx; with TF_REUSE_SOCKET the socket can be passed to AcceptAsync or ConnectAsync again.

Important

Close or destroy a socket only when no operation on it is pending: cancel them with CancelAsyncRequest or CancelAllAsyncRequests and wait for their completions first. Both only request the cancellation; the requests and buffers must stay alive until the operations have completed. At the end, the last DisableAsyncIo requires that no operation is pending in the whole process.

TC::DisableTimers();
TC::AsyncIo::DisableAsyncIo(); // all sockets are closed, nothing is pending
TC::Windows::Socket::Cleanup();

Errors

All socket functions return a WinsockErrorCode: 0 for success or a WSA* code; nothing throws. WinsockError names the common codes and offers Succeeded, Failed and IsPending; note that Failed is also true for Pending. The waiting forms set the byte count to 0 on failure. For callers who prefer exceptions, WinsockErrorException wraps a code.

The program prints:

clients with a correct echo: 8 of 8
OK

See also