A header-only C++20 networking library providing cross-platform TCP and UDP multicast communication.
- Cross-platform: Windows and Unix/Linux support
- Header-only: No separate compilation required
- Modern C++: C++20 design with CRTP for most components
- Asynchronous: Non-blocking socket operations with timeout handling
- TCP Communication: Client and server implementations
- UDP Multicast: One-to-many communication support
- Logging: Template-based logger interface with console output
- Windows: Requires wepoll for epoll-like functionality
- Automatically fetched via CMake FetchContent if not installed
- Or install via vcpkg:
vcpkg install wepoll
- Unix/Linux/macOS: No external dependencies
If you're using vcpkg, install wepoll first:
vcpkg install wepollThen use CMake with the vcpkg toolchain:
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=[path-to-vcpkg]/scripts/buildsystems/vcpkg.cmakeThe easiest way to use slick-socket is to fetch it directly in your CMakeLists.txt:
include(FetchContent)
# Disable slick-socket examples and tests
set(BUILD_SLICK_SOCKET_EXAMPLES OFF CACHE BOOL "" FORCE)
set(BUILD_SLICK_SOCKET_TESTING OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
slick-socket
GIT_REPOSITORY https://github.com/SlickQuant/slick-socket.git
GIT_TAG v1.0.6 # Use the desired version
)
FetchContent_MakeAvailable(slick-socket)
# Link against slick-socket (automatically links ws2_32 and wepoll on Windows)
target_link_libraries(your_target PRIVATE slick::socket)Note: On Windows, if wepoll is not found, CMake will automatically fetch and build it from GitHub.
If you have slick-socket installed, you can use it with find_package:
find_package(slick-socket REQUIRED)
target_link_libraries(your_target PRIVATE slick::socket)- C++20 compatible compiler (GCC 11+, Clang 12+, MSVC 2022+)
- CMake 3.25 or higher
- Windows only: wepoll (automatically fetched if not found)
-
Configure the build:
cmake -S . -B build -
Build the library:
cmake --build build --config Release
-
Copy to your project:
cp -r build/dist/include/slick /path/to/your/project/include/
The library is header-only on Unix/Linux/macOS platforms, so only headers are needed.
-
(Optional) Install wepoll via vcpkg:
vcpkg install wepoll
-
Configure the build:
# With vcpkg cmake -S . -B build -G "Visual Studio 17 2022" -DCMAKE_TOOLCHAIN_FILE=[path-to-vcpkg]/scripts/buildsystems/vcpkg.cmake # Without vcpkg (wepoll will be fetched automatically) cmake -S . -B build -G "Visual Studio 17 2022"
-
Build the library:
cmake --build build --config Release
-
Install (optional):
cmake --install build --prefix /path/to/install
Then in your project:
find_package(slick-socket REQUIRED) target_link_libraries(your_target PRIVATE slick::socket)
Include the headers you need in your project:
#include <slick/socket/tcp_server.h>
#include <slick/socket/tcp_client.h>
#include <slick/socket/multicast_sender.h>
#include <slick/socket/multicast_receiver.h>#include <slick/socket/tcp_server.h>
class MyServer : public slick::socket::TCPServerBase<MyServer>
{
public:
MyServer() : TCPServerBase("MyServer", {/*.port = 5000*/}) {}
void onClientConnected(int client_id, const std::string& address)
{
std::cout << "Client " << client_id << " connected from " << address << std::endl;
}
void onClientData(int client_id, const uint8_t* data, size_t size)
{
// Echo back to client
send_data(client_id, std::vector<uint8_t>(data, data + size));
}
void onClientDisconnected(int client_id)
{
std::cout << "Client " << client_id << " disconnected" << std::endl;
}
};
int main()
{
MyServer server;
server.start();
// ... server runs in background thread
server.stop();
return 0;
}Server configuration notes:
- Set
TCPServerConfig::port = 0to let the OS pick a free port;port()returns the bound port afterstart(). TCPServerConfig::max_connectionscaps concurrent clients. Connections beyond the limit are accepted and closed immediately (the client sees a disconnect). A value<= 0means unlimited.stop()may be called from a server callback: no further callbacks are dispatched, and the server thread closes its sockets once the callback returns. A laterstart()restarts the server.TCPServerConfig::idle_timeoutdisconnects a client after that long with no traffic in either direction (nothing received and no send progress), and reports it throughonClientDisconnected(). A receive-only client stays connected as long as the server keeps sending to it and it keeps reading. Idle clients are closed within 1/8 of the timeout after it expires.0(the default) disables it.onClientDisconnected()fires exactly once for every client that leaves, whether the peer closed, an I/O error occurred, or the server calleddisconnect_client(). A disconnect triggered from a callback, including a failedsend_data(), is reported after that callback returns, never from inside it. Clients still connected when the server stops are closed without this callback.get_connected_client_count()is safe to call from any thread.send_data()never blocks the server thread. Data a slow client cannot take right away is queued per client and flushed when its socket becomes writable. If queuing a message could exceedTCPServerConfig::max_pending_send_bytes(default 16 MiB,0= unlimited),send_data()returnsfalseand drops that whole message before writing any of it, so the stream stays intact. A single message larger than the limit is therefore always rejected. Callsend_data()/disconnect_client()on the server thread, i.e. from a server callback.- With
cpu_affinityset, the server thread busy-polls for the lowest latency; otherwise it blocks in the event loop.
#include <slick/socket/tcp_client.h>
class MyClient : public slick::socket::TCPClientBase<MyClient>
{
public:
MyClient(const slick::socket::TCPClientConfig& config)
: TCPClientBase("MyClient", config) {}
void onConnected()
{
std::cout << "Connected to server" << std::endl;
}
void onDisconnected()
{
std::cout << "Disconnected from server" << std::endl;
}
void onData(const uint8_t* data, size_t length)
{
std::string received_data((const char*)data, length);
std::cout << "Received: " << received_data << std::endl;
}
};
int main()
{
slick::socket::TCPClientConfig config;
config.server_address = "127.0.0.1"; // IPv4 address or hostname (default: "localhost")
config.server_port = 5000;
MyClient client(config);
client.connect();
if (client.is_connected())
{
client.send_data("Hello Server!");
// ... process responses
client.disconnect();
}
return 0;
}If the server closes the connection, onDisconnected() is called and is_connected() becomes false. Calling connect() again reconnects the same client object.
The client thread blocks in poll() while idle; setting TCPClientConfig::cpu_affinity pins it to a core and switches to busy-polling recv() for the lowest latency. send_data() blocks the calling thread (without spinning) until the server has accepted all of the data.
Lifetime: servers, clients and multicast receivers must be stopped (
stop()/disconnect(), called from outside their worker thread) before the derived object is destroyed, e.g. in the derived destructor. This also applies after the server closed a client's connection or afterstop()was called from a callback: the outside call joins the finished worker thread. The base destructor runs after the derived members are gone, so a callback still running at that point would touch destroyed state. Destroying a running object is reported: an error is logged andSLICK_SOCKET_ON_UNSAFE_DESTROY()is invoked, which asserts in debug builds. Define that macro before including any slick-socket header to handle it differently (e.g. count or abort). It runs inside a destructor, so it must not throw: an escaping exception callsstd::terminate(). As a last-resort safety net, a client destroyed while connected skipsonDisconnected().
#include <slick/socket/multicast_sender.h>
int main()
{
slick::socket::MulticastSenderConfig config;
config.multicast_address = "224.0.0.100";
config.port = 12345;
config.ttl = 1; // Local network only
slick::socket::MulticastSender sender("MySender", config);
if (!sender.start())
{
std::cerr << "Failed to start sender" << std::endl;
return -1;
}
// Send data to multicast group
sender.send_data("Hello Multicast World!");
// Check statistics
std::cout << "Packets sent: " << sender.get_packets_sent() << std::endl;
sender.stop();
return 0;
}#include <slick/socket/multicast_receiver.h>
class MyReceiver : public slick::socket::MulticastReceiverBase<MyReceiver>
{
public:
MyReceiver(const slick::socket::MulticastReceiverConfig& config)
: MulticastReceiverBase("MyReceiver", config) {}
void handle_multicast_data(const std::vector<uint8_t>& data, const std::string& sender_address)
{
std::string message(data.begin(), data.end());
std::cout << "Received from " << sender_address << ": " << message << std::endl;
}
};
int main()
{
slick::socket::MulticastReceiverConfig config;
config.multicast_address = "224.0.0.100";
config.port = 12345;
config.reuse_address = true; // Allow multiple receivers
MyReceiver receiver(config);
if (!receiver.start())
{
std::cerr << "Failed to start receiver" << std::endl;
return -1;
}
// ... receiver runs in background thread
std::this_thread::sleep_for(std::chrono::seconds(30));
receiver.stop();
return 0;
}stop() may be called from handle_multicast_data(); the receiver thread leaves the group and closes its socket once the callback returns.
For more examples, see the examples/ directory.
Run the complete test suite:
cd build && ctest --output-on-failure -C DebugRun specific tests:
cd build && ctest -R TCPServerTest -C DebugEnable verbose test output:
cd build && ctest -V -C DebugEnable AddressSanitizer for debugging memory issues:
cmake -S . -B build -DENABLE_ASAN=ON
cmake --build build --config Debugcmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Releaseslick-socket/
├── include/slick/socket/ # Public headers
│ ├── tcp_server.h # TCP server base class
│ ├── tcp_client.h # TCP client base class
│ ├── multicast_sender.h # UDP multicast sender
│ ├── multicast_receiver.h # UDP multicast receiver
│ ├── worker_thread.h # Worker-thread identity helper (internal)
│ └── logger.h # Logger interface
├── src/ # Implementation files (Windows-specific)
├── examples/ # Usage examples
├── tests/ # Unit and integration tests
└── CMakeLists.txt
The library uses a three-file pattern for cross-platform support:
component.h- Base class with platform-independent interfacecomponent_win32.h- Windows implementationcomponent_unix.h- Unix/Linux implementation
Most components use CRTP (Curiously Recurring Template Pattern) for compile-time polymorphism without virtual function overhead. MulticastSender is implemented as a regular class without CRTP for simpler usage.
This project is licensed under the MIT License - see the LICENSE file for details.
Made with ⚡ by SlickQuant