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, clientsThe 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; itsRecvthen 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; withTF_REUSE_SOCKETthe socket can be passed toAcceptAsyncorConnectAsyncagain.
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