websocketclient-cpp
)A transport-agnostic, high-performance, header-only C++23 WebSocket client library with minimal dependencies.
permessage-deflate
protocol extension, RFC 7692)zlib-ng
over zlib
library for improved (de-)compression performance on modern architectures-fno-exceptions
)NOTE: Despite being used in production, this library is still under development and the API may change.
Dependency | Description | Required |
---|---|---|
simdutf | SIMD instructions based UTF-8 validator used for text messages payload validation. | Optional |
openssl 3 | WebSocket Secure (WSS) support. | If using WSS. |
zlib | Message compression support through permessage-deflate extension. | If using compression (permessage-deflate). |
zlib-ng | Faster alternative to zlib library with optimizations for modern CPUs. |
If using compression (permessage-deflate), alternative to zlib . |
See the examples directory for more information.
For configuration of dependencies, refer to the section Configuration.
Working examples can be found in the examples directory. Examples exist for both built-in blocking I/O based on POSIX, and asynchronous I/O using ASIO.
Custom HTTP headers, e.g. for authentication, can be set on the Handshake
instance as follows:
handshake.get_request_header().fields.set("X-Custom-Header", "Custom-Value");
The following compile-time configuration switches can be set:
Option | Values | Description |
---|---|---|
WS_CLIENT_USE_SIMD_UTF8 |
1 or 0 |
Enable/disable SIMD instructions based UTF-8 validator for text messages payload validation. |
WS_CLIENT_USE_ZLIB_NG |
1 or 0 |
Enable/disable zlib-ng instead of zlib library for permessage-deflate compression. |
WS_CLIENT_VALIDATE_UTF8 |
1 or 0 |
Enable/disable UTF-8 validation for text messages payload. |
WS_CLIENT_LOG_HANDSHAKE |
1 or 0 |
Enable/disable handshake log messages. |
WS_CLIENT_LOG_MSG_PAYLOADS |
1 or 0 |
Enable/disable message payload log messages. |
WS_CLIENT_LOG_MSG_SIZES |
1 or 0 |
Enable/disable message size log messages. |
WS_CLIENT_LOG_FRAMES |
1 or 0 |
Enable/disable frame log messages. |
WS_CLIENT_LOG_COMPRESSION |
1 or 0 |
Enable/disable compression log messages. |
Example:
target_compile_definitions(my_binary PRIVATE
WS_CLIENT_USE_ZLIB_NG=1 # Use zlib-ng instead of zlib
WS_CLIENT_VALIDATE_UTF8=1 # Enable utf-8 validation
WS_CLIENT_USE_SIMD_UTF8=1 # Use simdutf for utf-8 validation
WS_CLIENT_LOG_HANDSHAKE=1
WS_CLIENT_LOG_MSG_PAYLOADS=0
WS_CLIENT_LOG_MSG_SIZES=1
WS_CLIENT_LOG_FRAMES=0
WS_CLIENT_LOG_COMPRESSION=0
)
The following CMake options can be set in order to build examples, tests and/or benchmarks:
Option | Description |
---|---|
WS_CLIENT_BUILD_EXAMPLES |
Build examples in the examples directory. |
WS_CLIENT_BUILD_TESTS |
Build unit tests in the test directory. |
WS_CLIENT_BUILD_BENCH |
Build performance benchmarks in the bench directory. |
WS_CLIENT_BUILD_SCRATCH |
Build scratch examples in the scratch directory. |
Output files are generated in the build
directory.
The following generates a CMake project and copies all header files to build/install
.
This is sufficient if you only want to use the library in your project.
cmake -B build -DCMAKE_INSTALL_PREFIX=build/install
cmake --build build
cmake --install build
If you want to install it locally to the system's default location, usually /usr/local/include
, omit the -DCMAKE_INSTALL_PREFIX=build/install
option.
The build is based on vcpkg
, which is used to install dependencies.
Ensure that vcpkg
is installed and the environment variable VCPKG_ROOT
is set.
cmake --preset gcc_dev_install
cmake --build --preset gcc_dev_install
cmake --install build/gcc/dev_install --config Release
cmake --preset clang_dev_install
cmake --build --preset clang_dev_install
cmake --install build/clang/dev_install --config Release
The library is designed to be transport layer agnostic, which is one of its unique features compared to other WebSocket libraries.
The library supports both blocking and non-blocking, asynchronous transport layers via WebSocketClient
, or WebSocketClientAsync
respectively.
Blocking I/O is provided by the built-in transport layersTcpSocket
and OpenSslSocket
, which uses POSIX I/O functions and OpenSSL for TLS connections.
An async I/O socket implementation is provided using the ASIO library and C++20 coroutines, see AsioSocket
class.
The user can provide their own transport layer (socket) implementation if needed. All that is required is to implement the following functions in a custom socket class:
read_some(buffer, timeout);
write_some(buffer, timeout);
shutdown(timeout);
close();
For details, see the concepts HasSocketOperations
in HasSocketOperations.hpp, or HasSocketOperationsAsync
in HasSocketOperationsAsync.hpp respectively.
By default, the library logs directly to std::clog
, hence there is no dependency to any logging library.
The default implementation allows to set the log level at compile-time, which can be used to filter log messages.
ConsoleLogger<LogLevel::I> logger;
auto client = WebSocketClient(&logger, [...]);
In this example, only log messages with log level I
(info) and higher will be printed.
The available log levels are:
enum class LogLevel : uint8_t
{
N = 0, // Disabled
E = 1, // Error
W = 2, // Warning
I = 3, // Info
D = 4 // Debug
};
You can implement a custom logger like the following. It logs all messages to std::cout
:
/**
* Custom logger implementation.
* Logs all messages to `std::cout`.
*/
struct CustomLogger
{
/**
* Check if the logger is enabled for the given log level.
*/
template <LogLevel level>
constexpr bool is_enabled() const noexcept
{
return true;
}
/**
* Log a message with the given log level.
*/
template <LogLevel level>
constexpr void log(
std::string_view message, const std::source_location loc = std::source_location::current()
) noexcept
{
std::cout << "CustomLogger: " << loc.file_name() << ":" << loc.line() << " " << message
<< std::endl;
}
};
Sometimes, changing the log-level will either show too many messages, or hide the ones of interest.
In order to filter for specific implementation details, the following compile definitions are available (0
= disabled, 1
= enabled):
Option | Values | Description |
---|---|---|
WS_CLIENT_LOG_HANDSHAKE |
1 or 0 |
Enable/disable handshake log messages. |
WS_CLIENT_LOG_MSG_PAYLOADS |
1 or 0 |
Enable/disable message payload log messages. |
WS_CLIENT_LOG_MSG_SIZES |
1 or 0 |
Enable/disable message size log messages. |
WS_CLIENT_LOG_FRAMES |
1 or 0 |
Enable/disable frame log messages. |
WS_CLIENT_LOG_COMPRESSION |
1 or 0 |
Enable/disable compression log messages. |
By setting a variable to 0
= disabled (1
= enabled), the compiler will optimize out all logging code for maximum performance.
For example, the handshake log messages are useful to inspect the HTTP headers sent and received during the WebSocket handshake. Among others, the negotiated parameters for the permessage-deflate compression extension can be inspected this way.
Alternatively, use CMake's compile definition function target_compile_definitions
to set the log levels (see above).
Template type parameters are supplemented by C++23 concepts, which are used to validate template parameters at compile-time. Concepts have the advantage to formalize requirements for a template parameter, similar to interface definitions, and provide more meaningful error messages.
This client implementation is not thread-aware, hence does not employ any synchronization primitives. If used in a multi-threaded environment, synchronization needs to be handled by the user.
The control frames ping, pong and close are returned to the client in the same order as they are received. The user is responsible for sending the corresponding pong or close frame in response. By returning those frames to the user, the library enables the user to decide when and what to write as response, and does not hide any networking control flow, which would require synchronization.
All network operations have a timeout parameter to limit the time spent on a single operation, thus avoiding blocking the application indefinitely.
This includes the following functions in WebSocketClient
and WebSocketClientAsync
:
handshake
wait_message
read_message
send_message
send_pong_frame
close
The implementation does not allocate separate memory for each message and/or frames.
The user can supply the read buffer for messages as first argument to the read_message
function.
On a write operation, the message payload is directly written to the socket, without copying it into a separate buffer.
Additionally, if enabled, the permessage-deflate
compression extension maintains a compression and decompression buffer internally, which are used for all messages and frames.
The respective buffer size limits can be configured in the PermessageDeflate
struct:
struct PermessageDeflate
{
[...]
size_t decompress_buffer_size{100 * 1024}; // 100 KiB
size_t compress_buffer_size{100 * 1024}; // 100 KiB
};
The initial size, and the maximum buffer size are set at the creation of a Buffer
instance:
// create buffer with initial size of 4 KiB, and maximum size of 1 MiB
Buffer::create(4096, 1024 * 1024);
The initial size is allocated directly.
Operations that need more buffer memory up to the maximum size lead to on-demand allocations by extending the buffer memory dynamically.
If any buffer limit would be exceeded by an operation, a buffer_error
error will be returned.
Received Message
objects must be processed immediately after receiving them, otherwise, the next message will overwrite the payload since all message objects share the same buffer.
Message
objects must not be stored for later processing.
If deferred processing is required, the payload must be copied away to a user-defined buffer.
Pull requests or issues are welcome, see CONTRIBUTE.md.
The library passes all tests of the Autobahn Testsuite, see Autobahn Testsuite report.
Distributed under the MIT license, see LICENSE.