From 20956504b70ea219d4e87b3457628e765ab63b05 Mon Sep 17 00:00:00 2001 From: FarnaHerry Date: Thu, 6 Aug 2026 01:40:30 +0800 Subject: [PATCH] feat: compat.websocket server + zlib features (gated; server implies zlib) --- .agents/docs/2026-08-06-add-websocket-plan.md | 38 ++- README.md | 2 +- README.zh-CN.md | 2 +- mcpp.toml | 1 + pkgs/c/compat.websocket.lua | 46 ++++ tests/examples/websocket-features/mcpp.toml | 17 ++ .../tests/ws_features_test.cpp | 257 ++++++++++++++++++ 7 files changed, 358 insertions(+), 5 deletions(-) create mode 100644 tests/examples/websocket-features/mcpp.toml create mode 100644 tests/examples/websocket-features/tests/ws_features_test.cpp diff --git a/.agents/docs/2026-08-06-add-websocket-plan.md b/.agents/docs/2026-08-06-add-websocket-plan.md index 73c858d..0f3d3da 100644 --- a/.agents/docs/2026-08-06-add-websocket-plan.md +++ b/.agents/docs/2026-08-06-add-websocket-plan.md @@ -96,10 +96,42 @@ mcpp test -p websocket → test result ok. 1 passed; 0 fa lint:`lua` 语法、`check_mirror_urls`(plain-string url 不触发镜像约束)、`check_package_name` 全过; `mcpp xpkg parse` 在本地较新 mcpp(2026.8.4.1)与 CI pin(2026.8.3.3)上均 OK。 -## 7. 后续待办 +## 7. 后续:server + zlib features(同一天,独立 PR 叠加在 #158 上) + +基座合流后,又补了两个可选 feature,`pkgs/c/compat.websocket.lua` 增加 `features` 表: + +- **`server`**:把基座剔掉的 4 个 TU 编回来。逐文件核对过,它们只 include 标准库与库内已编的 IX 头, + 零新增外部依赖。消费端 `features = ["server"]` 即获得 `ix::WebSocketServer`(`SocketServer` 派生, + 自带 `getPort()` 等)。 +- **`zlib`**:`defines = { "IXWEBSOCKET_USE_ZLIB=1" }` + `deps = { ["compat.zlib"] = "1.3.2" }`, + 把 gzip codec 从 no-op 变成真正的 permessage-deflate 压缩。define 只到包自身 TU,消费端无需 + 拿到 —— 客户端 `enablePerMessageDeflate()`、server 默认开。 + +**`server` implies `zlib` —— 一个靠读源码才发现的正确性坑**:`IXWebSocketServer` 默认开启 +permessage-deflate(`_enablePerMessageDeflate = true`),而 transport 的扩展协商**不受** +`IXWEBSOCKET_USE_ZLIB` 门控(只有 codec 被门控)。若 server 编了而 zlib 没编,server 会宣称压缩、 +却执行不了 —— 因此用具名 feature 的 `implies = { "zlib" }` 强制一起开(参考 `compat.eigen` 的 +`use_blas`;`default` feature 的 implies 恒生效是另一个已知坑,具名 feature 不受影响)。 + +### 验证(与 CI 一致:mcpp 2026.8.3.3 + gcc@16.1.0 + `MCPP_BUILD_CACHE=local`) + +``` +mcpp test -p websocket → test result ok. 1 passed; 0 failed (默认零依赖基座,回归) +mcpp test -p websocket-features → test result ok. 1 passed; 0 failed (server + zlib) +``` + +- `websocket-features` 成员:起真实 `ix::WebSocketServer` 于 loopback,text/binary 往返;**压缩在 + 线路上可观测**:64 KiB 重复 `'a'` 往返,`message.str.size()` = 65536 而 `wireSize` = **80** —— + 该字段在 transport 里取原始帧负载大小(压缩后),`str` 是解压后内容,比值即压缩证据。 +- **负向验证**:默认成员(无 feature)的构建产物里,4 个 server TU 的 `.o` 不存在,`nm` 全量 + 无 `WebSocketServer`/`SocketServer`/`HttpServer` 符号 —— 消费端不开 `server` 却引用它会链接报错, + feature 门控真实生效。 + +## 8. 后续待办 - **CN 镜像**:有 `mcpp-res` 写权限后,`gtc` 建 `mcpp-res/websocket`、传与 GLOBAL 字节一致的 `IXWebSocket-12.0.1.tar.gz`,把三平台 url 改写为 `{ GLOBAL, CN }`(sha 不变)。 -- **TLS feature**(可选):索引里有 `compat.openssl`/`compat.mbedtls`,若要支持 wss,可加 feature 把对应 - TLS TU 与依赖编入 —— 本包刻意先做零依赖纯客户端。 +- **TLS feature**(最后再考虑):索引里有 `compat.openssl`/`compat.mbedtls`,若要支持 wss,可加 feature + 把对应 TLS TU 与依赖编入(linux+openssl / windows+mbedtls / mac+SecureTransport)。它是唯一会显著 + 拖慢 CI 的选项(openssl 走 install() 重型构建),故排在最后。 - **C++23 薄封装头**(可选):用户计划稿提到的 `websocket.hpp`(span/format/RAII 薄层)不阻塞本包,可后续单独加。 diff --git a/README.md b/README.md index 23b086a..2ecb6b3 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Two kinds of packages live here: | Native module library (Form A) | [`mcpplibs.xpkg`](pkgs/x/xpkg.lua) · [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`tensorvia-cpu`](pkgs/t/tensorvia-cpu.lua) · [`ffmpeg`](pkgs/f/ffmpeg.lua) (module layer; sources compiled directly through `compat.ffmpeg`) · [`opencv`](pkgs/o/opencv.lua) (single repository: the module layer and the full OpenCV 5 source build both live in the package, and only this descriptor stays on the index side) · [`mcpplibs.grpc`](pkgs/g/grpc.lua) (gRPC 1.83.0 — the one library here that CANNOT be a compat descriptor: upstream publishes no self-contained source artifact, its tag archive carrying abseil/protobuf/re2/boringssl/zlib as empty submodule placeholders, so [grpc-m](https://github.com/mcpplibs/grpc-m)'s release tarball IS that artifact. It vendors only gRPC's own source and takes the five dependencies from this index, so a consumer that also uses protobuf links one copy rather than two) | | C-source compat (with `features`) | [`compat.cjson`](pkgs/c/compat.cjson.lua) · [`compat.zlib`](pkgs/c/compat.zlib.lua) | | C++-source compat, one depending on the other | [`compat.abseil`](pkgs/c/compat.abseil.lua) (151 TUs; a wildcard over `absl/**` trimmed by upstream's test/benchmark naming conventions) · [`compat.protobuf`](pkgs/c/compat.protobuf.lua) (the libprotobuf runtime, 79 TUs transcribed from upstream's own `src/file_lists.cmake`; declares `compat.abseil` as a dependency because protobuf's public headers include `absl/…`, and its `gzip` feature defines `HAVE_ZLIB` and pulls `compat.zlib`, while `upb` adds protobuf's 64-TU C runtime out of the same tarball) · [`compat.re2`](pkgs/c/compat.re2.lua) (22 TUs, upstream's own `RE2_SOURCES`) | -| C++-source compat, client-only with zero deps | [`compat.websocket`](pkgs/c/compat.websocket.lua) (IXWebSocket 12.0.1 — a pure RFC 6455 client compiled from upstream's `IXWEBSOCKET_SOURCES` minus the four server TUs. TLS is off (no `IXWEBSOCKET_USE_TLS`; the OpenSSL/MbedTLS/AppleSSL TUs aren't built) and zlib is off too (`IXWEBSOCKET_USE_ZLIB` unset, so the gzip codec compiles to a no-op) — no external dependency at all. Its test brings its own minimal RFC 6455 echo server on loopback sockets, so the handshake, masking, fragmentation and close are exercised offline) | +| C++-source compat, zero-dep client + optional components | [`compat.websocket`](pkgs/c/compat.websocket.lua) (IXWebSocket 12.0.1 — a pure RFC 6455 client compiled from upstream's `IXWEBSOCKET_SOURCES` minus the four server TUs, so the **base build has zero external dependencies**: TLS off (the OpenSSL/MbedTLS/AppleSSL TUs aren't built) and `IXWEBSOCKET_USE_ZLIB` unset, so the gzip codec compiles to a no-op. Two optional features add on top: `server` (the four server TUs — `IXWebSocketServer`, `IXSocketServer`, `IXHttpServer`, `IXWebSocketProxyServer` — needing nothing external, and it **implies `zlib`** because upstream's server advertises permessage-deflate by default, which the transport negotiates regardless of the define) and `zlib` (deps `compat.zlib` and turns the codec into real per-message-deflate compression). The default-feature test brings its own minimal RFC 6455 echo server on loopback sockets (handshake, masking, fragmentation and close all exercised offline); a second member, `websocket-features`, runs a real `ix::WebSocketServer` and asserts the compression is observable on the wire — a 64 KiB repeated payload round-trips with `wireSize` = 80) | | header-only (with `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) | | Runtime loader compat (pure sources, sidestepping upstream codegen/asm) | [`compat.vulkan`](pkgs/c/compat.vulkan.lua) (the Khronos loader: `loader/generated/` is checked in, and the assembly path degrades to plain C through `UNKNOWN_FUNCTIONS_SUPPORTED`, so no CMake/Python/assembler is needed; windows deferred) · [`compat.vulkan-headers`](pkgs/c/compat.vulkan-headers.lua) | | Whole-source direct build + generated config (only where a platform lacks one) | [`compat.curl`](pkgs/c/compat.curl.lua) (win32 uses upstream's checked-in config, unix generates one) · [`compat.sdl2`](pkgs/c/compat.sdl2.lua) (win/mac use upstream's checked-in config; linux generates one and enables X11 by hand) · [`compat.c-ares`](pkgs/c/compat.c-ares.lua) (91 TUs; the release tarball already ships `ares_build.h` and a Windows config, so only `ares_config.h` is snapshotted per OS) | diff --git a/README.zh-CN.md b/README.zh-CN.md index 111e958..1b10781 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -37,7 +37,7 @@ mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上 | 原生模块库(Form A) | [`mcpplibs.xpkg`](pkgs/x/xpkg.lua) · [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`tensorvia-cpu`](pkgs/t/tensorvia-cpu.lua) · [`ffmpeg`](pkgs/f/ffmpeg.lua)(模块层,源码经 `compat.ffmpeg` 直编) · [`opencv`](pkgs/o/opencv.lua)(单仓库:模块层与 OpenCV 5 全源码构建同在包内,索引侧只留本描述符) · [`mcpplibs.grpc`](pkgs/g/grpc.lua)(gRPC 1.83.0 —— 本索引里唯一**无法**做成 compat 描述符的库:上游不发布任何自包含源码产物,其 tag 归档里 abseil/protobuf/re2/boringssl/zlib 全是空 submodule 占位,因此 [grpc-m](https://github.com/mcpplibs/grpc-m) 的 release tarball 才是那个产物。它只 vendor gRPC 自己的源码,五个依赖全取自本索引,故同时直接使用 protobuf 的消费者链进去的是同一份而非两份)| | C 源码 compat(含 `features`) | [`compat.cjson`](pkgs/c/compat.cjson.lua) · [`compat.zlib`](pkgs/c/compat.zlib.lua) | | C++ 源码 compat(彼此依赖) | [`compat.abseil`](pkgs/c/compat.abseil.lua)(151 TU;对 `absl/**` 取通配后,按上游自身的 test/benchmark 命名约定裁剪) · [`compat.protobuf`](pkgs/c/compat.protobuf.lua)(libprotobuf 运行时,79 TU 逐条转录自上游 `src/file_lists.cmake`;因 protobuf 公开头文件 include 了 `absl/…`,故显式依赖 `compat.abseil`;`gzip` feature 定义 `HAVE_ZLIB` 并拉入 `compat.zlib`,`upb` feature 则从同一个 tarball 里再编出 protobuf 的 64 TU C 运行时) · [`compat.re2`](pkgs/c/compat.re2.lua)(22 TU,取自上游自身的 `RE2_SOURCES`) | -| C++ 源码 compat(纯客户端,零依赖) | [`compat.websocket`](pkgs/c/compat.websocket.lua)(IXWebSocket 12.0.1 —— 从上游 `IXWEBSOCKET_SOURCES` 剔掉 4 个 server TU 后直编的纯 RFC 6455 客户端。TLS 关闭(不定义 `IXWEBSOCKET_USE_TLS`,OpenSSL/MbedTLS/AppleSSL 三组 TU 均不编),zlib 也关闭(`IXWEBSOCKET_USE_ZLIB` 不定义,gzip codec 编译为 no-op),于是零外部依赖。其测试自带一个基于 loopback 原始 socket 的最小 RFC 6455 echo server,握手/掩码/分片/关闭全部离线实测) | +| C++ 源码 compat(零依赖客户端 + 可选组件) | [`compat.websocket`](pkgs/c/compat.websocket.lua)(IXWebSocket 12.0.1 —— 从上游 `IXWEBSOCKET_SOURCES` 剔掉 4 个 server TU 后直编的纯 RFC 6455 客户端,**基座零外部依赖**:TLS 关闭(OpenSSL/MbedTLS/AppleSSL 三组 TU 均不编),`IXWEBSOCKET_USE_ZLIB` 不定义(gzip codec 编译为 no-op)。两个可选 feature 在基座上叠加:`server`(4 个 server TU —— `IXWebSocketServer`/`IXSocketServer`/`IXHttpServer`/`IXWebSocketProxyServer`,零新增外部依赖,且 **implies `zlib`** —— 因为上游 server 默认就宣称 permessage-deflate,而 transport 的协商不受宏门控)与 `zlib`(依赖 `compat.zlib`,把 codec 变成真正的 permessage-deflate 压缩)。默认构建的测试自带基于 loopback 原始 socket 的最小 RFC 6455 echo server(握手/掩码/分片/关闭全部离线实测);第二个成员 `websocket-features` 跑真实的 `ix::WebSocketServer`,并断言压缩在线路上可观测 —— 64 KiB 重复载荷往返,`wireSize` = 80) | | header-only(含 `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) | | 运行时 loader compat(纯源码,绕开上游 codegen/asm) | [`compat.vulkan`](pkgs/c/compat.vulkan.lua)(Khronos loader:`loader/generated/` 已签入,汇编路径经 `UNKNOWN_FUNCTIONS_SUPPORTED` 降级为纯 C,故无需 CMake/Python/汇编器;windows 延后)· [`compat.vulkan-headers`](pkgs/c/compat.vulkan-headers.lua) | | 全源码直编 + 生成 config(仅缺口平台) | [`compat.curl`](pkgs/c/compat.curl.lua)(win32 用上游签入 config,unix 生成) · [`compat.sdl2`](pkgs/c/compat.sdl2.lua)(win/mac 用上游签入 config,linux 生成 + 手工开 X11) · [`compat.c-ares`](pkgs/c/compat.c-ares.lua)(91 TU;release tarball 已自带 `ares_build.h` 与 Windows 配置,故只需按 OS 冻结 `ares_config.h`) | diff --git a/mcpp.toml b/mcpp.toml index f73b421..908d98d 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -63,6 +63,7 @@ members = [ "tests/examples/tinyhttps", "tests/examples/vulkan", "tests/examples/websocket", + "tests/examples/websocket-features", "tests/examples/tray", "tests/examples/yyjson", "tests/examples/magic_enum", diff --git a/pkgs/c/compat.websocket.lua b/pkgs/c/compat.websocket.lua index 2d7c3ab..c0d2dcb 100644 --- a/pkgs/c/compat.websocket.lua +++ b/pkgs/c/compat.websocket.lua @@ -117,6 +117,52 @@ package = { targets = { ["websocket"] = { kind = "lib" } }, + -- ── Optional components ────────────────────────────────────────── + -- The base build is the zero-dependency client above; two components + -- can be enabled on top. mcpp features only ADD, so a consumer naming + -- one keeps everything else: + -- + -- websocket = "12.0.1" -> client only + -- websocket = { …, features = ["server"] } -> + the server (implies zlib) + -- websocket = { …, features = ["zlib"] } -> + compression + -- websocket = { …, features = ["server", "zlib"] } -> + both + -- + -- `server` brings the four TUs the base build leaves out. It needs + -- nothing external: every IX* header it touches is already compiled + -- into the client, and its external includes are stdlib only (checked + -- against the four .cpp files). `IXWebSocketServer` derives from + -- `SocketServer`, so consumers reach `getPort()` etc. through it. + -- + -- `server` IMPLIES `zlib`: upstream's server enables permessage-deflate + -- by default and the transport's extension negotiation is NOT gated on + -- IXWEBSOCKET_USE_ZLIB (only the gzip codec is). A server built without + -- the define would offer compression its codec cannot perform, so the + -- implication keeps every server build capable of what it advertises. + -- + -- `zlib` turns the gzip codec from its no-op into real + -- permessage-deflate compression. Only + -- IXWebSocketPerMessageDeflateCodec.cpp is gated by + -- IXWEBSOCKET_USE_ZLIB; the negotiation logic in + -- IXWebSocketPerMessageDeflate.cpp is zlib-free. The define reaches + -- this package's own TUs — consumers need no define, they just call + -- enablePerMessageDeflate() on their client (and the server enables it + -- by default). + features = { + ["server"] = { + implies = { "zlib" }, + sources = { + "*/ixwebsocket/IXSocketServer.cpp", + "*/ixwebsocket/IXHttpServer.cpp", + "*/ixwebsocket/IXWebSocketServer.cpp", + "*/ixwebsocket/IXWebSocketProxyServer.cpp", + }, + }, + ["zlib"] = { + defines = { "IXWEBSOCKET_USE_ZLIB=1" }, + deps = { ["compat.zlib"] = "1.3.2" }, + }, + }, + -- ── Platform-specific ────────────────────────────────────────────── -- Upstream: Threads::Threads on UNIX, wsock32/ws2_32/shlwapi + -- _CRT_SECURE_NO_WARNINGS on Windows. shlwapi is only reached from the diff --git a/tests/examples/websocket-features/mcpp.toml b/tests/examples/websocket-features/mcpp.toml new file mode 100644 index 0000000..ff08343 --- /dev/null +++ b/tests/examples/websocket-features/mcpp.toml @@ -0,0 +1,17 @@ +# websocket-features test project: consumes compat.websocket with the `server` +# and `zlib` features enabled and asserts them under `mcpp test`. Part of the +# mcpp-index self-referential workspace — the workspace-root `[indices]` +# redirect points at this repo, so the dependency resolves to the checked-in +# recipe (pkgs/c/compat.websocket.lua). +# +# The test is entirely OFFLINE: it runs an ix::WebSocketServer (the `server` +# feature) on loopback and drives the ix::WebSocket client against it, then +# checks that per-message-deflate (the `zlib` feature) actually compresses on +# the wire. The sibling member tests/examples/websocket covers the zero-dep +# client with no features; this member exists to prove the features. +[package] +name = "websocket-features-tests" +version = "0.1.0" + +[dependencies.compat] +websocket = { version = "12.0.1", features = ["server", "zlib"] } diff --git a/tests/examples/websocket-features/tests/ws_features_test.cpp b/tests/examples/websocket-features/tests/ws_features_test.cpp new file mode 100644 index 0000000..df00c5e --- /dev/null +++ b/tests/examples/websocket-features/tests/ws_features_test.cpp @@ -0,0 +1,257 @@ +// ws_features_test.cpp — asserts compat.websocket's `server` and `zlib` +// features, enabled through the member's dependency declaration. +// +// * `server`: an ix::WebSocketServer runs inside the test process on +// loopback and echoes every message; the client's round-trips go through +// the REAL server class (the sibling member tests/examples/websocket uses +// a hand-written raw-socket echo server precisely so the zero-dep client +// is tested against something independent — here the point is the opposite: +// prove the compiled-in server works). +// * `zlib`: the client enables per-message-deflate. A highly-compressible +// payload must come back as a message whose str is the full payload but +// whose wireSize is the COMPRESSED frame bytes (the transport delivers +// str = decompressed content and wireSize = raw wire payload), which is +// how a compression round-trip is observable without sniffing the socket. +// +// No network access is required; everything stays on 127.0.0.1. + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Raw sockets only for find_free_port() (a fixed test port would collide on +// shared CI runners; port 0 is the race-free way to ask the OS). +#ifdef _WIN32 +#include +#include +using SockType = SOCKET; +constexpr SockType kInvalidSocket = INVALID_SOCKET; +#else +#include +#include +#include +#include +using SockType = int; +constexpr SockType kInvalidSocket = -1; +#endif + +namespace +{ + +int g_failures = 0; + +void check(bool ok, const std::string& what) +{ + if (ok) + { + std::cout << " ok: " << what << "\n"; + } + else + { + std::cout << " FAIL: " << what << "\n"; + ++g_failures; + } +} + +int find_free_port() +{ +#ifdef _WIN32 + WSADATA wsa; + WSAStartup(MAKEWORD(2, 2), &wsa); +#endif + SockType s = socket(AF_INET, SOCK_STREAM, 0); + if (s == kInvalidSocket) return -1; + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + if (bind(s, reinterpret_cast(&addr), sizeof(addr)) != 0) + { +#ifdef _WIN32 + closesocket(s); +#else + ::close(s); +#endif + return -1; + } +#ifdef _WIN32 + int len = sizeof(addr); +#else + socklen_t len = sizeof(addr); +#endif + if (getsockname(s, reinterpret_cast(&addr), &len) != 0) + { +#ifdef _WIN32 + closesocket(s); +#else + ::close(s); +#endif + return -1; + } + int port = ntohs(addr.sin_port); +#ifdef _WIN32 + closesocket(s); +#else + ::close(s); +#endif + return port; +} + +// ── Client-side message collector ────────────────────────────────────────── + +struct RecvMsg +{ + ix::WebSocketMessageType type; + std::string str; + bool binary; + size_t wireSize; +}; + +class Collector +{ +public: + void add(const ix::WebSocketMessagePtr& msg) + { + std::lock_guard lk(m_); + msgs_.push_back(RecvMsg{msg->type, msg->str, msg->binary, msg->wireSize}); + cv_.notify_all(); + } + + template + bool wait_for(Pred pred, int timeout_ms) + { + std::unique_lock lk(m_); + return cv_.wait_for(lk, std::chrono::milliseconds(timeout_ms), [&] { return pred(msgs_); }); + } + +private: + std::mutex m_; + std::condition_variable cv_; + std::vector msgs_; +}; + +} // namespace + +int main() +{ + std::cout << std::unitbuf; + + bool netOk = ix::initNetSystem(); + check(netOk, "ix::initNetSystem()"); + + int port = find_free_port(); + check(port > 0, "found a free loopback port"); + if (port <= 0) + { + ix::uninitNetSystem(); + return 1; + } + + // The `server` feature: a real ix::WebSocketServer. + ix::WebSocketServer server(port, "127.0.0.1"); + server.setOnClientMessageCallback( + [](std::shared_ptr /*state*/, + ix::WebSocket& ws, + const ix::WebSocketMessagePtr& msg) + { + if (msg->type == ix::WebSocketMessageType::Message) + { + if (msg->binary) ws.sendBinary(msg->str); + else ws.sendText(msg->str); + } + }); + check(server.listenAndStart(), "ix::WebSocketServer listenAndStart()"); + + Collector collector; + ix::WebSocket ws; + ws.setUrl("ws://127.0.0.1:" + std::to_string(port) + "/"); + ws.enablePerMessageDeflate(); + ws.setOnMessageCallback([&](const ix::WebSocketMessagePtr& msg) { collector.add(msg); }); + ws.start(); + + check(collector.wait_for( + [](const std::vector& v) { + for (const auto& m : v) + if (m.type == ix::WebSocketMessageType::Open) return true; + return false; + }, + 5000), + "client opened against ix::WebSocketServer"); + + // Text round-trip through the real server. + ws.sendText("hello, real server!"); + check(collector.wait_for( + [](const std::vector& v) { + for (const auto& m : v) + if (m.type == ix::WebSocketMessageType::Message && + m.str == "hello, real server!" && !m.binary) + return true; + return false; + }, + 5000), + "text message echoed by ix::WebSocketServer"); + + // Binary round-trip through the real server. + std::string bin("\x00\x01\xfe\xff\x80\x01binary", 10); + ws.sendBinary(bin); + check(collector.wait_for( + [&](const std::vector& v) { + for (const auto& m : v) + if (m.type == ix::WebSocketMessageType::Message && m.str == bin && m.binary) + return true; + return false; + }, + 5000), + "binary message echoed by ix::WebSocketServer"); + + // The `zlib` feature: per-message-deflate must actually compress on the + // wire. 64 KiB of a repeated byte compresses to a few hundred bytes; the + // client delivers the FULL decompressed str but reports wireSize = the raw + // (compressed) frame payload, so the ratio is observable. + const size_t kBig = 65536; + ws.sendText(std::string(kBig, 'a')); + size_t compressedWire = 0; + bool gotBig = collector.wait_for( + [&](const std::vector& v) { + for (const auto& m : v) + if (m.type == ix::WebSocketMessageType::Message && m.str.size() == kBig && + !m.binary) + { + compressedWire = m.wireSize; + return true; + } + return false; + }, + 5000); + check(gotBig, "64 KiB compressible message round-tripped"); + if (gotBig) + { + check(compressedWire < 1024, + "per-message-deflate compressed the wire (wireSize=" + std::to_string(compressedWire) + + " << 65536)"); + } + + ws.stop(); + server.stop(); + ix::uninitNetSystem(); + + if (g_failures == 0) + { + std::cout << "ALL WEBSOCKET-FEATURES ASSERTIONS PASSED\n"; + return 0; + } + std::cout << g_failures << " ASSERTION(S) FAILED\n"; + return 1; +}