<?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 协程网络库】06 socket层次化封装]]></title><description><![CDATA[<p dir="auto">在构建了底层的异步轮询引擎（<code>IOContext</code>）、协程机制以及端点（<code>Endpoint</code>）的内存布局后，我们进入网络编程的核心实体：套接字（Socket）。</p>
<p dir="auto">在早期的概念验证阶段（第四篇博客中），为了快速验证系统可行性，我们曾实现过一个简陋的 <code>Socket</code> 类，将描述符的创建、<code>bind</code>、<code>listen</code>、<code>accept</code>、<code>read</code> 和 <code>write</code> 糅合在一个结构中。</p>
<p dir="auto">作为原型，它是合格的。但作为工业级的基础设施，这种设计存在致命的类型安全隐患：如果一个用于 UDP 的数据报套接字暴露了 <code>listen()</code> 和 <code>accept()</code> 接口，编译器并不会报错，逻辑谬误只会在运行时以 <code>-EOPNOTSUPP</code>（Operation not supported）的形式暴露。</p>
<p dir="auto">本篇的目标是从宏观架构出发，构建一个职责单一、零运行时开销，且受 C++20 强类型系统严格保护的套接字层级体系。</p>
<hr />
<h3>1. 宏观架构</h3>
<p dir="auto">POSIX 系统为网络通信提供了极度灵活但也极其松散的 C API。在现代 C++ 中，核心接口设计原则是：<strong>让接口易于正确使用，难以被误用</strong>。为此，我们必须根据网络协议的物理行为特征，将 Socket 拆解为层次分明的类簇：</p>
<ol>
<li>
<p dir="auto"><strong><code>BaseSocket</code></strong>：<br />
所有套接字的基类。其唯一职责是：<strong>基于 RAII 原则管理文件描述符（fd）的生命周期</strong>，并提供底层统一的套接字选项（Socket Options）配置接口。</p>
</li>
<li>
<p dir="auto"><strong><code>Acceptor</code>（被动接收器）</strong>：<br />
继承自 <code>BaseSocket</code>。专门用于服务端监听。仅开放 <code>bind</code>、<code>listen</code> 和 <code>accept</code> 接口。它剥离了数据读写能力，因为监听套接字本身不应参与数据载荷的收发。</p>
</li>
<li>
<p dir="auto"><strong><code>StreamSocket</code>（流式套接字）</strong>：<br />
继承自 <code>BaseSocket</code>。代表<strong>面向连接、可靠的字节流通信</strong>（如 <code>ip::tcp</code> 或本机流式 IPC <code>local::stream_protocol</code>）。开放 <code>connect</code>、<code>read_some</code> 和 <code>write_some</code> 接口。</p>
</li>
<li>
<p dir="auto"><strong><code>DatagramSocket</code>（数据报套接字）</strong>：<br />
继承自 <code>BaseSocket</code>。代表<strong>无连接、不可靠的数据报通信</strong>（如 <code>ip::udp</code>）。无 <code>connect</code> 语义，仅开放 <code>send_to</code> 和 <code>receive_from</code> 接口。</p>
</li>
</ol>
<p dir="auto">通过这一分层，若业务代码试图在 UDP Socket 上调用 <code>accept</code>，编译器将在编译阶段直接抛出“找不到该成员函数”的错误。我们将潜在的运行时崩溃彻底转化为编译期约束。同时，该架构也为未来扩充不同的协议栈保留了正交性。</p>
<h3>2. 契约先行：Protocol 的 Concept 约束</h3>
<p dir="auto">在泛型编程中，必须确保传入的模板参数是合法的协议类型。根据上文对 TCP 的设计，协议需要提供调用 <code>::socket()</code> 系统调用所需的三要素：<code>domain</code>、<code>type</code> 和 <code>protocol</code>。</p>
<p dir="auto">我们使用 C++20 的 Concept 来定义这一显式契约：</p>
<pre><code class="language-cpp">#include &lt;concepts&gt;

// src/socket.h
template &lt;typename T&gt;
concept has_domain = requires (const T&amp; t) { 
    { t.domain() } -&gt; std::convertible_to&lt;int&gt;;
};

template &lt;typename T&gt;
concept has_type = requires (const T&amp; t) { 
    { t.type() } -&gt; std::convertible_to&lt;int&gt;;
};

template &lt;typename T&gt;
concept has_protocol = requires (const T&amp; t) {
    { t.protocol() } -&gt; std::convertible_to&lt;int&gt;;
};

template &lt;typename T&gt;
concept socket_protocol = has_domain&lt;T&gt; &amp;&amp; has_type&lt;T&gt; &amp;&amp; has_protocol&lt;T&gt;;
</code></pre>
<p dir="auto">引入 <code>socket_protocol</code> 约束后，任何不满足规范的自定义协议类型在实例化 Socket 时，编译器都会提供精确的诊断信息。</p>
<h3>3. 第一步：RAII 资源基类 BaseSocket</h3>
<p dir="auto">接下来构建套接字的基类 <code>BaseSocket</code>。</p>
<p dir="auto">其核心是实现严格的所有权（Ownership）和移动语义。在系统级编程中，文件描述符是独占资源。尽管可以通过 <code>dup</code> 复制文件描述符，但在基础套接字封装中，拷贝往往意味着所有权语义的混乱。因此，我们明确拒绝拷贝语义。</p>
<pre><code class="language-cpp">#include &lt;system_error&gt;
#include &lt;utility&gt;
#include &lt;unistd.h&gt;
#include "exceptions.h"

template&lt;socket_protocol Protocol, typename Context&gt;
class BaseSocket {
public:
    using context_type = Context;
    using protocol_type = Protocol;

    // 1. 基于协议类型创建底层 fd
    BaseSocket(Context&amp; context, const Protocol&amp; protocol)
      : context_{ &amp;context }, 
        fd_{ create(protocol) }
    {}

    // 2. 彻底禁用拷贝语义
    BaseSocket(const BaseSocket&amp;) = delete;
    auto operator=(const BaseSocket&amp;) -&gt; BaseSocket&amp; = delete;

    // 3. 完美的移动语义：交接控制权，并将源对象的 fd 置为无效
    BaseSocket(BaseSocket&amp;&amp; other) noexcept
      : context_{ std::exchange(other.context_, nullptr) }, 
        fd_{ std::exchange(other.fd_, INVALID_SOCKET) }
    {}

    auto operator=(BaseSocket&amp;&amp; other) noexcept -&gt; BaseSocket&amp;
    {
        if (this == &amp;other) return *this;
        close(); // 覆盖前必须先关闭自己现有的 fd
        context_ = std::exchange(other.context_, nullptr);
        fd_ = std::exchange(other.fd_, INVALID_SOCKET);
        return *this;
    }

    // 4. RAII 析构
    virtual ~BaseSocket() 
    {
        close();
    }

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; constexpr auto is_valid() const noexcept -&gt; bool 
    {
        return fd_ != INVALID_SOCKET;
    }

    auto close() noexcept -&gt; std::expected&lt;void, std::error_code&gt; 
    {
        if (is_valid()) {
            auto res = ::close(fd_);
            fd_ = INVALID_SOCKET;
            if (res == -1) return unexpected_system_error();
        }
        return {};
    }

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; constexpr auto native_handle() const noexcept -&gt; int { return fd_; }
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; auto context() noexcept -&gt; Context&amp; { return *context_; }

protected:
    // 供 Acceptor 接收新连接后直接接管已存在的 fd
    BaseSocket(Context&amp; context, int fd)
      : context_{ &amp;context }, fd_{ fd }
    {}

private:
    static constexpr int INVALID_SOCKET = -1;
    Context* context_;
    int fd_ = INVALID_SOCKET;

    static auto create(const Protocol&amp; protocol) -&gt; int 
    {
        auto res = ::socket(protocol.domain(), protocol.type(), protocol.protocol());
        if (res == -1) throw_system_error("Failed to create socket");
        return res;
    }
};
</code></pre>
<p dir="auto">这里模板参数的声明顺序（<code>Protocol</code>, <code>Context</code>）具有其实际意义。通过让 <code>Context</code> 作为第二个模板参数，我们可以在提供 <code>Protocol</code> 的前提下，利用 C++17 的类模板参数推导（CTAD）简化客户端代码：</p>
<pre><code class="language-cpp">// 固定协议别名
template&lt;typename Context&gt;
using socket = stream_socket&lt;tcp, Context&gt;;

IOContext context{};

// 编译器自动推导 Context 为 IOContext，无需显式指定模板参数
auto s = socket{ context };
</code></pre>
<h3>4. 第二步：构建面向连接的 StreamSocket</h3>
<p dir="auto">确立了资源管理基线后，我们可以派生出 <code>StreamSocket</code>。</p>
<p dir="auto">流式套接字的核心特征在于点对点连接（提供 <code>connect</code>）及无边界的字节流传输（提供 <code>read_some</code> 和 <code>write_some</code>）。这里我们摒弃了传统的 <code>void* buffer</code> 配合 <code>size_t length</code>，全面使用 C++20 的 <code>std::span&lt;std::byte&gt;</code>。它携带连续内存的边界信息，能在编译期和运行期最大限度地避免内存越界。</p>
<pre><code class="language-cpp">#ifndef BLOG_IP_STREAM_SOCKET_H
#define BLOG_IP_STREAM_SOCKET_H

#include &lt;span&gt;
#include &lt;expected&gt;
#include "socket.h"
#include "operations.h"
#include "readsome_awaiter.h"   // 协程 Awaiter
#include "writesome_awaiter.h"

namespace ip {

template&lt;typename Protocol, typename Context&gt;
class StreamSocket: public BaseSocket&lt;Protocol, Context&gt; {
public:
    using base_socket_type = BaseSocket&lt;Protocol, Context&gt;;
    using endpoint_type = typename Protocol::endpoint;

    explicit StreamSocket(Context&amp; context)
      : base_socket_type{ context, Protocol{} }
    {}

    // 接收内核已完成的连接 (用于 Acceptor 生成)
    StreamSocket(Context&amp; context, int fd)
      : base_socket_type{ context, fd }
    {}

    StreamSocket(StreamSocket&amp;&amp;) = default;
    StreamSocket&amp; operator=(StreamSocket&amp;&amp;) = default;

    ~StreamSocket() = default;

    // --- 同步阻塞接口 ---
    void connect(const endpoint_type&amp; peer) 
    {
        // 配合 Endpoint 的 data() 和 size() 语义
        operations::connect(this-&gt;native_handle(), peer.data(), peer.size());
    }

    auto read_some(std::span&lt;std::byte&gt; buffer) noexcept 
        -&gt; std::expected&lt;std::size_t, std::error_code&gt; 
    {
        return operations::read_some(this-&gt;native_handle(), buffer);
    }

    auto write_some(std::span&lt;const std::byte&gt; buffer) noexcept 
        -&gt; std::expected&lt;std::size_t, std::error_code&gt; 
    {
        return operations::write_some(this-&gt;native_handle(), buffer);
    }

    // --- 异步协程接口 (对接 io_uring) ---
    auto async_read_some(std::span&lt;std::byte&gt; buffer) noexcept 
        -&gt; ReadSomeAwaiter&lt;Context&gt; 
    {
        return ReadSomeAwaiter&lt;Context&gt;{ this-&gt;context(), this-&gt;native_handle(), buffer };
    }

    auto async_write_some(std::span&lt;const std::byte&gt; buffer) noexcept 
        -&gt; WriteSomeAwaiter&lt;Context&gt; 
    {
        return WriteSomeAwaiter&lt;Context&gt;{ this-&gt;context(), this-&gt;native_handle(), buffer };
    }
};

} // namespace ip
#endif // BLOG_IP_STREAM_SOCKET_H
</code></pre>
<p dir="auto">通过继承，<code>StreamSocket</code> 天然获取了生命周期管理能力，仅需专注具体的 I/O 逻辑。</p>
<p dir="auto">值得探讨的是，<strong>为何 <code>StreamSocket</code> 位于 <code>namespace ip</code> 中？</strong></p>
<p dir="auto">在纯抽象层面，流式套接字似乎应是一个全局泛型概念：只要能读写字节流，即为 Stream Socket。然而，底层协议之间天然存在物理特征的不兼容。例如，IP 协议栈的流式套接字拥有专属于 IP 层的控制选项（如控制 Nagle 算法的 <code>TCP_NODELAY</code>）。</p>
<p dir="auto">如果将其强制抽象为全局通用的 <code>StreamSocket</code>，会导致这些特有配置选项失去编译期类型保护，进而增加运行时决议的复杂度和错误风险。因此，利用 <code>namespace ip</code> 进行物理与语义双重隔离，是维持零开销抽象的必要设计：</p>
<pre><code class="language-cpp">namespace ip {
template&lt;typename Protocol, typename Context&gt;
class StreamSocket: public BaseSocket&lt;Protocol, Context&gt; {
public:
    // ...
    // IP 协议簇专属的类型安全选项
    using keep_alive_idle = ValueOption&lt;IPPROTO_TCP, TCP_KEEPIDLE&gt;;
    using keep_alive_interval = ValueOption&lt;IPPROTO_TCP, TCP_KEEPINTVL&gt;;
    using keep_alive_count = ValueOption&lt;IPPROTO_TCP, TCP_KEEPCNT&gt;;
    using no_delay = BooleanOption&lt;IPPROTO_TCP, TCP_NODELAY&gt;;
    // ...
};
}
</code></pre>
<h4>缓冲区适配：优雅的调用体验 (Ergonomics)</h4>
<p dir="auto">与此同时，我们必须正视一个工程体验问题：虽然将底层的 I/O 接口固化为 <code>std::span&lt;std::byte&gt;</code> 确保了绝对的内存边界安全，但这会给上层业务代码带来不适。我们显然不能强制用户在每次读写时，都笨拙地手动强转指针，或者仅仅为了网络传输而将所有业务层容器都声明为 <code>std::array&lt;std::byte, N&gt;</code>。</p>
<p dir="auto">为了优化调用侧的体验，同时不向底层 I/O 方法中引入任何多余的模板复杂度，我们利用 C++20 的 Concept，提供一个轻量级的视图转换工厂函数 <code>buffer()</code>。</p>
<p dir="auto">对于<strong>只读</strong>的连续内存容器，我们将其零开销转换为 <code>std::span&lt;const std::byte&gt;</code>，用于 <code>write_some</code>：</p>
<pre><code class="language-cpp">template&lt;std::ranges::contiguous_range T&gt;
auto buffer(const T&amp; range) noexcept -&gt; std::span&lt;const std::byte&gt;
{
    return std::as_bytes(std::span{ range });
}
</code></pre>
<p dir="auto">这样一来，用户可以顺畅地写出如下代码，而不必亲自干预指针转换：</p>
<pre><code class="language-cpp">std::string msg = "Hello, io_uring!";
// 自动推导并转换，安全且零拷贝
client_sock.write_some(buffer(msg));
</code></pre>
<p dir="auto">对于<strong>可写</strong>的连续内存容器，我们将其转换为 <code>std::span&lt;std::byte&gt;</code>，用于 <code>read_some</code> 这样的输出调用：</p>
<pre><code class="language-cpp">template&lt;std::ranges::contiguous_range T&gt;
    requires (!std::is_const_v&lt;std::remove_reference_t&lt;std::ranges::range_reference_t&lt;T&gt;&gt;&gt;)
auto buffer(T&amp; range) noexcept -&gt; std::span&lt;std::byte&gt;
{
    return std::as_writable_bytes(std::span{ range });
}
</code></pre>
<p dir="auto">同样地，读取操作也变得极其自然：</p>
<pre><code class="language-cpp">std::vector&lt;char&gt; recv_buf(1024);
// 安全地提取底层连续内存块供内核填充
client_sock.read_some(buffer(recv_buf));
</code></pre>
<p dir="auto">这一层薄薄的抽象，大大优化了调用体验，且没有在核心的 I/O 方法中引入多余的模板膨胀。</p>
<h3>5. 协议的装配：Type Traits 工厂</h3>
<p dir="auto">如何将泛型的 <code>StreamSocket</code> 与具体的协议（如 TCP）优雅绑定？</p>
<p dir="auto">在我们的设计中，<code>Protocol</code> 类（例如 <code>ip::tcp</code>）不仅提供静态常量，还充当类型特征（Type Traits）的装配枢纽。</p>
<p dir="auto">将上述组件装配进去：</p>
<pre><code class="language-cpp">namespace ip {

class tcp {
public:
    // 强制约束 tcp 的 socket 实现类型
    template&lt;typename Context&gt;
    using socket = StreamSocket&lt;tcp, Context&gt;;
    
    // ... 其他静态特征 ...
};

} // namespace ip
</code></pre>
<h3>结语：零开销与强类型的交响曲</h3>
<p dir="auto">至此，让我们审视业务代码的最终形态：</p>
<pre><code class="language-cpp">ip::tcp::socket&lt;IOContext&gt; client{ context };
ip::tcp::endpoint target = ip::tcp::endpoint::from_string("127.0.0.1", 8080);

client.connect(target);

std::vector&lt;char&gt; recv_buf(1024);
client.read_some(buffer(recv_buf))
</code></pre>
<p dir="auto">在这寥寥数行代码中，类型系统在幕后默默完成了以下推导与约束：</p>
<ol>
<li><code>ip::tcp::socket</code> 自动推导为 <code>StreamSocket&lt;ip::tcp, IOContext&gt;</code>。</li>
<li>编译器确保 <code>connect</code> 仅接受 <code>ip::tcp::endpoint</code>，如果误传 <code>udp::endpoint</code> 会导致编译立刻失败。</li>
<li><code>StreamSocket</code> 的基类构造函数静态提取 <code>ip::tcp</code> 的 <code>SOCK_STREAM</code> 和 <code>IPPROTO_TCP</code> 以发起安全的系统调用。</li>
<li>对象离开作用域时，<code>BaseSocket</code> 的 RAII 机制确保文件描述符被安全释放。</li>
</ol>
<p dir="auto">我们彻底隐藏了底层的状态机转移与裸露的 C API，以层次分明、类型严格的现代 C++ 接口取而代之。由于高度模板化和内联优化，这一系列抽象的运行期开销，严格等价于手写裸 C 语言代码。</p>
<p dir="auto">在下一篇文章中，我们将补齐拼图的最后一块：<code>Acceptor</code>（被动接收器）的封装。届时，便可利用这套基础设施，跑通完整的基于 <code>io_uring</code> 的全异步协程服务器。</p>
<p dir="auto"><a href="https://github.com/Doomjustin/blog/blob/main/src/ip/stream_socket.h" rel="nofollow ugc">完整代码</a></p>
]]></description><link>http://forum.d2learn.org/topic/194/基于-io_uring-的-c-20-协程网络库-06-socket层次化封装</link><generator>RSS for Node</generator><lastBuildDate>Wed, 22 Apr 2026 13:57:52 GMT</lastBuildDate><atom:link href="http://forum.d2learn.org/topic/194.rss" rel="self" type="application/rss+xml"/><pubDate>Tue, 21 Apr 2026 14:22:11 GMT</pubDate><ttl>60</ttl></channel></rss>