跳转至内容
  • A place to talk about whatever you want

    24 主题
    165 帖子
    semmyenatorS

    AREEM,一種無需黑箱的高精度π計算方法
    https://deepwiki.com/semmyenator/AREEM
    這個項目只是一個人完成的小型數學項目,並非一項發明或特殊功能設計。
    希望它對需要旋轉控制的工程師有所幫助。

  • 7 主题
    20 帖子
    SPeakS

    @dustchens 链表结构损坏, 不闭环了 (如果问题解决可以把帖子状态设置为已解决

  • 开源软件 | 开源社区 | 开源理念 | 开源与商业 | 开源可持续发展 等相关话的交流讨论
    注: 这里的"开源"是泛化的共建共享概念, 范围包含 OSI的范围、自由软件、CC等相关内容

    57 主题
    246 帖子
    MoYingJiM

    补一个 0BSD,这个许可证很多时候也被放在与 Unlicense 和 WTFPL 相提并论的(都是公共领域)

  • 56 主题
    250 帖子
    M

    lijin4884@gmail.com

  • 30 主题
    66 帖子
    DoomjustinD

    在构建了底层的异步轮询引擎(IOContext)、协程机制以及端点(Endpoint)的内存布局后,我们进入网络编程的核心实体:套接字(Socket)。

    在早期的概念验证阶段(第四篇博客中),为了快速验证系统可行性,我们曾实现过一个简陋的 Socket 类,将描述符的创建、bind、listen、accept、read 和 write 糅合在一个结构中。

    作为原型,它是合格的。但作为工业级的基础设施,这种设计存在致命的类型安全隐患:如果一个用于 UDP 的数据报套接字暴露了 listen() 和 accept() 接口,编译器并不会报错,逻辑谬误只会在运行时以 -EOPNOTSUPP(Operation not supported)的形式暴露。

    本篇的目标是从宏观架构出发,构建一个职责单一、零运行时开销,且受 C++20 强类型系统严格保护的套接字层级体系。

    1. 宏观架构

    POSIX 系统为网络通信提供了极度灵活但也极其松散的 C API。在现代 C++ 中,核心接口设计原则是:让接口易于正确使用,难以被误用。为此,我们必须根据网络协议的物理行为特征,将 Socket 拆解为层次分明的类簇:

    BaseSocket
    所有套接字的基类。其唯一职责是:基于 RAII 原则管理文件描述符(fd)的生命周期,并提供底层统一的套接字选项(Socket Options)配置接口。

    Acceptor(被动接收器)
    继承自 BaseSocket。专门用于服务端监听。仅开放 bind、listen 和 accept 接口。它剥离了数据读写能力,因为监听套接字本身不应参与数据载荷的收发。

    StreamSocket(流式套接字)
    继承自 BaseSocket。代表面向连接、可靠的字节流通信(如 ip::tcp 或本机流式 IPC local::stream_protocol)。开放 connect、read_some 和 write_some 接口。

    DatagramSocket(数据报套接字)
    继承自 BaseSocket。代表无连接、不可靠的数据报通信(如 ip::udp)。无 connect 语义,仅开放 send_to 和 receive_from 接口。

    通过这一分层,若业务代码试图在 UDP Socket 上调用 accept,编译器将在编译阶段直接抛出“找不到该成员函数”的错误。我们将潜在的运行时崩溃彻底转化为编译期约束。同时,该架构也为未来扩充不同的协议栈保留了正交性。

    2. 契约先行:Protocol 的 Concept 约束

    在泛型编程中,必须确保传入的模板参数是合法的协议类型。根据上文对 TCP 的设计,协议需要提供调用 ::socket() 系统调用所需的三要素:domain、type 和 protocol。

    我们使用 C++20 的 Concept 来定义这一显式契约:

    #include <concepts> // src/socket.h template <typename T> concept has_domain = requires (const T& t) { { t.domain() } -> std::convertible_to<int>; }; template <typename T> concept has_type = requires (const T& t) { { t.type() } -> std::convertible_to<int>; }; template <typename T> concept has_protocol = requires (const T& t) { { t.protocol() } -> std::convertible_to<int>; }; template <typename T> concept socket_protocol = has_domain<T> && has_type<T> && has_protocol<T>;

    引入 socket_protocol 约束后,任何不满足规范的自定义协议类型在实例化 Socket 时,编译器都会提供精确的诊断信息。

    3. 第一步:RAII 资源基类 BaseSocket

    接下来构建套接字的基类 BaseSocket。

    其核心是实现严格的所有权(Ownership)和移动语义。在系统级编程中,文件描述符是独占资源。尽管可以通过 dup 复制文件描述符,但在基础套接字封装中,拷贝往往意味着所有权语义的混乱。因此,我们明确拒绝拷贝语义。

    #include <system_error> #include <utility> #include <unistd.h> #include "exceptions.h" template<socket_protocol Protocol, typename Context> class BaseSocket { public: using context_type = Context; using protocol_type = Protocol; // 1. 基于协议类型创建底层 fd BaseSocket(Context& context, const Protocol& protocol) : context_{ &context }, fd_{ create(protocol) } {} // 2. 彻底禁用拷贝语义 BaseSocket(const BaseSocket&) = delete; auto operator=(const BaseSocket&) -> BaseSocket& = delete; // 3. 完美的移动语义:交接控制权,并将源对象的 fd 置为无效 BaseSocket(BaseSocket&& other) noexcept : context_{ std::exchange(other.context_, nullptr) }, fd_{ std::exchange(other.fd_, INVALID_SOCKET) } {} auto operator=(BaseSocket&& other) noexcept -> BaseSocket& { if (this == &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(); } [[nodiscard]] constexpr auto is_valid() const noexcept -> bool { return fd_ != INVALID_SOCKET; } auto close() noexcept -> std::expected<void, std::error_code> { if (is_valid()) { auto res = ::close(fd_); fd_ = INVALID_SOCKET; if (res == -1) return unexpected_system_error(); } return {}; } [[nodiscard]] constexpr auto native_handle() const noexcept -> int { return fd_; } [[nodiscard]] auto context() noexcept -> Context& { return *context_; } protected: // 供 Acceptor 接收新连接后直接接管已存在的 fd BaseSocket(Context& context, int fd) : context_{ &context }, fd_{ fd } {} private: static constexpr int INVALID_SOCKET = -1; Context* context_; int fd_ = INVALID_SOCKET; static auto create(const Protocol& protocol) -> int { auto res = ::socket(protocol.domain(), protocol.type(), protocol.protocol()); if (res == -1) throw_system_error("Failed to create socket"); return res; } };

    这里模板参数的声明顺序(Protocol, Context)具有其实际意义。通过让 Context 作为第二个模板参数,我们可以在提供 Protocol 的前提下,利用 C++17 的类模板参数推导(CTAD)简化客户端代码:

    // 固定协议别名 template<typename Context> using socket = stream_socket<tcp, Context>; IOContext context{}; // 编译器自动推导 Context 为 IOContext,无需显式指定模板参数 auto s = socket{ context }; 4. 第二步:构建面向连接的 StreamSocket

    确立了资源管理基线后,我们可以派生出 StreamSocket。

    流式套接字的核心特征在于点对点连接(提供 connect)及无边界的字节流传输(提供 read_some 和 write_some)。这里我们摒弃了传统的 void* buffer 配合 size_t length,全面使用 C++20 的 std::span<std::byte>。它携带连续内存的边界信息,能在编译期和运行期最大限度地避免内存越界。

    #ifndef BLOG_IP_STREAM_SOCKET_H #define BLOG_IP_STREAM_SOCKET_H #include <span> #include <expected> #include "socket.h" #include "operations.h" #include "readsome_awaiter.h" // 协程 Awaiter #include "writesome_awaiter.h" namespace ip { template<typename Protocol, typename Context> class StreamSocket: public BaseSocket<Protocol, Context> { public: using base_socket_type = BaseSocket<Protocol, Context>; using endpoint_type = typename Protocol::endpoint; explicit StreamSocket(Context& context) : base_socket_type{ context, Protocol{} } {} // 接收内核已完成的连接 (用于 Acceptor 生成) StreamSocket(Context& context, int fd) : base_socket_type{ context, fd } {} StreamSocket(StreamSocket&&) = default; StreamSocket& operator=(StreamSocket&&) = default; ~StreamSocket() = default; // --- 同步阻塞接口 --- void connect(const endpoint_type& peer) { // 配合 Endpoint 的 data() 和 size() 语义 operations::connect(this->native_handle(), peer.data(), peer.size()); } auto read_some(std::span<std::byte> buffer) noexcept -> std::expected<std::size_t, std::error_code> { return operations::read_some(this->native_handle(), buffer); } auto write_some(std::span<const std::byte> buffer) noexcept -> std::expected<std::size_t, std::error_code> { return operations::write_some(this->native_handle(), buffer); } // --- 异步协程接口 (对接 io_uring) --- auto async_read_some(std::span<std::byte> buffer) noexcept -> ReadSomeAwaiter<Context> { return ReadSomeAwaiter<Context>{ this->context(), this->native_handle(), buffer }; } auto async_write_some(std::span<const std::byte> buffer) noexcept -> WriteSomeAwaiter<Context> { return WriteSomeAwaiter<Context>{ this->context(), this->native_handle(), buffer }; } }; } // namespace ip #endif // BLOG_IP_STREAM_SOCKET_H

    通过继承,StreamSocket 天然获取了生命周期管理能力,仅需专注具体的 I/O 逻辑。

    值得探讨的是,为何 StreamSocket 位于 namespace ip 中?

    在纯抽象层面,流式套接字似乎应是一个全局泛型概念:只要能读写字节流,即为 Stream Socket。然而,底层协议之间天然存在物理特征的不兼容。例如,IP 协议栈的流式套接字拥有专属于 IP 层的控制选项(如控制 Nagle 算法的 TCP_NODELAY)。

    如果将其强制抽象为全局通用的 StreamSocket,会导致这些特有配置选项失去编译期类型保护,进而增加运行时决议的复杂度和错误风险。因此,利用 namespace ip 进行物理与语义双重隔离,是维持零开销抽象的必要设计:

    namespace ip { template<typename Protocol, typename Context> class StreamSocket: public BaseSocket<Protocol, Context> { public: // ... // IP 协议簇专属的类型安全选项 using keep_alive_idle = ValueOption<IPPROTO_TCP, TCP_KEEPIDLE>; using keep_alive_interval = ValueOption<IPPROTO_TCP, TCP_KEEPINTVL>; using keep_alive_count = ValueOption<IPPROTO_TCP, TCP_KEEPCNT>; using no_delay = BooleanOption<IPPROTO_TCP, TCP_NODELAY>; // ... }; } 缓冲区适配:优雅的调用体验 (Ergonomics)

    与此同时,我们必须正视一个工程体验问题:虽然将底层的 I/O 接口固化为 std::span<std::byte> 确保了绝对的内存边界安全,但这会给上层业务代码带来不适。我们显然不能强制用户在每次读写时,都笨拙地手动强转指针,或者仅仅为了网络传输而将所有业务层容器都声明为 std::array<std::byte, N>。

    为了优化调用侧的体验,同时不向底层 I/O 方法中引入任何多余的模板复杂度,我们利用 C++20 的 Concept,提供一个轻量级的视图转换工厂函数 buffer()。

    对于只读的连续内存容器,我们将其零开销转换为 std::span<const std::byte>,用于 write_some:

    template<std::ranges::contiguous_range T> auto buffer(const T& range) noexcept -> std::span<const std::byte> { return std::as_bytes(std::span{ range }); }

    这样一来,用户可以顺畅地写出如下代码,而不必亲自干预指针转换:

    std::string msg = "Hello, io_uring!"; // 自动推导并转换,安全且零拷贝 client_sock.write_some(buffer(msg));

    对于可写的连续内存容器,我们将其转换为 std::span<std::byte>,用于 read_some 这样的输出调用:

    template<std::ranges::contiguous_range T> requires (!std::is_const_v<std::remove_reference_t<std::ranges::range_reference_t<T>>>) auto buffer(T& range) noexcept -> std::span<std::byte> { return std::as_writable_bytes(std::span{ range }); }

    同样地,读取操作也变得极其自然:

    std::vector<char> recv_buf(1024); // 安全地提取底层连续内存块供内核填充 client_sock.read_some(buffer(recv_buf));

    这一层薄薄的抽象,大大优化了调用体验,且没有在核心的 I/O 方法中引入多余的模板膨胀。

    5. 协议的装配:Type Traits 工厂

    如何将泛型的 StreamSocket 与具体的协议(如 TCP)优雅绑定?

    在我们的设计中,Protocol 类(例如 ip::tcp)不仅提供静态常量,还充当类型特征(Type Traits)的装配枢纽。

    将上述组件装配进去:

    namespace ip { class tcp { public: // 强制约束 tcp 的 socket 实现类型 template<typename Context> using socket = StreamSocket<tcp, Context>; // ... 其他静态特征 ... }; } // namespace ip 结语:零开销与强类型的交响曲

    至此,让我们审视业务代码的最终形态:

    ip::tcp::socket<IOContext> client{ context }; ip::tcp::endpoint target = ip::tcp::endpoint::from_string("127.0.0.1", 8080); client.connect(target); std::vector<char> recv_buf(1024); client.read_some(buffer(recv_buf))

    在这寥寥数行代码中,类型系统在幕后默默完成了以下推导与约束:

    ip::tcp::socket 自动推导为 StreamSocket<ip::tcp, IOContext>。 编译器确保 connect 仅接受 ip::tcp::endpoint,如果误传 udp::endpoint 会导致编译立刻失败。 StreamSocket 的基类构造函数静态提取 ip::tcp 的 SOCK_STREAM 和 IPPROTO_TCP 以发起安全的系统调用。 对象离开作用域时,BaseSocket 的 RAII 机制确保文件描述符被安全释放。

    我们彻底隐藏了底层的状态机转移与裸露的 C API,以层次分明、类型严格的现代 C++ 接口取而代之。由于高度模板化和内联优化,这一系列抽象的运行期开销,严格等价于手写裸 C 语言代码。

    在下一篇文章中,我们将补齐拼图的最后一块:Acceptor(被动接收器)的封装。届时,便可利用这套基础设施,跑通完整的基于 io_uring 的全异步协程服务器。

    完整代码

  • 一个技术知识分享、学习、交流的社区

    15 主题
    50 帖子
    sunrisepeakS

    @Doomjustin 版块已创建, 可以检查确认一下是否有话题贴/Topic工具的权限

    https://forum.d2learn.org/category/26/xin
  • Got a question? Ask away!

    4 主题
    14 帖子
    SPeakS

    备注一下使数学公式的使用语法

    单行公式语法 - $ 你的公式 $

    $ log_2^n $

    $ log_2^n $

    多行公式语法 - $$ 你的公式 $$

    $$ log_2^n => log_2^9 = 3 , n = 9 $$

    $$
    log_2^n =>
    log_2^9 = 3, n = 9
    $$

公告栏 | Bulletin Board

欢迎加入d2learn社区 - 社区指南
Welcome to the d2learn Community - Community Guide

一个以 [知识、技术、代码、项目、想法、开源] 相关话题为主导的社区
A community focused on topics related to [knowledge, technology, code, projects, ideas, and open source].


在线用户