Network addresses

Using network channels requires a way to represent a network address, which is done with the address and endpoint types.

Unless otherwise specified, all symbols described here are defined in <sk/net/address.hxx> and reside in the sk::net namespace.

Addresses

An address is a network address, such as an IP address. The basic type for representing an address is address<>, which is templated over an address family.

address<inet_family> addr;  // IPv4 address
address<inet6_family> addr6; // IPv6 address
address<> uaddr; // Address of any type determined at runtime

For convenience, type aliases are provided for inet_address, inet6_address and unix_address.

When templated over a specific address type, the address family is known at compile time. When used as address<>, the address family is dynamic and can only be determined at runtime. This allows both compile-time type safety when dealing with a single address family, and runtime polymorphism when dealing with multiple address families, e.g. a TCP application that needs to support both IPv4 and IPv6.

Addresses can be created from strings with make_XXX_address(). This doesn’t do DNS resolution, so an address literal is required.

inet_address ipv4_localhost = make_inet_address("127.0.0.1");
inet6_address ipv6_localhost = make_inet6_address("::1");

address<> any_address = make_address("::1");

// Because any_address is an address<>, we can reassign a different type
// of address to it.
any_address = make_address("127.0.0.1");

To determine the family of an address at runtime, use tag(). This returns the address family tag, which can be compared to the tag constants:

template<address_family af>
auto get_address_type(address<af> const &addr) {
    // We could also switch on af::tag to get the compile-time type of the
    // address.  If this is an address<>, the compile-time tag will be
    // unspecified_family::tag.

    switch (tag(addr)) {
    case inet_family::tag:
        return "IPv4 address";
    case inet6_family::tag:
        return "IPv6 address";
    case unix_family::tag:
        return "UNIX address";
    default:
        return "unknown address";
}

Addresses can be converted between address<> and family-specific types using address_cast:

auto ipv4_localhost = make_address<inet_family>("127.0.0.1");
auto any_localhost = address_cast<address<>>(ipv4_localhost);

Addresses can be converted to strings using str(), or printed directly to a stream:

auto addr = make_address("::1");
std::string s = str(addr);
std::cout << addr << '\n';
template<typename T>
concept address_family

A concept that describes an address family.

address_family represents the characteristics of a particular address family, such as IPv4 (INET), IPv6 (INET6), or UNIX sockets. The address family types are typically not used directly, but provide the template argument for address<>.

using address_family_tag = implementation_defined

An integer type that represents an address family.

struct inet_family

The inet (IPv4) address family.

struct inet6_family

The inet6 (IPv6) address family.

struct unix_family

The UNIX socket address family.

struct unspecified_family

The unspecified address family (described below).

template<address_family af = unspecified_family>
class address

An address.

address<> represents a single network address, such as an IP address or UNIX path. It can be templated on an address family, such as address<inet_family>, or the type-erased address<> can be used to store any kind of address (providing runtime polymorphism over address type).

Working with addresses

Some generic functions are provided for working with address types.

template<address_family af>
auto tag(address<af> const&) noexcept -> address_family_tag

Return the address tag for an address. For address<>, this is determined at runtime, otherwise at compile time. The address tag can be used to determine the address family, by comparing it to a tag constant such as inet_family::tag.

template<address_family family>
auto socket_address_family(address<family> const&) -> int

Return the socket address family for an address, e.g. AF_INET or AF_UNIX.

template<address_family af>
auto str(address<af> const&) -> std::string

Convert an address to a string in the canonical format. For INET and INET6, this is the standard IP address representation; for UNIX addresses, it is the path.

template<address_family family>
auto operator<<(std::ostream&, address<family> const&) -> std::ostream&

Print str(addr) to strm.

template<typename To, typename From>
auto address_cast(From &&from) -> expected<To, std::error_code>

Convert one address type to another (described below).

template<address_family af1, address_family af2>
bool operator==(address<af1> const &a, address<af2> const &b)

Compare addresses for ordering.

template<address_family af1, address_family af2>
bool operator<(address<af1> const &a, address<af2> const &b)

Compare addresses for equality.

Address types

INET addresses

An inet_address represents an IPv4 address.

    struct inet_family {
        static constexpr address_family_tag tag = /* implementation-defined */;

        static constexpr std::size_t address_size = 4;
        struct address_type {
            std::array<std::uint8_t, address_size> bytes;
        };
    };

    template <>
    class address<inet_family> {
        using address_family = inet_family;
        using address_type = address_family::address_type;

        address() noexcept;
        address(address_type const &a) : _address(a) {}
        address(address const &other) noexcept;
        auto operator=(address const &other) noexcept -> address &;

        auto value() noexcept -> address_type &
        auto value() const noexcept -> address_type const &
        auto as_bytes() const noexcept
            -> std::span<std::byte const, inet_family::address_size>
    };

}

A default-constructed inet_address stores the zero address (0.0.0.0).

value() returns the stored address as an array of bytes. as_bytes() returns the stored address as an std::span.

auto make_inet_address(std::uint32_t) -> inet_address

Create an inet_address from an std::uint32_t representing an IP address in MSB order.

auto make_inet_address(std::string const&) -> expected<inet_address, std::error_code>

Create an inet_address from a literal address string.

INET6 addresses

An inet6_address represents an IPv6 address.

namespace sk::net {

    struct inet6_family {
        static constexpr address_family_tag tag = /* implementation-defined */;

        static constexpr std::size_t address_size = 128/8;
        struct address_type {
            std::array<std::uint8_t, address_size> bytes;
        };
    };

    template <>
    class address<inet6_family> {
        using address_family = inet6_family;
        using address_type = address_family::address_type;

        auto value() noexcept -> address_type &
        auto value() const noexcept -> address_type const &
        auto as_bytes() const noexcept
            -> std::span<std::byte const, inet6_family::address_size>
    };

}

A default-constructed inet_address stores the zero address (::).

value() returns the stored address as an array of bytes. as_bytes returns the stored address as an std::span.

auto make_inet6_address(in6_addr) -> inet_address

Create an inet6_address from an in6_addr.

auto make_inet6_address(std::string const&) -> expected<inet6_address, std::error_code>

Create an inet6_address from a literal address string.

UNIX addresses

A unix_address represents a UNIX socket address.

namespace sk::net {

    struct unix_family {
        static constexpr address_family_tag tag = /* implementation-defined */;

        static constexpr std::size_t address_size = /* implementation-defined */;
        struct address_type {
            std::array<char, address_size> path;
        };
    };

    template <>
    class address<unix_family> {
        using address_family = unix_family;
        using address_type = address_family::address_type;

        auto value() noexcept -> address_type &
        auto value() const noexcept -> address_type const &
        auto as_bytes() const noexcept
            -> std::span<std::byte const>
    };

}

A default-constructed unix_address stores an empty path, which is not a valid address and cannot be connected to or bound to.

value() returns the stored address as an array. This array is always the maximum possible length; if the stored path is shorter than the maximum, it will be NUL-terminated, otherwise there will be no NUL character.

as_bytes() returns the stored address as a variable-length std::span. The span is equal to the length of the stored path and will never contain a NUL character.

auto make_unix_address(std::string const&) -> expected<std::string, std::error_code>

Create a unix_address from a string path.

auto make_unix_address(std::filesystem::path const&) -> expected<unix_address, std::error_code>

Create a unix_address from a filesystem path.

The unspecified address

An address<> (also spelled as unspecified_address) represents an address that could be an IPv4 address, an IPv6 address or a UNIX socket. address<> can be queried at runtime for the type of address it holds, converted to other address types using address_cast<>, or used directly to construct an endpoint.

namespace sk::net {

    struct unspecified_family {
        static constexpr address_family_tag tag = /* implementation-defined */;

        static constexpr std::size_t address_size = /* implementation-defined */;
        using address_type = /* implementation-defined */;
    };

    template <>
    class address<unspecified_family> {
        using address_family = unspecified_family;
        using address_type = address_family::address_type;
    };

}

A default-constructed address<> stores an undefined value.

template<>
auto make_address<unspecified_family>(std::string const&) -> expected<unspecified_address, std::error_code>

Create an address<> from a string, which should be either an INET or INET6 address literal. Creating UNIX paths with make_address() is not supported.

Zero addresses

The INET and INET6 families support the concept of a zero address, which is 0.0.0.0 or ::. The value of a default-constructed address is the zero address, and a zero address constant is also available as a static class member:

inet6_address addr; // str(addr) == "::"
auto addr2 = inet6_address::zero_address; // str(addr) == "::"
addr == addr2; // true

To create a zero address for an address<> at runtime, use make_unspecified_zero_address.

auto make_unspecified_zero_address(address_family_tag af) -> expected<unspecified_address, std::error_code>

Create an unspecified zero address for the given address family. For example, make_unspecified_zero_address(inet6_family::tag).

Converting addresses

Addresses can be converted between concrete address types and address<> using address_cast:

template<typename To, typename From>
auto address_cast(From &&from)

Convert an address from the type From to the type To.

Converting an address type to address<> always succeeds, unless address<> cannot store the given address type, in which case an error is generated at compile-time.

inet6_address addr;
address<> uaddr = address_cast<address<>>(addr); // Cannot fail

Converting an address<> to an address type may fail at runtime, depending on whether the address<> holds the requested address type.

address<> uaddr;
auto addr = address_cast<inet6_address>(uaddr);
if (addr)
    std::cout << *addr; // Conversion succeeded
else
    std::cout << addr.error().message(); // Conversion failed.

Resolving addresses

Resolving symbolic hostnames to addresses is done with a resolver type. Currently only one resolver is provided, system_resolver<>, which uses the operating system’s resolver library.

template<address_family af = unspecified_family>
class system_resolver

Resolve names using a system-specific resolver such as getaddrinfo(). Since most systems do not provide true asynchronous resolvers, this requires spawning a new thread to run the name resolution.

If system_resolver is instantiated over unspecified_family, it will return both INET and INET6 addresses. If instantiated over inet_family or inet6_family, it will only return addresses for that address family. No other address families are supported.

system_resolver does not allocate any memory on the heap and cannot throw exceptions. However, the system resolver functions usually requires a heap allocation.

auto async_resolve(std::optional<std::string> const &name = {}, std::optional<std::string> const &service = {}) const noexcept -> task<expected<__implementation_defined, std::error_code>>

Resolve the given name and service and return the results as an implementation-defined container type, which can be forwarded-iterated over to obtain the addresses. The container will contain values of type address<af>. When resolving addresses, the service parameter has no effect and may be omitted. If name is not specified, the zero address will be returned.

template<std::output_iterator<address<af>> Iterator>
auto async_resolve(Iterator &&it, std::optional<std::string> const &name = {}, std::optional<std::string> const &service = {}) const noexcept -> task<expected<void, std::error_code>>

Call async_resolve(name, service) and copy the result into the given output iterator.

Example

system_resolver<> res;

auto ret = co_await res.async_resolve("hostname.example.com");
if (ret)
    std::ranges::copy(*ret, std::ostream_iterator<address<>>(std::cout, "\n"));
else
    std::cout << ret.error().message() << '\n';

Endpoints

Connecting to a network resource, or binding a channel to accept incoming connections, requires an endpoint, which is a combination of an address (possibly the zero address) and optionally some protocol-specific additional data. For INET and INET6 channels, this is the TCP or UDP port number. UNIX endpoints do not have any additional data.

TCP endpoints

Defined in <sk/net/tcpchannel.hxx>.

class tcp_endpoint

Represents an INET or INET6 address and TCP port number.

using port_type = std::uint16_t
using address_type = address<>
using const_address_type = address<> const
auto address() const noexcept -> const_address_type&
auto address() noexcept -> address_type&

Return the endpoint’s address.

auto port() const noexcept -> port_type

Return the endpoint’s port.

auto port(port_type p) noexcept -> port_type

Change the endpoint’s port. Returns the old port.

auto as_sockaddr_storage() const noexcept -> sockaddr_storage

Return a sockaddr_storage structure representing the endpoint’s address and port.

bool operator==(tcp_endpoint const &a, tcp_endpoint const &b) noexcept

Compare two tcp_endpoint for equality.

bool operator<(tcp_endpoint const &a, tcp_endpoint const &b) noexcept

Compare two tcp_endpoint for ordering.

auto str(tcp_endpoint const &ep) -> std::string

Return a string representation of the endpoint in the canonical form. For INET endpoints this is 127.0.0.1:80; for INET6 this is [::1]:80.

template<address_family af>
auto make_tcp_endpoint(address<af> const &addr, tcp_endpoint::port_type port) noexcept

Create a TCP endpoint from an address and a port number. The address family must be inet_family, inet6_family or unspecified_family.

auto make_tcp_endpoint(std::string const &str, tcp_endpoint::port_type port) noexcept

Create a TCP endpoint from an address literal and a port number.

UNIX endpoints

Defined in <sk/net/unixchannel.hxx>.

class unix_endpoint

Represents a UNIX socket endpoint.

using address_type = unix_address
using const_address_type = unix_address const
auto address() const noexcept -> const_address_type&
auto address() noexcept -> address_type&

Return the endpoint’s address.

auto as_sockaddr_storage() const noexcept -> sockaddr_storage

Return a sockaddr_storage structure representing the endpoint’s address.

bool operator==(unix_endpoint const &a, unix_endpoint const &b) noexcept

Compare two unix_endpoint for equality.

bool operator<(unix_endpoint const &a, unix_endpoint const &b) noexcept

Compare two unix_endpoint for ordering.

auto str(unix_endpoint const &ep) -> std::string

Return the endpoint’s path as a string.

auto make_unix_endpoint(unix_address const &addr) noexcept -> expected<unix_endpoint, std::error_code>;

Create a UNIX endpoint from a UNIX address.

auto make_unix_endpoint(address<> const &addr) noexcept -> expected<unix_endpoint, std::error_code>;

Create a UNIX endpoint from an address<> which holds a UNIX address.

auto make_unix_endpoint(std::filesystem::path const &addr) noexcept -> expected<unix_endpoint, std::error_code>;
auto make_unix_endpoint(std::string const &addr) noexcept -> expected<unix_endpoint, std::error_code>;

Create a UNIX endpoint from a filesystem path.

Resolving endpoints

To resolve endpoints, use tcp_endpoint_system_resolver. This has the same interface as system_resolver, except it return tcp_endpoint objects. Note that while the service parameter to async_resolve() has no effect when resolving addresses, when resolving endpoints, it will be used to determine the endpoint’s port number. To create a listening endpoint for all addresses on the local system, use async_resolve({}, "service-name").

Example

tcp_endpoint_system_resolver res;

auto ret = co_await res.async_resolve("hostname.example.com", "http");
if (ret)
    std::ranges::copy(*ret, std::ostream_iterator<tcp_endpoint>(std::cout, "\n"));
else
    std::cout << ret.error().message() << '\n';