WebSocket на C++11 и Rust: сравнительный анализ библиотек и двух реализаций одного протокола

от автора

WebSocket — один из самых распространенных транспортов для обмена данными в реальном времени: чаты, биржевые котировки, игровые серверы, IoT. На практике выбор библиотеки редко сводится к вопросу «кто быстрее парсит заголовок фрейма». Важнее сочетание совместимости со старым набором инструментов сборки, поддержки TLS, сжатия per-message-deflate, модели асинхронности или блокировки, размера бинарника и способа обработки ошибок.

В репозитории wscpp лежат две связанные, но независимые реализации одного протокольного стека:

  • wscpp — C++11, клиент и сервер, лицензия MIT;

  • ws-rs — Rust-компаньон с той же послойной архитектурой и теми же регрессионными векторами RFC 6455 §5.7.

Дисклеймер: обе библиотеки экспериментальные и созданы с помощью AI-агентов; люди курируют тесты, CI и релизы. Ниже — не рекламный обзор, а структурированный сравнительный анализ с воспроизводимыми цифрами.

Дисклеймер: задержки и пропускная способность — 7 июня 2026; размеры ELF перепроверены 8 июня 2026. Платформа: Linux/WSL2 (GCC 15), Release; в LAN — Debian 13 VM, RTT ~0,5–0,7 мс. В сравнительных тестах TLS не использовался (но код TLS/deflate включен в сборку). На каждый прогон — одно соединение.


Методология

Порог совместимости с RFC

Бенчмарки запускаются только после прохождения набора обязательных требований:

RFC

Содержание

Статус wscpp / ws-rs

RFC 6455

Фреймы, рукопожатие, закрытие, ping/pong, фрагментация, UTF-8 §8.1

Реализовано

RFC 2818

wss://, SNI

Реализовано

RFC 7692

Сжатие per-message-deflate

Реализовано

Сценарии измерений

Сценарий

Что измеряем

Задержка эхо-ответа (p50/p99)

100 циклов «отправил — получил» текстовым ping-сообщением на localhost и в LAN

Пропускная способность 64 KiB

100 итераций бинарных сообщений

Парсинг и сборка фреймов

1 MiB, локальный микробенчмарк (только CPU)

Задержка подключения

Только для клиентских библиотек (easywsclient)

Ограничения: одно соединение на прогон; в сравнительном наборе нет TLS; результаты в LAN зависят от реальной сети — на localhost доминируют микрооперации парсера, в LAN — сетевая карта и планировщик ОС.

Размер бинарника: что именно сравниваем

Во всех таблицах — размер ELF-файла тестового бенчмарка (stat -c%s на bench_*_roundtrip), Release-сборка. Это не «полностью статический» артефакт и не суммарный объем зависимостей на диске.

Стек

Что попадает в ELF

Что остается снаружи (динамические .so)

wscpp (C++)

код библиотеки (libwscpp.a линкуется в exe)

OpenSSL (libssl, libcrypto), zlib, libstdc++, libc

ws-rs (Rust)

rustls, ring, flate2, tokio (если включен)

в основном libc, libgcc_s

libwebsockets

тонкая обертка

libwebsockets.so и ее зависимости

Следствия для читателя:

  • C++ — бинарник выглядит меньше, потому что криптография (OpenSSL ~7 МБ в системных .so) не входит в ELF. На «голой» системе без OpenSSL эти .so придется поставлять отдельно.

  • Rust — бинарник больше, как файл, но самодостаточнее: TLS и deflate вшиты в ELF.

  • Прямое сравнение «286 КБ C++ vs 1,9 МБ Rust» по размеру файла некорректно без этой оговорки. Для embedded смотрите не только ELF, но и то, есть ли OpenSSL в образе.

  • C++ — замеры: WSCPP_ENABLE_LOGGING=OFF (по умолчанию логирование включено и дает ~650 КБ вместо ~286 КБ). Rust-замеры: [profile.release] с lto = "fat", strip = true в rust/Cargo.toml.


Часть 1. Экосистема WebSocket для C++11

Критерий отбора

В каталог попали библиотеки с официальной поддержкой C++11 (или C API, вызываемого из C++11 без более нового стандарта). Решения, требующие C++17 и выше (uWebSockets, Poco Net 1.13+, seasocks), исключены.

Первый уровень: полноценные клиент и сервер

Библиотека

C++11

TLS

Асинхронность

Зависимости

websocketpp

да

да

ASIO

ASIO, OpenSSL

Boost.Beast

да

да

Boost.Asio

Boost, OpenSSL

IXWebSocket

да

да

потоки

zlib, OpenSSL/MbedTLS

Simple-WebSocket-Server

да

да

ASIO

ASIO, OpenSSL

wscpp

да

да

ASIO или POSIX

ASIO† или только OpenSSL

† ASIO 1.20 подтягивается через FetchContent при WSCPP_USE_ASIO=ON.

Второй и третий уровни: минималисты и C-стеки

  • easywsclient (~600 строк) — только блокирующий клиент, без TLS;

  • libwebsockets — зрелый production-стек на C, но с другой моделью интеграции.

Результаты на localhost (задержка эхо и размер бинарника)

Библиотека

p50

p99

64 KiB

Размер бинарника

wscpp (linux POSIX)

0,25 мс

0,36 мс

92 МБ/с

286 КБ

wscpp (ASIO)

0,25 мс

0,32 мс

82 МБ/с

383 КБ†

websocketpp 0.8.2

0,31 мс

0,59 мс

687 КБ

IXWebSocket 11.4.6

0,28 мс

0,68 мс

62 МБ/с

473 КБ

libwebsockets 4.3.5

0,26 мс

0,40 мс

126 КБ*

Boost.Beast 1.88

0,25 мс

0,32 мс

68 МБ/с

667 КБ

Simple-WebSocket-Server

0,28 мс

0,46 мс

675 КБ

* libwebsockets линкуется с системной .so — размер ELF нельзя напрямую сравнивать с аналогами, где OpenSSL подключается отдельно.

† размер ELF; OpenSSL и zlib — динамические .so (см. методологию).

Результаты в локальной сети (две машины, plain ws://)

Библиотека

p50

p99

64 KiB

wscpp (linux)

0,34 мс

1,24 мс

27 МБ/с

wscpp (ASIO)

0,40 мс

0,77 мс

28 МБ/с

websocketpp

0,32 мс

2,01 мс

IXWebSocket

0,42 мс

0,85 мс

31 МБ/с

libwebsockets

0,33 мс

3,55 мс

Beast

0,32 мс

0,86 мс

29 МБ/с

Вывод по C++: по медиане (p50) на localhost все библиотеки первого уровня укладываются в ~0,25–0,31 мс. Различия заметнее в хвосте распределения (p99): websocketpp и libwebsockets на LAN показывают более высокие значения. Особняком стоят размер ELF и модель API. Транспорт wscpp на POSIX-сокетах дает самый компактный файл бенчмарка среди полнофункциональных C++ — стеков — при условии, что OpenSSL уже есть в системе.

wscpp: два транспорта

# Минимальный ELF — POSIX, OpenSSL динамическиcmake -B build-linux -DWSCPP_BUILD_BENCHMARKS=ON \  -DWSCPP_USE_ASIO=OFF -DWSCPP_ENABLE_LOGGING=OFF# Кроссплатформенный ASIOcmake -B build-asio -DWSCPP_BUILD_BENCHMARKS=ON \  -DWSCPP_USE_ASIO=ON -DWSCPP_ENABLE_LOGGING=OFF

Транспорт

p50

p99

64 KiB

ELF (КиБ)

linux POSIX

0,25 мс

0,36 мс

92 МБ/с

286 КБ

ASIO

0,25 мс

0,32 мс

82 МБ/с

383 КБ

Микробенчмарки слоя фреймов (не зависят от транспорта):

Операция

Пропускная способность

Сборка фрейма

~15 ГБ/с

Парсинг фрейма

~22 ГБ/с

XOR-маскирование

~70 ГБ/с


Часть 2. Экосистема WebSocket для Rust

Первый уровень: асинхронные клиент и сервер на tokio

Библиотека

Среда выполнения

TLS

Лицензия

tokio-tungstenite

tokio

rustls/native-tls

MIT

fastwebsockets

tokio / ручной режим

rustls

Apache-2.0

tokio-websockets

tokio

rustls

MIT

ws-rs

tokio + опционально блокирующий режим

rustls

MIT

Результаты на localhost

Библиотека

p50

p99

64 KiB

ELF (КиБ)

ws-rs (tokio)

0,26 мс

0,37 мс

78 МБ/с

2311 КБ

ws-rs (блокирующий)

0,25 мс

0,31 мс

84 МБ/с

1895 КБ

tokio-tungstenite

0,30 мс

0,41 мс

76 МБ/с

856 КБ

fastwebsockets

0,29 мс

0,55 мс

83 МБ/с

1037 КБ

tokio-websockets

0,31 мс

0,44 мс

85 МБ/с

779 КБ

Результаты в локальной сети

Библиотека

p50

p99

64 KiB

ws-rs (tokio)

0,35 мс

0,51 мс

30 МБ/с

ws-rs (блокирующий)

0,40 мс

0,51 мс

30 МБ/с

tokio-tungstenite

0,37 мс

0,67 мс

30 МБ/с

fastwebsockets

0,36 мс

0,60 мс

30 МБ/с

tokio-websockets

0,38 мс

0,67 мс

30 МБ/с

Вывод по Rust: tokio-tungstenite — де-факто стандарт экосистемы. fastwebsockets и tokio-websockets конкурируют по пропускной способности на localhost. По размеру ELF ws-rs тяжелее wscpp: в Rust-бинарник вшиты rustls, ring и flate2, тогда как у C++ OpenSSL остается в .so. Блокирующий bench_blocking_roundtrip (~1,9 МБ) меньше tokio-варианта (~2,3 МБ), но все равно крупнее C++ ELF (~286 КБ) — зато автономнее по зависимостям.

Замеры Rust — через cargo build --release -p ws-rs-benches --bins (все default-features ws-rs). Для библиотеки без tokio в своем приложении:

cargo build -p ws-rs --release --no-default-features \  --features "std-blocking,deflate,blocking-tls"

Размер итогового бинарника приложения будет меньше, чем у тестового bench_blocking_roundtrip, который собирается с полным набором возможностей для сопоставимости с C++ — стеком.

В LAN все четыре библиотеки сходятся к ~30 МБ/с — упираются в сеть, а не в парсер.


Часть 3. C++ и Rust: одна архитектура, разные компромиссы

Проект wscpp — редкий случай, когда один протокольный стек реализован дважды с общими регрессионными векторами и зеркальными тестовыми стендами.

Архитектура

Слой

wscpp (C++)

ws-rs (Rust)

Фреймы

frame::parser / builder

frame::Parser / FrameBuilder

Рукопожатие

handshake

handshake

Соединение

поток + транспорт

tokio TcpStream или std::net

Публичный API

client, server (обратные вызовы)

Client, Server (асинхронный опрос)

Ошибки

std::error_code

Result<T, Error>

TLS

OpenSSL

rustls

Паритет функций

Возможность

wscpp

ws-rs

RFC 6455

да

да

wss://

OpenSSL

rustls

per-message-deflate

да (v1.1.0)

да (v0.3.0+)

Два транспорта

ASIO / linux POSIX

tokio / std-blocking

Векторы §5.7

6 тестов

6 тестов (портированы)

Прямое сравнение на localhost

Метрика

wscpp (linux)

wscpp (ASIO)

ws-rs (tokio)

ws-rs (блокирующий)

p50

0,25 мс

0,25 мс

0,26 мс

0,25 мс

p99

0,36 мс

0,32 мс

0,37 мс

0,31 мс

64 KiB

92 МБ/с

82 МБ/с

78 МБ/с

84 МБ/с

ELF

286 КБ†

383 КБ†

2311 КБ

1895 КБ

Динамические .so

OpenSSL, zlib

OpenSSL, zlib

libc

libc

† OpenSSL не входит в ELF — см. методологию.

Микробенчмарки слоя фреймов

Операция

wscpp (ориентир)

ws-rs до v0.4.x

ws-rs v0.4.x

Сборка 1 MiB

~15 ГБ/с

8886 МБ/с

~16 100 МБ/с

Парсинг 1 MiB

~22 ГБ/с

19701 МБ/с

~17 100 МБ/с*

XOR-маскирование

~70 ГБ/с

49664 МБ/с

~62 600 МБ/с

* путь парсера не менялся — разброс ±15 % на WSL2, операция упирается в memcpy.

Что дал проход оптимизаций ws-rs (v0.4.x)

После выхода на функциональный паритет с wscpp слой ws-rs прошел отдельный проход по скорости (все — в безопасном Rust, unsafe_code = "forbid"):

  • Профиль релиза: lto = "fat", codegen-units = 1, strip — межмодульная оптимизация и подстановка функций; тесты остаются на dev-профиле, раскрутка стека при панике сохранена.

  • Кодировщик фрейма в один проход: полезная нагрузка копируется в выходной буфер один раз и маскируется на месте — было до трех копий на отправку, стало одна.

  • Parser::take_frame(): разобранная нагрузка отдается через mem::take, без клонирования.

  • Постоянный буфер чтения с курсором (чанк 64 KiB) в обоих транспортах: нет аллокации Vec на каждый read_frame, курсор убирает drain на каждый байт, крупные чтения экономят системные вызовы. Попутно устранена скрытая потеря фреймов, пришедших «в хвосте» одного read().

  • Cow<[u8]> на отправке: на несжатом (типичном) пути нет to_vec().

Итог: сборка фрейма выросла ~в 1,85 раза и вплотную подошла к C++ — ориентиру (~16 против ~15 ГБ/с), XOR-маскирование — ~в 1,3 раза. Пропускная способность 64 KiB на localhost поднялась с 74 до ~78 МБ/с (tokio) и с 79 до ~84 МБ/с (блокирующий режим). На сквозной задержке эхо разница по-прежнему сглаживается: доминируют системные вызовы, планировщик и копирование в буфер сокета. Те же приемы — маскирование на месте, передача владения вместо O(n)-копии — применяет и fastwebsockets; дополнительно RUSTFLAGS="-C target-cpu=native" включает более широкую автовекторизацию маски (ценой переносимости бинарника).


Плюсы и минусы реализаций

wscpp (C++)

Плюсы:

  • C++11 без Boost — встраивается в унаследованный код и embedded-сборки;

  • Два транспорта: linux POSIX (286 КБ ELF) и ASIO (кроссплатформа);

  • std::error_code на всех путях ввода-вывода — без исключений; удобно при -fno-exceptions;

  • Самый компактный ELF среди полнофункциональных C++ — аналогов (OpenSSL — в .so);

  • Явное разделение на слои frame → connection → client/server; 94 автотеста в репозитории;

  • ASIO 1.20 подключается через FetchContent без ручной установки зависимостей.

Минусы:

  • Экспериментальный статус, код сгенерирован с помощью AI — перед продакшеном нужна независимая проверка;

  • Меньше примеров и расширений, чем у websocketpp, Beast или libwebsockets;

  • Проверка клиентского сертификата по умолчанию отключена (verify_none) — удобно для разработки, не для продакшена;

  • per-message-deflate включается явно через enable_permessage_deflate();

  • На LAN p99 у linux-транспорта (1,24 мс) хуже, чем у ASIO (0,77 мс) — вероятная проблема с хвостом распределения в транспорте на POSIX poll.

ws-rs (Rust)

Плюсы:

  • Идиоматичный Rust: Result, владение, документация на все публичные элементы, предупреждения clippy трактуются как ошибки в CI;

  • Паритет с wscpp — общие RFC-векторы, зеркальные скрипты бенчмарков;

  • Два режима: tokio и блокирующий std::net без runtime;

  • Безопасность памяти на уровне языка; на протокольных путях нет panic;

  • Оптимизированные горячие пути (v0.4.x): кодировщик фрейма в один проход, буфер чтения с курсором, Cow на отправке — без аллокаций на сообщение, все в безопасном Rust;

  • Микробенчмарки фреймов — одни из лучших показателей в наборе (~63 ГБ/с XOR-маскирование, ~16 ГБ/с сборка фрейма).

Минусы:

  • Крупный ELF (~2,3 МБ tokio, ~1,9 МБ blocking) — rustls/ring/flate2 внутри файла; выбор runtime заметно влияет на размер;

  • Молодая библиотека, та же экспериментальная оговорка;

  • Меньше сообщества, чем у tokio-tungstenite;

  • API с опросом (read_message, recv_text) — не обратные вызовы, как в C++; при миграции придется переписать цикл обработки событий.

Сравнение экосистем (широкий взгляд)

Критерий

C++

Rust

Legacy C++11, минимум зависимостей

wscpp linux / easywsclient

ws-rs std-blocking

Максимум возможностей

websocketpp, Beast

tokio-tungstenite

Мобильные / кроссплатформенные SDK

IXWebSocket

Зрелый C-стек для эксплуатации

libwebsockets

Без исключений

wscpp

Rust по умолчанию

Минимальный ELF бенчмарка (полный стек)

wscpp 286 КБ (+ OpenSSL .so)

ws-rs blocking 1895 КБ (TLS внутри)

Кросс-языковая регрессия RFC

wscpp + ws-rs вместе

wscpp + ws-rs вместе


Практические рекомендации

Задача

Рекомендация

Embedded / минимальный ELF на C++11 (OpenSSL в системе)

wscpp WSCPP_USE_ASIO=OFF

Embedded без OpenSSL в образе

сравните суммарный объем: C++ ELF + .so против Rust ELF с rustls

Существующий ASIO-проект

wscpp ASIO или websocketpp

Rust + tokio микросервис

tokio-tungstenite (зрелость) или ws-rs (RFC-паритет с wscpp)

Блокирующий Rust без async runtime

ws-rs --no-default-features --features std-blocking,...

Продакшен без ревью кода

Ни wscpp, ни ws-rs — сначала аудит; для C++ смотрите Beast/libwebsockets

Нужен per-message-deflate

wscpp 1.1+, IXWebSocket, Beast, libwebsockets, ws-rs 0.3+


Как воспроизвести

C++ — оба транспорта и сравнение с аналогами:

# Для сопоставимых размеров ELF отключите логирование:cmake -B build-bench-linux -DWSCPP_BUILD_BENCHMARKS=ON \  -DWSCPP_USE_ASIO=OFF -DWSCPP_ENABLE_LOGGING=OFF -DCMAKE_BUILD_TYPE=Releasecmake --build build-bench-linux --target run_benchmarks -j"$(nproc)"bash benchmarks/run_benchmarks_both.shbash benchmarks/run_remote_network_compare.sh

Rust:

bash rust/benchmarks/run_benchmarks.shbash rust/benchmarks/run_remote_network_compare.sh

Полные таблицы и журнал измерений: ANALYSIS.md, ANALYSIS_RUST.md.


Заключение

Экосистемы WebSocket на C++11 и Rust не делятся на «быстрые» и «медленные» на типичных сценариях эхо-ответа: медиана на localhost у всех библиотек первого уровня укладывается в доли миллисекунды. Реальный выбор определяется сопутствующими факторами:

  • Стандарт языка и toolchain (C++11 vs Rust 2021);

  • Модель параллелизма (обратные вызовы + поток vs async/await vs блокирующий std::net);

  • Размер и модель линковки (286 КБ ELF wscpp + OpenSSL .so vs 1,9–2,3 МБ ws-rs с TLS внутри ELF);

  • Обработка ошибок (error_code vs Result);

  • Зрелость и сообщество (Beast, libwebsockets, tokio-tungstenite vs экспериментальные wscpp/ws-rs).

Пара wscpp + ws-rs полезна как RFC реализация на двух языках: один набор RFC-требований, общие регрессионные векторы, сопоставимые тестовые стенды — редкий инструмент для проверки «правильно ли мы понимаем RFC 6455», а не только для выбора библиотеки в продакшене.

ссылка на оригинал статьи https://habr.com/ru/articles/1044686/