From c47bf518e0a44960b7f555ebc831c4db5898bde0 Mon Sep 17 00:00:00 2001 From: Lars Ivar Hatledal Date: Sun, 29 Sep 2024 00:33:12 +0200 Subject: [PATCH] Add WebSocket client (#11) --- include/simple_socket/WebSocket.hpp | 20 ++ src/CMakeLists.txt | 3 + src/simple_socket/ws/WebSocket.cpp | 164 ++-------------- src/simple_socket/ws/WebSocketClient.cpp | 94 +++++++++ src/simple_socket/ws/WebSocketConnection.hpp | 180 ++++++++++++++++++ .../ws/WebSocketHandshakeKeyGen.hpp | 175 +++++++++++++++++ tests/CMakeLists.txt | 4 + tests/integration/run_ws.cpp | 3 + tests/test_ws.cpp | 70 +++++++ 9 files changed, 563 insertions(+), 150 deletions(-) create mode 100644 src/simple_socket/ws/WebSocketClient.cpp create mode 100644 src/simple_socket/ws/WebSocketConnection.hpp create mode 100644 src/simple_socket/ws/WebSocketHandshakeKeyGen.hpp create mode 100644 tests/test_ws.cpp diff --git a/include/simple_socket/WebSocket.hpp b/include/simple_socket/WebSocket.hpp index f989f3f..5a1a099 100644 --- a/include/simple_socket/WebSocket.hpp +++ b/include/simple_socket/WebSocket.hpp @@ -43,6 +43,26 @@ namespace simple_socket { std::unique_ptr pimpl_; }; + class WebSocketClient { + + public: + std::function onOpen; + std::function onClose; + std::function onMessage; + + WebSocketClient(); + + void connect(const std::string& host, uint16_t port); + + void close(); + + ~WebSocketClient(); + + private: + struct Impl; + std::unique_ptr pimpl_; + }; + }// namespace simple_socket #endif//SIMPLE_SOCKET_WEBSOCKET_HPP diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7fe53c9..816e6f4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,7 +22,9 @@ set(privateHeaders "simple_socket/Socket.hpp" "simple_socket/util/uuid.hpp" + "simple_socket/ws/WebSocketConnection.hpp" "simple_socket/ws/WebSocketHandshake.hpp" + "simple_socket/ws/WebSocketHandshakeKeyGen.hpp" ) set(sources @@ -32,6 +34,7 @@ set(sources "simple_socket/UnixDomainSocket.cpp" "simple_socket/ws/WebSocket.cpp" + "simple_socket/ws/WebSocketClient.cpp" "simple_socket/modbus/HoldingRegister.cpp" "simple_socket/modbus/ModbusClient.cpp" diff --git a/src/simple_socket/ws/WebSocket.cpp b/src/simple_socket/ws/WebSocket.cpp index 3135f2e..a499af5 100644 --- a/src/simple_socket/ws/WebSocket.cpp +++ b/src/simple_socket/ws/WebSocket.cpp @@ -1,9 +1,12 @@ #include "simple_socket/WebSocket.hpp" + #include "simple_socket/TCPSocket.hpp" #include "simple_socket/socket_common.hpp" #include "simple_socket/util/uuid.hpp" + +#include "simple_socket/ws/WebSocketConnection.hpp" #include "simple_socket/ws/WebSocketHandshake.hpp" #include @@ -11,50 +14,16 @@ #include #include #include +#include #include using namespace simple_socket; namespace { - std::vector createFrame(const std::string& message) { - std::vector frame; - frame.push_back(0x81);// FIN, text frame - - if (message.size() <= 125) { - frame.push_back(static_cast(message.size())); - } else if (message.size() <= 65535) { - frame.push_back(126); - frame.push_back((message.size() >> 8) & 0xFF); - frame.push_back(message.size() & 0xFF); - } else { - frame.push_back(127); - for (int i = 7; i >= 0; --i) { - frame.push_back((message.size() >> (i * 8)) & 0xFF); - } - } - - frame.insert(frame.end(), message.begin(), message.end()); - return frame; - } -}// namespace - -class WebSocketConnectionImpl: public WebSocketConnection { - -public: - WebSocketConnectionImpl(WebSocket* socket, std::unique_ptr conn) - : socket(socket), conn(std::move(conn)) { - - handshake(); - - thread = std::thread([this] { - listen(); - }); - } - - void handshake() const { + void handshake(SimpleConnection& conn) { std::vector buffer(1024); - const auto bytesReceived = conn->read(buffer); + const auto bytesReceived = conn.read(buffer); if (bytesReceived == -1) { throwSocketError("Failed to read handshake request from client."); } @@ -78,116 +47,11 @@ class WebSocketConnectionImpl: public WebSocketConnection { << "Sec-WebSocket-Accept: " << secWebSocketAccept << "\r\n\r\n"; const std::string responseStr = response.str(); - if (!conn->write(responseStr)) { + if (!conn.write(responseStr)) { throwSocketError("Failed to send handshake response"); } } - - void send(const std::string& message) override { - const auto frame = createFrame(message); - conn->write(frame); - } - - void listen() { - std::vector buffer(1024); - while (!closed) { - - const auto recv = conn->read(buffer); - if (recv == -1) { - break; - } - - std::vector frame{buffer.begin(), buffer.begin() + recv}; - - if (frame.size() < 2) return; - - uint8_t opcode = frame[0] & 0x0F; - // bool isFinal = (frame[0] & 0x80) != 0; - bool isMasked = (frame[1] & 0x80) != 0; - uint64_t payloadLen = frame[1] & 0x7F; - - size_t pos = 2; - if (payloadLen == 126) { - payloadLen = (frame[2] << 8) | frame[3]; - pos += 2; - } else if (payloadLen == 127) { - payloadLen = 0; - for (int i = 0; i < 8; ++i) { - payloadLen = (payloadLen << 8) | frame[2 + i]; - } - pos += 8; - } - - std::vector mask(4); - if (isMasked) { - for (int i = 0; i < 4; ++i) { - mask[i] = frame[pos++]; - } - } - - std::vector payload(frame.begin() + pos, frame.begin() + pos + payloadLen); - if (isMasked) { - for (size_t i = 0; i < payload.size(); ++i) { - payload[i] ^= mask[i % 4]; - } - } - - switch (opcode) { - case 0x1:// Text frame - { - std::string message(payload.begin(), payload.end()); - socket->onMessage(this, message); - } break; - case 0x8:// Close frame - close(false); - - break; - case 0x9:// Ping frame - std::cout << "Received ping frame" << std::endl; - { - std::vector pongFrame = {0x8A};// FIN, Pong frame - pongFrame.push_back(static_cast(payload.size())); - pongFrame.insert(pongFrame.end(), payload.begin(), payload.end()); - conn->write(pongFrame); - } - break; - case 0xA:// Pong frame - std::cout << "Received pong frame" << std::endl; - break; - default: - std::cerr << "Unsupported opcode: " << static_cast(opcode) << std::endl; - break; - } - } - } - - void close(bool self) { - if (!closed) { - closed = true; - - socket->onClose(this); - - if (!self) { - std::vector closeFrame = {0x88};// FIN, Close frame - closeFrame.push_back(0); - conn->write(closeFrame); - } - conn->close(); - } - } - - ~WebSocketConnectionImpl() override { - close(true); - if (thread.joinable()) { - thread.join(); - } - } - - std::atomic_bool closed{false}; - WebSocket* socket; - std::unique_ptr conn; - std::thread thread; -}; +}// namespace struct WebSocket::Impl { @@ -201,17 +65,17 @@ struct WebSocket::Impl { while (!stop_) { try { - auto ws = std::make_unique(scope, socket.accept()); - scope->onOpen(ws.get()); + auto ws = std::make_unique(WebSocketCallbaks{scope->onOpen, scope->onClose, scope->onMessage}, socket.accept()); + ws->run(handshake); connections.emplace_back(std::move(ws)); - } catch (std::exception& ex) { + } catch (std::exception&) { // std::cerr << ex.what() << std::endl; } //cleanup connections for (auto it = connections.begin(); it != connections.end();) { - if ((*it)->closed) { + if ((*it)->closed()) { it = connections.erase(it); } else { ++it; @@ -222,8 +86,8 @@ struct WebSocket::Impl { void start() { thread = std::thread([this] { - run(); - }); + run(); + }); } void stop() { diff --git a/src/simple_socket/ws/WebSocketClient.cpp b/src/simple_socket/ws/WebSocketClient.cpp new file mode 100644 index 0000000..361194b --- /dev/null +++ b/src/simple_socket/ws/WebSocketClient.cpp @@ -0,0 +1,94 @@ + +#include "simple_socket/WebSocket.hpp" + +#include "simple_socket/TCPSocket.hpp" +#include "simple_socket/socket_common.hpp" + +#include "simple_socket/ws/WebSocketConnection.hpp" +#include "simple_socket/ws/WebSocketHandshakeKeyGen.hpp" + +#include + +using namespace simple_socket; + +namespace { + void performHandshake(SimpleConnection& conn, const std::string& host, uint16_t port) { + const std::string key; + char output[28] = {};// 28 chars for base64-encoded output + WebSocketHandshakeKeyGen::generate(key, output); + + std::ostringstream request; + request << "GET " + << "/ws" + << " HTTP/1.1\r\n" + << "Host: " << host << ":" << port << "\r\n" + << "Upgrade: websocket\r\n" + << "Connection: Upgrade\r\n" + << "Sec-WebSocket-Key: " << key << "\r\n" + << "Sec-WebSocket-Version: 13\r\n\r\n"; + + const std::string requestStr = request.str(); + if (!conn.write(requestStr)) { + throwSocketError("Failed to send handshake request"); + } + + std::vector buffer(1024); + const auto bytesReceived = conn.read(buffer); + if (bytesReceived == -1) { + throwSocketError("Failed to read handshake response from server."); + } + + const std::string response(buffer.begin(), buffer.begin() + bytesReceived); + if (response.find(" 101 ") == std::string::npos) { + throwSocketError("Handshake failed with the server."); + } + } +}// namespace + +struct WebSocketClient::Impl { + + std::unique_ptr conn; + + explicit Impl(WebSocketClient* scope) + : scope_(scope) {} + + void connect(const std::string& host, uint16_t port) { + + auto c = ctx_.connect(host, port); + + conn = std::make_unique(WebSocketCallbaks{scope_->onOpen, scope_->onClose, scope_->onMessage}, std::move(c)); + conn->run([host, port](SimpleConnection& conn) { + performHandshake(conn, host, port); + }); + } + + void send(const std::string& message) { + conn->send(message); + } + + void close() { + conn->close(true); + } + + ~Impl() { + close(); + } + +private: + TCPClientContext ctx_; + WebSocketClient* scope_; +}; + +WebSocketClient::WebSocketClient() + : pimpl_(std::make_unique(this)) {} + + +void WebSocketClient::connect(const std::string& host, uint16_t port) { + pimpl_->connect(host, port); +} + +void WebSocketClient::close() { + pimpl_->close(); +} + +WebSocketClient::~WebSocketClient() = default; diff --git a/src/simple_socket/ws/WebSocketConnection.hpp b/src/simple_socket/ws/WebSocketConnection.hpp new file mode 100644 index 0000000..a5a9b38 --- /dev/null +++ b/src/simple_socket/ws/WebSocketConnection.hpp @@ -0,0 +1,180 @@ + +#ifndef SIMPLE_SOCKET_WEBSOCKETFRAME_HPP +#define SIMPLE_SOCKET_WEBSOCKETFRAME_HPP + +#include +#include +#include +#include +#include +#include + +#include "simple_socket/WebSocket.hpp" + +namespace simple_socket { + + struct WebSocketCallbaks { + std::function& onOpen; + std::function& onClose; + std::function& onMessage; + + WebSocketCallbaks(std::function& onOpen, + std::function& onClose, + std::function& onMessage) + : onOpen(onOpen), + onClose(onClose), + onMessage(onMessage) {} + }; + + struct WebSocketConnectionImpl: WebSocketConnection { + + explicit WebSocketConnectionImpl(const WebSocketCallbaks& callbacks, std::unique_ptr conn) + : callbacks_(callbacks), conn_(std::move(conn)) {} + + void run(const std::function& handshake) { + + handshake(*conn_); + if (callbacks_.onOpen) callbacks_.onOpen(this); + + thread_ = std::thread([this] { + listen(); + }); + } + + void send(const std::string& message) override { + const auto frame = createFrame(message); + conn_->write(frame); + } + + void close(bool self) { + if (!closed_) { + closed_ = true; + + if (callbacks_.onClose) callbacks_.onClose(this); + + if (self) { + std::vector closeFrame = {0x88};// FIN, Close frame + closeFrame.push_back(0); + conn_->write(closeFrame); + } + conn_->close(); + } + } + + bool closed() const { + return closed_; + } + + ~WebSocketConnectionImpl() override { + close(true); + if (thread_.joinable()) { + thread_.join(); + } + } + + private: + std::atomic_bool closed_{false}; + WebSocket* socket_{}; + std::unique_ptr conn_; + WebSocketCallbaks callbacks_; + std::thread thread_; + + + void listen() { + std::vector buffer(1024); + while (!closed_) { + + const auto recv = conn_->read(buffer); + if (recv == -1) { + break; + } + + std::vector frame{buffer.begin(), buffer.begin() + recv}; + + if (frame.size() < 2) return; + + uint8_t opcode = frame[0] & 0x0F; + // bool isFinal = (frame[0] & 0x80) != 0; + bool isMasked = (frame[1] & 0x80) != 0; + uint64_t payloadLen = frame[1] & 0x7F; + + size_t pos = 2; + if (payloadLen == 126) { + payloadLen = (frame[2] << 8) | frame[3]; + pos += 2; + } else if (payloadLen == 127) { + payloadLen = 0; + for (int i = 0; i < 8; ++i) { + payloadLen = (payloadLen << 8) | frame[2 + i]; + } + pos += 8; + } + + std::vector mask(4); + if (isMasked) { + for (int i = 0; i < 4; ++i) { + mask[i] = frame[pos++]; + } + } + + std::vector payload(frame.begin() + pos, frame.begin() + pos + payloadLen); + if (isMasked) { + for (size_t i = 0; i < payload.size(); ++i) { + payload[i] ^= mask[i % 4]; + } + } + + switch (opcode) { + case 0x1:// Text frame + { + std::string message(payload.begin(), payload.end()); + if (callbacks_.onMessage) callbacks_.onMessage(this, message); + } break; + case 0x8:// Close frame + close(false); + + break; + case 0x9:// Ping frame + std::cout << "Received ping frame" << std::endl; + { + std::vector pongFrame = {0x8A};// FIN, Pong frame + pongFrame.push_back(static_cast(payload.size())); + pongFrame.insert(pongFrame.end(), payload.begin(), payload.end()); + conn_->write(pongFrame); + } + break; + case 0xA:// Pong frame + std::cout << "Received pong frame" << std::endl; + break; + default: + std::cerr << "Unsupported opcode: " << static_cast(opcode) << std::endl; + break; + } + } + } + + static std::vector createFrame(const std::string& message) { + std::vector frame; + frame.push_back(0x81);// FIN, text frame + + if (message.size() <= 125) { + frame.push_back(static_cast(message.size())); + } else if (message.size() <= 65535) { + frame.push_back(126); + frame.push_back((message.size() >> 8) & 0xFF); + frame.push_back(message.size() & 0xFF); + } else { + frame.push_back(127); + for (int i = 7; i >= 0; --i) { + frame.push_back((message.size() >> (i * 8)) & 0xFF); + } + } + + frame.insert(frame.end(), message.begin(), message.end()); + return frame; + } + }; +};// namespace simple_socket + +#endif//SIMPLE_SOCKET_WEBSOCKETFRAME_HPP diff --git a/src/simple_socket/ws/WebSocketHandshakeKeyGen.hpp b/src/simple_socket/ws/WebSocketHandshakeKeyGen.hpp new file mode 100644 index 0000000..c8784fe --- /dev/null +++ b/src/simple_socket/ws/WebSocketHandshakeKeyGen.hpp @@ -0,0 +1,175 @@ + +// Copyright (c) 2016 Alex Hultman and contributors + +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. + +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: + +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgement in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. + +#ifndef WEBSOCKETHANDSHAKEKEYGEN_HPP +#define WEBSOCKETHANDSHAKEKEYGEN_HPP + +#include +#include +#include +#include + +class WebSocketHandshakeKeyGen +{ + template + struct static_for + { + void operator()(uint32_t* a, uint32_t* b) + { + static_for()(a, b); + T::template f(a, b); + } + }; + + template + struct static_for<0, T> + { + void operator()(uint32_t* /*a*/, uint32_t* /*hash*/) + { + } + }; + + template + struct Sha1Loop + { + static inline uint32_t rol(uint32_t value, size_t bits) + { + return (value << bits) | (value >> (32 - bits)); + } + static inline uint32_t blk(uint32_t b[16], size_t i) + { + return rol(b[(i + 13) & 15] ^ b[(i + 8) & 15] ^ b[(i + 2) & 15] ^ b[i], 1); + } + + template + static inline void f(uint32_t* a, uint32_t* b) + { + switch (state) + { + case 1: + a[i % 5] += + ((a[(3 + i) % 5] & (a[(2 + i) % 5] ^ a[(1 + i) % 5])) ^ a[(1 + i) % 5]) + + b[i] + 0x5a827999 + rol(a[(4 + i) % 5], 5); + a[(3 + i) % 5] = rol(a[(3 + i) % 5], 30); + break; + case 2: + b[i] = blk(b, i); + a[(1 + i) % 5] += + ((a[(4 + i) % 5] & (a[(3 + i) % 5] ^ a[(2 + i) % 5])) ^ a[(2 + i) % 5]) + + b[i] + 0x5a827999 + rol(a[(5 + i) % 5], 5); + a[(4 + i) % 5] = rol(a[(4 + i) % 5], 30); + break; + case 3: + b[(i + 4) % 16] = blk(b, (i + 4) % 16); + a[i % 5] += (a[(3 + i) % 5] ^ a[(2 + i) % 5] ^ a[(1 + i) % 5]) + + b[(i + 4) % 16] + 0x6ed9eba1 + rol(a[(4 + i) % 5], 5); + a[(3 + i) % 5] = rol(a[(3 + i) % 5], 30); + break; + case 4: + b[(i + 8) % 16] = blk(b, (i + 8) % 16); + a[i % 5] += (((a[(3 + i) % 5] | a[(2 + i) % 5]) & a[(1 + i) % 5]) | + (a[(3 + i) % 5] & a[(2 + i) % 5])) + + b[(i + 8) % 16] + 0x8f1bbcdc + rol(a[(4 + i) % 5], 5); + a[(3 + i) % 5] = rol(a[(3 + i) % 5], 30); + break; + case 5: + b[(i + 12) % 16] = blk(b, (i + 12) % 16); + a[i % 5] += (a[(3 + i) % 5] ^ a[(2 + i) % 5] ^ a[(1 + i) % 5]) + + b[(i + 12) % 16] + 0xca62c1d6 + rol(a[(4 + i) % 5], 5); + a[(3 + i) % 5] = rol(a[(3 + i) % 5], 30); + break; + case 6: b[i] += a[4 - i]; + } + } + }; + + static inline void sha1(uint32_t hash[5], uint32_t b[16]) + { + uint32_t a[5] = {hash[4], hash[3], hash[2], hash[1], hash[0]}; + static_for<16, Sha1Loop<1>>()(a, b); + static_for<4, Sha1Loop<2>>()(a, b); + static_for<20, Sha1Loop<3>>()(a, b); + static_for<20, Sha1Loop<4>>()(a, b); + static_for<20, Sha1Loop<5>>()(a, b); + static_for<5, Sha1Loop<6>>()(a, hash); + } + + static inline void base64(unsigned char* src, char* dst) + { + const char* b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for (int i = 0; i < 18; i += 3) + { + *dst++ = b64[(src[i] >> 2) & 63]; + *dst++ = b64[((src[i] & 3) << 4) | ((src[i + 1] & 240) >> 4)]; + *dst++ = b64[((src[i + 1] & 15) << 2) | ((src[i + 2] & 192) >> 6)]; + *dst++ = b64[src[i + 2] & 63]; + } + *dst++ = b64[(src[18] >> 2) & 63]; + *dst++ = b64[((src[18] & 3) << 4) | ((src[19] & 240) >> 4)]; + *dst++ = b64[((src[19] & 15) << 2)]; + *dst++ = '='; + } + +public: + static inline void generate(const std::string& inputStr, char output[28]) + { + char input[25] = {}; + strncpy(input, inputStr.c_str(), 25 - 1); + input[25 - 1] = '\0'; + + uint32_t b_output[5] = {0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0}; + uint32_t b_input[16] = {0, + 0, + 0, + 0, + 0, + 0, + 0x32353845, + 0x41464135, + 0x2d453931, + 0x342d3437, + 0x44412d39, + 0x3543412d, + 0x43354142, + 0x30444338, + 0x35423131, + 0x80000000}; + + for (int i = 0; i < 6; i++) + { + b_input[i] = (input[4 * i + 3] & 0xff) | (input[4 * i + 2] & 0xff) << 8 | + (input[4 * i + 1] & 0xff) << 16 | (input[4 * i + 0] & 0xff) << 24; + } + sha1(b_output, b_input); + uint32_t last_b[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 480}; + sha1(b_output, last_b); + for (int i = 0; i < 5; i++) + { + uint32_t tmp = b_output[i]; + char* bytes = (char*) &b_output[i]; + bytes[3] = tmp & 0xff; + bytes[2] = (tmp >> 8) & 0xff; + bytes[1] = (tmp >> 16) & 0xff; + bytes[0] = (tmp >> 24) & 0xff; + } + base64((unsigned char*) b_output, output); + } +}; + +#endif //WEBSOCKETHANDSHAKEKEYGEN_HPP diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 75107e8..ffe9dee 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -11,6 +11,10 @@ add_executable(test_un test_un.cpp) add_test(NAME test_un COMMAND test_un) target_link_libraries(test_un PRIVATE simple_socket Catch2::Catch2WithMain) +add_executable(test_ws test_ws.cpp) +add_test(NAME test_ws COMMAND test_ws) +target_link_libraries(test_ws PRIVATE simple_socket Catch2::Catch2WithMain) + add_executable(test_conversion test_conversion.cpp) add_test(NAME test_conversion COMMAND test_conversion) target_link_libraries(test_conversion PRIVATE simple_socket Catch2::Catch2WithMain) diff --git a/tests/integration/run_ws.cpp b/tests/integration/run_ws.cpp index fefc9d0..c5378a5 100644 --- a/tests/integration/run_ws.cpp +++ b/tests/integration/run_ws.cpp @@ -11,6 +11,7 @@ int main() { ws.start(); ws.onOpen = [](auto c) { + c->send("Hello from server!"); std::cout << "[" << c->uuid() << "] onOpen" << std::endl; }; ws.onClose = [](auto c) { @@ -21,7 +22,9 @@ int main() { c->send(msg); }; +#ifdef _WIN32 system("start ws_client.html"); +#endif //wait for key press std::cout << "Press any key to stop server.." << std::endl; diff --git a/tests/test_ws.cpp b/tests/test_ws.cpp new file mode 100644 index 0000000..a67f8ae --- /dev/null +++ b/tests/test_ws.cpp @@ -0,0 +1,70 @@ + +#include "simple_socket/WebSocket.hpp" +#include "simple_socket/util/port_query.hpp" + +#include +#include + +#include + +using namespace simple_socket; + +TEST_CASE("test Websocket") { + + const auto port = getAvailablePort(8000, 9000); + REQUIRE(port); + + std::atomic_bool serverOpen{false}; + std::atomic_bool serverClose{false}; + std::string servermsg; + + std::atomic_bool clientOpen{false}; + std::atomic_bool clientClose{false}; + std::string clientmsg; + + WebSocket ws(*port); + ws.start(); + + + ws.onOpen = [&](auto c) { + serverOpen = true; + c->send("Hello from server!"); + }; + + ws.onMessage = [&](auto c, const auto& msg) { + servermsg = msg; + }; + + ws.onClose = [&](auto c) { + serverClose = true; + }; + + + WebSocketClient client; + client.onOpen = [&](auto c) { + clientOpen = true; + c->send("Hello from client!"); + }; + + client.onMessage = [&](auto c, const auto& msg) { + clientmsg = msg; + }; + + client.onClose = [&](auto c) { + clientClose = true; + }; + + client.connect("127.0.0.1", *port); + std::this_thread::sleep_for(std::chrono::seconds(2)); + + client.close(); + ws.stop(); + + CHECK(serverOpen.load()); + CHECK(serverClose.load()); + CHECK(clientOpen.load()); + CHECK(clientClose.load()); + + CHECK(servermsg == "Hello from client!"); + CHECK(clientmsg == "Hello from server!"); +}