<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[【 基于 io_uring 的 C++20 协程网络库】05 Protocol与Endpoint的封装]]></title><description><![CDATA[<p dir="auto">在构建了底层的异步 I/O 引擎（<code>IOContext</code>）与核心的 Awaiter 机制后，我们的网络库已经具备了处理并发事件的能力。但要让它真正与网络世界通信，我们必须跨越网络编程的第一道门槛：<strong>地址与端点（Endpoint）的封装</strong>。</p>
<p dir="auto">在原生的 POSIX C API 中，网络地址的表示极其繁琐。开发者需要手动处理 <code>sockaddr_in</code>（IPv4）、<code>sockaddr_in6</code>（IPv6）甚至 <code>sockaddr_un</code>（Unix Domain Socket），并充斥着各种宏与危险的指针强制转换。</p>
<p dir="auto">本章的目标是：从零开始，一步步利用 C++20 的语言特性，构建出一套强类型、内存安全且<strong>零运行时开销</strong>的 Endpoint 体系。（也可以看做对asio的cosplay）</p>
<p dir="auto">我们最终期望的业务层 API 形态，应该是极致简洁的：</p>
<pre><code class="language-cpp">// 期望的现代 C++ 用法：自动推导，透明解析
auto ep1 = ip::tcp::endpoint::from_string("127.0.0.1", 12345); 
auto ep2 = ip::tcp::endpoint::from_string("::1", 12345);       
</code></pre>
<p dir="auto">要实现这一目标，我们需要拆解三个核心概念：协议（Protocol）、IP 地址（Address）与端点（Endpoint）。</p>
<hr />
<h3>1. 协议抽象：静态特征与运行期状态的权衡</h3>
<p dir="auto">任何网络通信都需要指定协议。在 C++ 中，为了追求性能，我们通常倾向于将一切可能的信息在编译期固化。对于 <code>Protocol</code>，第一时间，我们可能会设计出一个如下的纯模板类：</p>
<pre><code class="language-cpp">// 纯静态协议封装（存在局限性）
template&lt;int Domain, int Type, int Protocol&gt;
struct BasicProtocol {
    static constexpr int domain = Domain;
    static constexpr int type = Type;
    static constexpr int protocol = Protocol;
};
</code></pre>
<p dir="auto">对于 <code>Type</code>（如 <code>SOCK_STREAM</code> 代表 TCP）和 <code>Protocol</code>（如 <code>IPPROTO_TCP</code>），它们确实是静态不变的。然而，<code>Domain</code>（地址族，即 <code>AF_INET</code> 或 <code>AF_INET6</code>）在现代网络编程中，<strong>并非总能在编译期绝对固化</strong>。</p>
<p dir="auto">考虑一个支持 <strong>双栈(Dual-Stack)</strong> 的服务器：当你创建一个监听 <code>::</code> 的 Acceptor 时，它在运行期需要同时处理 IPv4 和 IPv6 的接入。如果 <code>Domain</code> 被彻底写死在模板参数中，我们将无法用单一的泛型类型来描述这个 Acceptor。</p>
<p dir="auto">因此，正确的设计哲学是：<strong>固化协议的本原特征，保留地址族的运行期决议能力</strong>。</p>
<p dir="auto">以下是我们对 <code>ip::tcp</code> 的完整实现：</p>
<pre><code class="language-cpp">namespace ip {
class tcp {
public:
    // 1. 本原特征：使用 consteval 强制在编译期求值，拒绝任何运行时状态的介入
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; consteval auto type() const noexcept -&gt; int { return SOCK_STREAM; }
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; consteval auto protocol() const noexcept -&gt; int { return IPPROTO_TCP; }

    // 2. 运行期特征：使用 constexpr，允许在运行时根据双栈需求进行动态切换
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; constexpr auto domain() const noexcept -&gt; int { return domain_; }

    // 具名构造器，语义清晰
    static auto v4() noexcept -&gt; tcp { return tcp{ AF_INET }; }
    static auto v6() noexcept -&gt; tcp { return tcp{ AF_INET6 }; }

private:
    int domain_ = AF_INET;
    explicit tcp(int domain) : domain_{ domain } {}
};
} // namespace ip
</code></pre>
<p dir="auto">通过将 <code>type()</code> 和 <code>protocol()</code> 声明为 <code>consteval</code>，我们在编译器层面建立了一条严格的契约，这在后续作为 Type Traits 提取协议参数时，提供了与静态常量完全一致的零开销保证。</p>
<h3>2. IP 地址封装</h3>
<p dir="auto">端点是由 IP 地址和端口组成的。在封装端点之前，我们需要先解决繁琐的 IP 地址解析。</p>
<p dir="auto">在 POSIX 中，IPv4 被存储为 4 字节的 <code>in_addr</code>，IPv6 被存储为 16 字节的 <code>in6_addr</code>。</p>
<p dir="auto">我们首先利用 RAII 将它们分别封装，并隐藏丑陋的 <code>inet_pton</code>（字符串转网络字节序）系统调用。</p>
<p dir="auto">以 <code>AddressV4</code> 为例：</p>
<pre><code class="language-cpp">struct AddressV4 {
    using address_type = in_addr;
    address_type address{};

    // 字符串解析工厂
    static auto from_string(std::string_view address) -&gt; AddressV4 {
        AddressV4 result;
        if (::inet_pton(AF_INET, address.data(), &amp;result.address) != 1)
            throw_system_error("Failed to convert string to IPv4 address");
        return result;
    }

    // 格式化输出
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; auto to_string() const -&gt; std::string {
        std::string buffer(INET_ADDRSTRLEN, '\0');
        if (::inet_ntop(AF_INET, &amp;address, buffer.data(), INET_ADDRSTRLEN) == nullptr)
            throw_system_error("Failed to convert IPv4 address to string");
        return buffer;
    }
};
// AddressV6 的实现高度对称，此处省略
</code></pre>
<p dir="auto"><strong>统一抽象：构建 Address 类</strong></p>
<p dir="auto">在业务代码中，我们不希望用户去手动 <code>if/else</code> 判断当前字符串是 V4 还是 V6。我们需要一个统一的 <code>Address</code> 类来容纳它们。</p>
<p dir="auto">此时，<strong><code>std::variant</code></strong> 成为了最完美的工具。我们可以给出如下实现：</p>
<pre><code class="language-cpp">class Address {
public:
    using address_type = std::variant&lt;AddressV4, AddressV6&gt;;

    Address() = default;
    Address(const AddressV4&amp; ipv4) : address_{ ipv4 } {}
    Address(const AddressV6&amp; ipv6) : address_{ ipv6 } {}

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; constexpr auto is_v4() const noexcept -&gt; bool {
        return std::holds_alternative&lt;AddressV4&gt;(address_);
    }

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; auto to_string() const -&gt; std::string {
        // 优雅的多态调用
        return std::visit([](const auto&amp; addr) { return addr.to_string(); }, address_);
    }

    static auto from_string(std::string_view address) -&gt; Address {
        AddressV4 ipv4{};
        if (::inet_pton(AF_INET, address.data(), &amp;ipv4.address) == 1)
            return { ipv4 };

        AddressV6 ipv6{};
        if (::inet_pton(AF_INET6, address.data(), &amp;ipv6.address) == 1)
            return { ipv6 };

        throw_system_error("Invalid IP address format");
        std::unreachable();
    }

private:
    address_type address_;
};
</code></pre>
<p dir="auto">通过 <code>from_string</code>，我们实现了一个健壮的解析器：它会依次尝试按 IPv4 和 IPv6 解析字符串，并将成功的结果打包进安全的 <code>variant</code> 容器中。</p>
<p dir="auto"><a href="https://github.com/Doomjustin/blog/blob/main/src/ip/address.h" rel="nofollow ugc">Address完整代码</a></p>
<h3>3. Endpoint 封装：跨越 ABI 边界的内存博弈</h3>
<p dir="auto">现在，我们来到了最核心的部件 <code>BasicEndpoint</code>。它需要将前面实现的 <code>Protocol</code>、<code>Address</code> 与端口（Port）结合起来，并最终生成底层的 <code>sockaddr</code> 结构供内核使用。</p>
<h4>3.1 为什么引入 Protocol 模板参数？</h4>
<p dir="auto">你可能会疑惑：既然 IP 层的底层表示都是 <code>sockaddr</code>，为什么我们要设计成模板类 <code>BasicEndpoint&lt;Protocol&gt;</code>，而不是一个通用的 <code>Endpoint</code> 类？</p>
<p dir="auto">这是出于 <strong>强类型安全</strong> 的考量。<br />
TCP 的 <code>127.0.0.1:80</code> 和 UDP 的 <code>127.0.0.1:80</code> 在底层字节上完全一致，但在物理逻辑上是截然不同的通道。如果它们是同一个类型，开发者极易将 UDP 的端点传给 TCP 的 Socket 进行 <code>connect</code>，这种谬误只能在运行时由内核抛出异常。<br />
通过 <code>Protocol</code> 模板，<code>endpoint&lt;tcp&gt;</code> 和 <code>endpoint&lt;udp&gt;</code> 在 C++ 编译器眼中变成了绝对正交的两种类型，任何混用都会在编译期被拦截，这是零开销抽象的典范。</p>
<h4>3.2 致命陷阱：为何 Endpoint 内部必须摒弃 std::variant？</h4>
<p dir="auto">在封装 <code>Address</code> 时，我们使用了 <code>std::variant</code>。但在 <code>Endpoint</code> 内部存储底层结构时，却不能继续使用 <code>std::variant&lt;sockaddr_in, sockaddr_in6&gt;</code> 了，</p>
<p dir="auto"><code>Endpoint</code> 的内存不仅用于读取，更要以裸指针（<code>sockaddr*</code>）的形式交给内核 API（如 <code>accept</code> 或 <code>recvfrom</code>）。这些 API 具有 <strong>Overwrite</strong> 语义：内核会直接根据实际接收到的连接，向这块内存灌入 IPv4 或 IPv6 的字节流。</p>
<p dir="auto">如果底层是 <code>std::variant</code>：</p>
<ol>
<li><strong>内存布局破坏</strong>：<code>variant</code> 内部存在一个用于记录当前类型的 <code>index</code> 标记（以及可能的对齐 Padding）。内核如果从首地址开始写 <code>sa_family</code>，会直接破坏这个标记。</li>
<li><strong>状态脱节（UB）</strong>：假设 <code>variant</code> 当前为 IPv4（16字节），内核写入了 IPv6（28字节）的数据。内核无从知晓 C++ 的机制，绝不会去更新 <code>variant</code> 的 <code>index</code>。当 C++ 代码再次读取时，将发生严重的未定义行为（Undefined Behavior）。</li>
</ol>
<h4>3.3 解决方案：Union 与 sockaddr_storage</h4>
<p dir="auto">为了在确保 C 兼容性的同时提供 C++ 视图，最标准的解决方案是：<strong>使用 <code>union</code> 配合 <code>sockaddr_storage</code>。</strong> 也借此机会，强调一下C++的底层哲学：<strong>程序的世界没有银弹</strong>。对于不同场景选择适合的方式，所以C++提供了大量特性。</p>
<pre><code class="language-cpp">template&lt;typename Protocol&gt;
class BasicEndpoint {
public:
    using protocol_type = Protocol;
    using address_type = Address;

    // ... 构造函数见下文 ...

    // 提供给内核 API 的多态强转接口
    auto data() noexcept -&gt; sockaddr* 
    {
        return reinterpret_cast&lt;sockaddr*&gt;(&amp;data_.storage);
    }

private:
    // 经典的多态内存视图 (Aliased Views)
    union AddressType {
        sockaddr_storage storage; // 128字节，最严格对齐，提供绝对安全的物理容量兜底
        sockaddr_in v4;           // 提供给 C++ 侧的 IPv4 具象化读写视图
        sockaddr_in6 v6;          // 提供给 C++ 侧的 IPv6 具象化读写视图
    } data_;
};
</code></pre>
<p dir="auto">这里巧妙利用了 C/C++ 语言规范中的<strong>公共初始序列</strong>机制：所有 <code>sockaddr</code> 家族结构体的头两个字节都是 <code>sa_family_t</code>。</p>
<p dir="auto">因此，即便内核粗暴地覆盖了 <code>storage</code>，我们依然可以通过 <code>data_.storage.ss_family</code> 安全且合法地获知当前内存中实际装载的协议。</p>
<h3>4. 严守边界：Size 与 Capacity 的严格隔离</h3>
<p dir="auto">底层封装中最容易触发“缓冲区截断”Bug 的，是如何向内核报告这块 <code>union</code> 的长度。我们必须显式隔离 <code>size()</code> 和 <code>capacity()</code> 的语义：</p>
<pre><code class="language-cpp">    // 专供 输出型 API (如 accept, recvfrom) 使用
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; constexpr auto capacity() const noexcept -&gt; socklen_t {
        return sizeof(sockaddr_storage); // 永远返回最大容量 128 字节
    }

    // 专供 输入型 API (如 bind, connect) 使用
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; constexpr auto size() const noexcept -&gt; socklen_t {
        if (data_.storage.ss_family == AF_INET) return sizeof(sockaddr_in); // 16 字节
        return sizeof(sockaddr_in6); // 28 字节
    }
</code></pre>
<ul>
<li><strong>Input 操作（<code>bind</code> / <code>connect</code>）</strong>：内核要求<strong>精确匹配</strong>。如果你调用 <code>bind</code> 时传入了 128 字节（<code>capacity</code>），内核会因为长度不符合 IPv4（16）或 IPv6（28）的规约而直接返回 <code>-EINVAL</code>。必须严格使用动态计算的 <code>size()</code>。</li>
<li><strong>Output 操作（<code>accept</code>）</strong>：内核要求提供<strong>最大安全缓冲</strong>。在双栈监听模式下，随时可能接入 28 字节的 IPv6 客户端。如果此时你传入的是 <code>size()</code>（若端点默认初始化为 v4，则为 16），内核会直接截断写入，导致提取到的客户端地址完全损坏。必须严格使用 <code>capacity()</code>。</li>
</ul>
<h3>5. 拼图闭环：优雅的构造过程</h3>
<p dir="auto">有了上述坚实的底层基础，我们可以将 <code>Address</code> 对象转换为底层的 <code>union</code> 表示，最终兑现文章开头“一键构造”的承诺：</p>
<pre><code class="language-cpp">    BasicEndpoint(const address_type&amp; address, in_port_t port) {
        std::memset(&amp;data_, 0, sizeof(data_)); // 彻底清空，防止残留垃圾数据
        
        if (address.is_v4()) {
            data_.storage.ss_family = AF_INET;
            data_.v4.sin_family = AF_INET;
            data_.v4.sin_port = ::htons(port);
            data_.v4.sin_addr = address.to_v4().address;
        } else {
            data_.storage.ss_family = AF_INET6;
            data_.v6.sin6_family = AF_INET6;
            data_.v6.sin6_port = ::htons(port);
            data_.v6.sin6_addr = address.to_v6().address;
        }
    }

    static auto from_string(std::string_view address, in_port_t port) -&gt; BasicEndpoint {
        // 委托给 Address 的 variant 解析引擎，解析完成后交由本类的构造函数填充物理内存
        return BasicEndpoint{ address_type::from_string(address), port };
    }
</code></pre>
<p dir="auto">至此，我们的 <code>Endpoint</code> 彻底打通了从上层字符串抽象到底层 C 语言裸内存的通路。它对外提供了严格的类型契约，对内完美化解了 ABI 边界的内存博弈。</p>
<p dir="auto">对了，不要忘了，在tcp中，导出我们的endpoint</p>
<pre><code class="language-c++">class tcp {
public:
    using address = Address;

    using endpoint = BasicEndpoint&lt;tcp&gt;;
};
</code></pre>
<p dir="auto"><a href="https://github.com/Doomjustin/blog/blob/main/src/ip/endpoint.h" rel="nofollow ugc">Endpoint完整代码</a></p>
]]></description><link>http://forum.d2learn.org/topic/193/基于-io_uring-的-c-20-协程网络库-05-protocol与endpoint的封装</link><generator>RSS for Node</generator><lastBuildDate>Tue, 21 Apr 2026 13:40:57 GMT</lastBuildDate><atom:link href="http://forum.d2learn.org/topic/193.rss" rel="self" type="application/rss+xml"/><pubDate>Tue, 21 Apr 2026 06:12:58 GMT</pubDate><ttl>60</ttl></channel></rss>