Vix.cpp v2.7.0 is here Read the blog
Skip to content

TCP

This guide shows how to use async TCP with Vix Async.

Use this page when you want to connect to a TCP server, read and write bytes, or accept incoming TCP connections from an async server.

Public header

cpp
#include <vix/async.hpp>
#include <vix/print.hpp>

What TCP provides

The TCP API is built around two interfaces.

TypePurpose
tcp_streamRepresents a connected TCP stream.
tcp_listenerRepresents a listening TCP socket.

A TCP stream can connect to a remote endpoint, read bytes, write bytes, close the connection, and expose the native socket handle when supported.

A TCP listener can listen on a local endpoint, accept incoming connections, and close the listener.

TCP endpoint

A TCP endpoint has a host and a port.

cpp
vix::async::net::tcp_endpoint endpoint{
    "127.0.0.1",
    8080
};

You can use an IP address or a hostname:

cpp
vix::async::net::tcp_endpoint endpoint{
    "example.com",
    80
};

Create a TCP stream

Use make_tcp_stream with an io_context.

cpp
auto stream = vix::async::net::make_tcp_stream(ctx);

The stream is attached to the async runtime.

Connect to a server

Use async_connect.

cpp
co_await stream->async_connect({
    "example.com",
    80
});

Complete example:

cpp
#include <vix/async.hpp>
#include <vix/print.hpp>

#include <exception>

vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
  try
  {
    auto stream = vix::async::net::make_tcp_stream(ctx);

    co_await stream->async_connect({
        "example.com",
        80
    });

    vix::print("connected =", stream->is_open() ? "yes" : "no");

    stream->close();
  }
  catch (const std::exception &ex)
  {
    vix::eprint(ex.what());
  }

  ctx.stop();
  co_return;
}

int main()
{
  vix::async::core::io_context ctx;

  auto t = app(ctx);
  std::move(t).start(ctx.get_scheduler());

  ctx.run();

  return 0;
}

Write to a TCP stream

Use async_write with a byte buffer.

cpp
std::string request = "hello";

auto bytes = co_await stream->async_write(
    std::as_bytes(std::span{request}));

Example:

cpp
#include <vix/async.hpp>
#include <vix/print.hpp>

#include <exception>
#include <span>
#include <string>

vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
  try
  {
    auto stream = vix::async::net::make_tcp_stream(ctx);

    co_await stream->async_connect({
        "127.0.0.1",
        8080
    });

    std::string message = "hello from Vix";

    const std::size_t bytes = co_await stream->async_write(
        std::as_bytes(std::span{message}));

    vix::print("written =", bytes);

    stream->close();
  }
  catch (const std::exception &ex)
  {
    vix::eprint(ex.what());
  }

  ctx.stop();
  co_return;
}

int main()
{
  vix::async::core::io_context ctx;

  auto t = app(ctx);
  std::move(t).start(ctx.get_scheduler());

  ctx.run();

  return 0;
}

Read from a TCP stream

Use async_read with a mutable byte buffer.

cpp
std::array<std::byte, 1024> buffer{};

const std::size_t bytes = co_await stream->async_read(
    std::span<std::byte>{buffer.data(), buffer.size()});

Example:

cpp
#include <vix/async.hpp>
#include <vix/print.hpp>

#include <array>
#include <exception>
#include <span>
#include <string>

vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
  try
  {
    auto stream = vix::async::net::make_tcp_stream(ctx);

    co_await stream->async_connect({
        "127.0.0.1",
        8080
    });

    std::array<std::byte, 1024> buffer{};

    const std::size_t bytes = co_await stream->async_read(
        std::span<std::byte>{buffer.data(), buffer.size()});

    std::string text;
    text.resize(bytes);

    for (std::size_t i = 0; i < bytes; ++i)
    {
      text[i] = static_cast<char>(buffer[i]);
    }

    vix::print("received =", text);

    stream->close();
  }
  catch (const std::exception &ex)
  {
    vix::eprint(ex.what());
  }

  ctx.stop();
  co_return;
}

int main()
{
  vix::async::core::io_context ctx;

  auto t = app(ctx);
  std::move(t).start(ctx.get_scheduler());

  ctx.run();

  return 0;
}

HTTP request example

TCP streams can be used to write simple protocols.

This example sends a minimal HTTP request.

cpp
#include <vix/async.hpp>
#include <vix/print.hpp>

#include <array>
#include <exception>
#include <span>
#include <string>

vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
  try
  {
    auto stream = vix::async::net::make_tcp_stream(ctx);

    co_await stream->async_connect({
        "example.com",
        80
    });

    std::string request =
        "GET / HTTP/1.1\r\n"
        "Host: example.com\r\n"
        "Connection: close\r\n"
        "\r\n";

    const std::size_t written = co_await stream->async_write(
        std::as_bytes(std::span{request}));

    vix::print("written =", written);

    std::array<std::byte, 4096> buffer{};

    const std::size_t read = co_await stream->async_read(
        std::span<std::byte>{buffer.data(), buffer.size()});

    std::string response;
    response.resize(read);

    for (std::size_t i = 0; i < read; ++i)
    {
      response[i] = static_cast<char>(buffer[i]);
    }

    vix::print(response);

    stream->close();
  }
  catch (const std::exception &ex)
  {
    vix::eprint(ex.what());
  }

  ctx.stop();
  co_return;
}

int main()
{
  vix::async::core::io_context ctx;

  auto t = app(ctx);
  std::move(t).start(ctx.get_scheduler());

  ctx.run();

  return 0;
}

Create a TCP listener

Use make_tcp_listener.

cpp
auto listener = vix::async::net::make_tcp_listener(ctx);

Then listen on an endpoint.

cpp
co_await listener->async_listen({
    "127.0.0.1",
    8080
});

Accept a connection

Use async_accept.

cpp
auto client = co_await listener->async_accept();

The accepted connection is returned as a std::unique_ptr<tcp_stream>.

Minimal TCP server

cpp
#include <vix/async.hpp>
#include <vix/print.hpp>

#include <array>
#include <exception>
#include <span>
#include <string>

vix::async::core::task<void> server(vix::async::core::io_context &ctx)
{
  try
  {
    auto listener = vix::async::net::make_tcp_listener(ctx);

    co_await listener->async_listen({
        "127.0.0.1",
        8080
    });

    vix::print("listening on 127.0.0.1:8080");

    auto client = co_await listener->async_accept();

    vix::print("client accepted");

    std::string message = "hello from Vix TCP\n";

    (void)co_await client->async_write(
        std::as_bytes(std::span{message}));

    client->close();
    listener->close();
  }
  catch (const std::exception &ex)
  {
    vix::eprint(ex.what());
  }

  ctx.stop();
  co_return;
}

int main()
{
  vix::async::core::io_context ctx;

  auto t = server(ctx);
  std::move(t).start(ctx.get_scheduler());

  ctx.run();

  return 0;
}

Test from another terminal:

nc 127.0.0.1 8080

Expected output:

hello from Vix TCP

Echo server

This example accepts one client, reads data, and writes it back.

cpp
#include <vix/async.hpp>
#include <vix/print.hpp>

#include <array>
#include <exception>
#include <span>

vix::async::core::task<void> server(vix::async::core::io_context &ctx)
{
  try
  {
    auto listener = vix::async::net::make_tcp_listener(ctx);

    co_await listener->async_listen({
        "127.0.0.1",
        8080
    });

    vix::print("echo server ready");

    auto client = co_await listener->async_accept();

    std::array<std::byte, 1024> buffer{};

    const std::size_t bytes = co_await client->async_read(
        std::span<std::byte>{buffer.data(), buffer.size()});

    (void)co_await client->async_write(
        std::span<const std::byte>{buffer.data(), bytes});

    client->close();
    listener->close();
  }
  catch (const std::exception &ex)
  {
    vix::eprint(ex.what());
  }

  ctx.stop();
  co_return;
}

int main()
{
  vix::async::core::io_context ctx;

  auto t = server(ctx);
  std::move(t).start(ctx.get_scheduler());

  ctx.run();

  return 0;
}

Test:

printf "hello\n" | nc 127.0.0.1 8080

Expected output:

hello

Cancellation

TCP operations accept a cancel_token.

cpp
vix::async::core::cancel_source source;

co_await stream->async_connect(endpoint, source.token());

You can use cancellation with:

cpp
co_await stream->async_connect(endpoint, token);
co_await stream->async_read(buffer, token);
co_await stream->async_write(buffer, token);
co_await listener->async_accept(token);

Example:

cpp
#include <vix/async.hpp>
#include <vix/print.hpp>

#include <chrono>
#include <exception>
#include <system_error>

using namespace std::chrono_literals;

vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
  vix::async::core::cancel_source source;

  ctx.timers().after(100ms, [&source]()
  {
    source.request_cancel();
  });

  try
  {
    auto stream = vix::async::net::make_tcp_stream(ctx);

    co_await stream->async_connect({
        "10.255.255.1",
        80
    }, source.token());

    stream->close();
  }
  catch (const std::system_error &ex)
  {
    vix::eprint("tcp cancelled or failed:", ex.code().message());
  }
  catch (const std::exception &ex)
  {
    vix::eprint(ex.what());
  }

  ctx.stop();
  co_return;
}

int main()
{
  vix::async::core::io_context ctx;

  auto t = app(ctx);
  std::move(t).start(ctx.get_scheduler());

  ctx.run();

  return 0;
}

Close a stream

Use close() when the connection is no longer needed.

cpp
stream->close();

Check whether it is open:

cpp
vix::print("open =", stream->is_open() ? "yes" : "no");

Close a listener

Use close() when the listener should stop accepting new connections.

cpp
listener->close();

Check whether it is open:

cpp
vix::print("listening =", listener->is_open() ? "yes" : "no");

Native handle

tcp_stream::native_handle() returns the native socket handle when supported.

cpp
const int fd = stream->native_handle();

This is mainly useful for lower-level integrations such as TLS adapters. Most application code should not need it.

TCP lifecycle

Client flow:

create io_context
create tcp_stream
connect
write or read
close stream
stop context

Server flow:

create io_context
create tcp_listener
listen
accept client
read or write
close client
close listener
stop context

TCP API summary

APIPurpose
make_tcp_stream(ctx)Creates a TCP stream.
make_tcp_listener(ctx)Creates a TCP listener.
async_connect(endpoint, token)Connects to a remote endpoint.
async_read(buffer, token)Reads bytes from a stream.
async_write(buffer, token)Writes bytes to a stream.
async_listen(endpoint, backlog)Starts listening on a local endpoint.
async_accept(token)Accepts one incoming connection.
close()Closes a stream or listener.
is_open()Checks open state.
native_handle()Returns native socket handle when supported.

Common workflows

Connect to a server

cpp
auto stream = vix::async::net::make_tcp_stream(ctx);

co_await stream->async_connect({
    "example.com",
    80
});

Write bytes

cpp
std::string message = "hello";

const std::size_t bytes = co_await stream->async_write(
    std::as_bytes(std::span{message}));

Read bytes

cpp
std::array<std::byte, 1024> buffer{};

const std::size_t bytes = co_await stream->async_read(
    std::span<std::byte>{buffer.data(), buffer.size()});

Listen for one connection

cpp
auto listener = vix::async::net::make_tcp_listener(ctx);

co_await listener->async_listen({
    "127.0.0.1",
    8080
});

auto client = co_await listener->async_accept();

Close resources

cpp
client->close();
listener->close();

Common mistakes

Forgetting to run the context

Wrong:

cpp
auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());

Correct:

cpp
auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());

ctx.run();

Forgetting to stop the context

After the TCP flow is complete, stop the runtime.

cpp
ctx.stop();
co_return;

Using TCP APIs outside a coroutine

Wrong:

cpp
stream->async_connect({"127.0.0.1", 8080});

Correct:

cpp
co_await stream->async_connect({"127.0.0.1", 8080});

Passing a text buffer directly to async_write

async_write expects bytes.

Wrong:

cpp
co_await stream->async_write(message);

Correct:

cpp
co_await stream->async_write(
    std::as_bytes(std::span{message}));

Reading into an immutable buffer

async_read needs a mutable byte span.

cpp
std::array<std::byte, 1024> buffer{};

const std::size_t bytes = co_await stream->async_read(
    std::span<std::byte>{buffer.data(), buffer.size()});

Forgetting to close sockets

Close streams and listeners when you are done.

cpp
stream->close();
listener->close();

Expecting async_read to fill the whole buffer

async_read returns the number of bytes actually read. Use the returned byte count.

Best practices

Use tcp_stream for outgoing client connections. Use tcp_listener for incoming server connections. Keep buffers alive until the awaited operation completes. Use std::as_bytes for write buffers. Use mutable std::span<std::byte> for read buffers. Handle std::system_error around network operations. Use cancellation tokens for long-running connect, read, write, and accept operations. Close streams and listeners explicitly when finished. Stop the io_context when the top-level TCP flow is complete.

PagePurpose
io_contextLearn the runtime context.
TasksLearn coroutine tasks.
TimersLearn timeout-style flows.
CancellationLearn cancellation tokens.
UDPLearn UDP sockets.
DNSLearn hostname resolution.
API ReferenceSee the public API surface.

Next step

Continue with UDP.

Released under the MIT License.