跨平台网络抽象需隔离底层差异:WSA初始化、事件模型语义、广播配置、系统调用支持度、DNS解析行为及缓冲区管理均须封装,接口层禁用条件编译与平台类型。

为什么不能直接封装 socket API 做跨平台抽象
直接把 socket、bind、connect 等系统调用套一层 C++ 类,看似简单,但会掉进几个坑:
Windows 的 WSAStartup 初始化和错误码(WSAGetLastError)与 POSIX 完全不兼容;Linux 的 epoll 和 macOS/iOS 的 kqueue 事件模型语义不同,比如就绪通知方式、边缘/水平触发默认值;UDP 广播在 Windows 上需要显式 setsockopt(SO_BROADCAST),而某些嵌入式 POSIX 实现可能默认禁用。更麻烦的是,Android 的 Bionic libc 对 sendfile、splice 支持不全,iOS 又禁用 fork 相关能力——这些底层差异一旦暴露到上层接口,使用者就得写条件编译。
IOEventLoop 接口必须隔离事件驱动细节
核心不是“怎么调用 epoll_wait”,而是定义清楚“谁来通知我 fd 就绪”。推荐用纯虚函数声明:virtual void register_fd(int fd, EventMask mask) = 0;、virtual void run_once(int timeout_ms) = 0;。关键点在于:
• EventMask 必须是位掩码(如 READABLE | WRITABLE),不能用枚举类,否则 Windows 的 WSAEventSelect 模式无法对齐
• run_once 不应返回事件列表,而应要求子类调用回调(如 on_readable(int fd)),避免内存分配和跨平台容器差异
• Android NDK r21+ 开始支持 epoll,但需检查 __ANDROID_API__ >= 26,低于此版本得回退到 poll,这个判断必须封在实现里,不能让上层感知
地址解析必须绕过 getaddrinfo 的平台行为差异
getaddrinfo 在各平台返回结果顺序、AI_ADDRCONFIG 行为、IPv6-only 主机的处理逻辑都不一致。比如 macOS 默认返回 IPv6 地址优先,而某些 Linux 发行版会因 /etc/gai.conf 配置改变顺序。更糟的是,Windows 的 getaddrinfo 在无网络时可能阻塞数秒。解决方案:
• 抽象出 AddressResolver 接口,提供异步 resolve 方法(resolve_async(const char* host, const char* port, Callback cb))
• 底层实现:Linux/macOS 用线程池 + getaddrinfo,Windows 用 GetAddrInfoEx(支持取消)
• 强制统一解析策略:始终按 RFC 6724 规则排序,且默认启用 AI_ADDRCONFIG,但允许用户通过配置项关闭(比如测试纯 IPv6 环境时)
• DNS 超时必须独立控制(如 set_dns_timeout(5000)),不能依赖系统默认值
TcpStream 和 UdpSocket 的生命周期必须解耦缓冲区管理
很多跨平台库把读写缓冲区(std::vector<uint8_t></uint8_t>)直接绑在连接对象里,导致大文件传输时反复 realloc,或在异步场景下缓冲区被意外覆盖。正确做法:
• 所有 I/O 方法接受用户提供的 std::span<const uint8_t></const> 或 std::span<uint8_t></uint8_t>,不持有缓冲区所有权
• 写操作返回实际发送字节数,而非布尔值(POSIX 的 send 可能只发部分,Windows 的 WSASend 也类似)
• 关闭连接必须区分 shutdown_write() 和 close():前者发 FIN,后者释放 fd,两者语义在各平台虽有差异,但抽象层要保证行为可预测
• UDP 的 send_to 必须支持 std::optional<address></address> 参数,以支持连接态 socket 复用(connect() 后可直接 send),避免 iOS 上 sendto 对已 connect 的 socket 返回 EISCONN
立即学习“C++免费学习笔记(深入)”;
跨平台网络抽象最难的不是功能覆盖,而是让每个平台的“例外行为”不出现在头文件里——所有 #ifdef 必须严格限制在 .cpp 实现中,接口层连 SOCKET 这种类型都不能出现。


















