Skip to content

Commit 2e50f91

Browse files
committed
Release 1.2.1: Update CHANGELOG, CMake configuration, WebSocket implementation, and add comprehensive tests for immediate message sending after connection; Add vcpkg integration
1 parent 84b1746 commit 2e50f91

File tree

5 files changed

+313
-7
lines changed

5 files changed

+313
-7
lines changed

CHANGELOG.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,38 @@
1+
# [1.2.1] - 2025-01-25
2+
3+
## New Features
4+
- **vcpkg Package Manager Support**: Full integration with vcpkg for easy installation and dependency management
5+
- Created CMake config files for proper package discovery
6+
- Ready for submission to official vcpkg registry
7+
8+
## Improvements
9+
- **WebSocket Message Queueing**: Enhanced `send()` method to properly queue messages sent immediately after `open()`
10+
- Messages are now queued during `CONNECTING` state and sent after connection is established
11+
- Ensures messages sent right after `open()` are not lost and are delivered in order
12+
- Updated status check to allow queueing during both `DISCONNECTED` and `CONNECTING` states
13+
14+
## Testing
15+
- Added comprehensive WebSocket unit tests for send-after-open scenarios:
16+
- `SendImmediatelyAfterOpen_MessageQueuedAndSentAfterConnect` - Single message test
17+
- `SendImmediatelyAfterOpen_MultipleMessages` - Multiple messages queuing test
18+
- `SendImmediatelyAfterOpen_VerifyOrderPreserved` - Message ordering verification
19+
- `SendImmediatelyAfterOpen_LargeMessage` - Large message (5KB) queueing test
20+
21+
## Build System
22+
- Added CMake installation rules for proper package export
23+
- Created `slick_net-config.cmake.in` template for downstream projects
24+
- Added version compatibility checking (SameMajorVersion policy)
25+
- Improved CMake target exports with proper namespace (`slick::slick_net`)
26+
27+
## CI/CD
28+
- Updated GitHub Actions CI to use GCC 14 on Linux for full C++20 coroutine support
29+
- Removed GCC 13 compatibility workarounds for awaitable HTTP tests
30+
31+
## Documentation
32+
- Added `VCPKG.md` with complete vcpkg integration guide
33+
- Created `ports/slick-net/README.md` for port maintainers
34+
- Added usage instructions in `ports/slick-net/usage`
35+
136
# [1.2.0] - 2025-01-18
237

338
## New Features

CMakeLists.txt

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ if (MSVC)
88
endif()
99

1010
project(slick_net
11-
VERSION 1.2.0
11+
VERSION 1.2.1
1212
LANGUAGES CXX
1313
)
1414

@@ -132,6 +132,38 @@ endif()
132132
# Installation rules
133133
install(DIRECTORY include/ DESTINATION include)
134134

135+
# Install targets and create CMake config files
136+
install(TARGETS slick_net
137+
EXPORT slick_net-targets
138+
INCLUDES DESTINATION include
139+
)
140+
141+
install(EXPORT slick_net-targets
142+
FILE slick_net-targets.cmake
143+
NAMESPACE slick::
144+
DESTINATION share/slick_net
145+
)
146+
147+
# Create config file
148+
include(CMakePackageConfigHelpers)
149+
configure_package_config_file(
150+
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/slick_net-config.cmake.in"
151+
"${CMAKE_CURRENT_BINARY_DIR}/slick_net-config.cmake"
152+
INSTALL_DESTINATION share/slick_net
153+
)
154+
155+
write_basic_package_version_file(
156+
"${CMAKE_CURRENT_BINARY_DIR}/slick_net-config-version.cmake"
157+
VERSION ${PROJECT_VERSION}
158+
COMPATIBILITY SameMajorVersion
159+
)
160+
161+
install(FILES
162+
"${CMAKE_CURRENT_BINARY_DIR}/slick_net-config.cmake"
163+
"${CMAKE_CURRENT_BINARY_DIR}/slick_net-config-version.cmake"
164+
DESTINATION share/slick_net
165+
)
166+
135167
# Automatically run install after build in Release mode
136168
if(CMAKE_BUILD_TYPE STREQUAL "Release")
137169
add_custom_target(dist_slick_net ALL

cmake/slick_net-config.cmake.in

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
@PACKAGE_INIT@
2+
3+
include(CMakeFindDependencyMacro)
4+
5+
# Find required dependencies
6+
find_dependency(Boost REQUIRED COMPONENTS beast context)
7+
find_dependency(OpenSSL REQUIRED)
8+
9+
# slick_queue is fetched automatically, but vcpkg users might have it installed
10+
find_package(slick_queue QUIET)
11+
if(NOT slick_queue_FOUND)
12+
include(FetchContent)
13+
set(BUILD_SLICK_QUEUE_TESTS OFF CACHE BOOL "" FORCE)
14+
FetchContent_Declare(
15+
slick_queue
16+
GIT_REPOSITORY https://github.com/SlickQuant/slick_queue.git
17+
GIT_TAG v1.1.2
18+
)
19+
FetchContent_MakeAvailable(slick_queue)
20+
endif()
21+
22+
# Include the targets file
23+
include("${CMAKE_CURRENT_LIST_DIR}/slick_net-targets.cmake")
24+
25+
check_required_components(slick_net)

include/slick/net/websocket.h

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,11 @@ inline void Websocket::close()
312312

313313
inline void Websocket::send(const char* buffer, size_t len, bool is_binary)
314314
{
315-
auto l = len + 1; // +1 for is_bool flag
315+
if (status_.load(std::memory_order_relaxed) > Status::CONNECTED) {
316+
LOG_WARN("WebSocket not connected, cannot send data.");
317+
return;
318+
}
319+
auto l = static_cast<uint32_t>(len) + 1; // +1 for is_bool flag
316320
auto index = w_buffer_.reserve(l);
317321
*w_buffer_[index] = static_cast<char>(is_binary);
318322
memcpy(w_buffer_[index + 1], buffer, len);
@@ -500,14 +504,20 @@ inline asio::awaitable<void> Websocket::do_ws_session_plain() {
500504
}
501505

502506
inline void Websocket::do_write() {
507+
if (status_.load(std::memory_order_relaxed) != Status::CONNECTED) [[unlikely]] {
508+
if (status_.load(std::memory_order_relaxed) == Status::CONNECTING) {
509+
// Still connecting, repost do_write to try later
510+
auto executor = use_ssl_ ? wss_->get_executor() : ws_->get_executor();
511+
asio::post(executor, [self = shared_from_this()]() {
512+
self->do_write();
513+
});
514+
}
515+
// else: socket close is called
516+
return;
517+
}
503518
// Read is already within the executor strand, safe to access w_cursor_
504519
auto [msg, len] = w_buffer_.read(w_cursor_);
505520
if (msg && len) {
506-
if (status_.load(std::memory_order_relaxed) != Status::CONNECTED) [[unlikely]] {
507-
// socket close is called
508-
return;
509-
}
510-
511521
bool is_binary = msg[0];
512522
++msg;
513523
--len;

tests/websocket_tests.cpp

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,210 @@ TEST_F(WebsocketTest, PlainWebsocket_UrlParsing) {
881881
SUCCEED();
882882
}
883883

884+
// ======================== Send Immediately After Open Tests ========================
885+
886+
TEST_F(WebsocketTest, SendImmediatelyAfterOpen_MessageQueuedAndSentAfterConnect) {
887+
EventSynchronizer connected_sync;
888+
EventSynchronizer data_sync;
889+
std::string received_data;
890+
std::atomic<bool> message_sent{false};
891+
892+
auto ws = std::make_shared<Websocket>(
893+
"wss://ws.postman-echo.com/raw",
894+
[&]() {
895+
connected_sync.notify();
896+
},
897+
[&]() {},
898+
[&](const char* data, std::size_t len) {
899+
received_data.assign(data, len);
900+
data_sync.notify();
901+
},
902+
[&](std::string) {}
903+
);
904+
905+
// Initial state should be DISCONNECTED
906+
EXPECT_EQ(ws->status(), Websocket::Status::DISCONNECTED);
907+
908+
// Open the WebSocket connection
909+
ws->open();
910+
911+
// Immediately send data right after calling open (before connection is established)
912+
const char* test_message = "Immediate message after open";
913+
ws->send(test_message, strlen(test_message));
914+
message_sent.store(true);
915+
916+
// At this point, the connection should be CONNECTING or CONNECTED
917+
auto status_after_send = ws->status();
918+
EXPECT_TRUE(status_after_send == Websocket::Status::CONNECTING ||
919+
status_after_send == Websocket::Status::CONNECTED);
920+
921+
// Wait for connection to be established
922+
connected_sync.wait_for(std::chrono::milliseconds(10000));
923+
924+
EXPECT_TRUE(connected_sync.is_triggered());
925+
if (connected_sync.is_triggered()) {
926+
EXPECT_EQ(ws->status(), Websocket::Status::CONNECTED);
927+
EXPECT_TRUE(message_sent.load());
928+
929+
// Wait for the echo response
930+
data_sync.wait_for(std::chrono::milliseconds(5000));
931+
932+
if (data_sync.is_triggered()) {
933+
// Verify the message was sent and echoed back after connection was established
934+
EXPECT_EQ(received_data, "Immediate message after open");
935+
}
936+
}
937+
938+
// Always close the websocket
939+
ws->close();
940+
}
941+
942+
TEST_F(WebsocketTest, SendImmediatelyAfterOpen_MultipleMessages) {
943+
EventSynchronizer connected_sync;
944+
std::atomic<int> messages_received{0};
945+
std::vector<std::string> received_messages;
946+
std::mutex messages_mutex;
947+
948+
auto ws = std::make_shared<Websocket>(
949+
"wss://ws.postman-echo.com/raw",
950+
[&]() {
951+
connected_sync.notify();
952+
},
953+
[&]() {},
954+
[&](const char* data, std::size_t len) {
955+
std::lock_guard<std::mutex> lock(messages_mutex);
956+
received_messages.emplace_back(data, len);
957+
messages_received++;
958+
},
959+
[&](std::string) {}
960+
);
961+
962+
EXPECT_EQ(ws->status(), Websocket::Status::DISCONNECTED);
963+
964+
// Open connection
965+
ws->open();
966+
967+
// Immediately send multiple messages right after open
968+
const int num_messages = 3;
969+
for (int i = 0; i < num_messages; ++i) {
970+
std::string msg = "Quick message " + std::to_string(i);
971+
ws->send(msg.c_str(), msg.size());
972+
}
973+
974+
// Wait for connection to establish
975+
connected_sync.wait_for(std::chrono::milliseconds(10000));
976+
977+
EXPECT_TRUE(connected_sync.is_triggered());
978+
if (connected_sync.is_triggered()) {
979+
EXPECT_EQ(ws->status(), Websocket::Status::CONNECTED);
980+
981+
// Wait for all messages to be received
982+
wait_for_condition([&]() { return messages_received >= num_messages; },
983+
std::chrono::milliseconds(10000));
984+
985+
// All messages should have been queued and sent after connection established
986+
EXPECT_GE(messages_received.load(), num_messages);
987+
988+
std::lock_guard<std::mutex> lock(messages_mutex);
989+
EXPECT_GE(received_messages.size(), static_cast<size_t>(num_messages));
990+
}
991+
992+
ws->close();
993+
}
994+
995+
TEST_F(WebsocketTest, SendImmediatelyAfterOpen_VerifyOrderPreserved) {
996+
EventSynchronizer connected_sync;
997+
std::atomic<int> messages_received{0};
998+
std::vector<std::string> received_messages;
999+
std::mutex messages_mutex;
1000+
1001+
auto ws = std::make_shared<Websocket>(
1002+
"wss://ws.postman-echo.com/raw",
1003+
[&]() {
1004+
connected_sync.notify();
1005+
},
1006+
[&]() {},
1007+
[&](const char* data, std::size_t len) {
1008+
std::lock_guard<std::mutex> lock(messages_mutex);
1009+
received_messages.emplace_back(data, len);
1010+
messages_received++;
1011+
},
1012+
[&](std::string) {}
1013+
);
1014+
1015+
ws->open();
1016+
1017+
// Send messages immediately after open - they should be queued
1018+
ws->send("First", 5);
1019+
ws->send("Second", 6);
1020+
ws->send("Third", 5);
1021+
1022+
// Wait for connection
1023+
connected_sync.wait_for(std::chrono::milliseconds(10000));
1024+
1025+
EXPECT_TRUE(connected_sync.is_triggered());
1026+
if (connected_sync.is_triggered()) {
1027+
// Wait for all messages to arrive
1028+
wait_for_condition([&]() { return messages_received >= 3; },
1029+
std::chrono::milliseconds(10000));
1030+
1031+
std::lock_guard<std::mutex> lock(messages_mutex);
1032+
EXPECT_GE(received_messages.size(), 3u);
1033+
1034+
// Verify order is preserved (messages queued before connection should arrive in order)
1035+
if (received_messages.size() >= 3) {
1036+
EXPECT_EQ(received_messages[0], "First");
1037+
EXPECT_EQ(received_messages[1], "Second");
1038+
EXPECT_EQ(received_messages[2], "Third");
1039+
}
1040+
}
1041+
1042+
ws->close();
1043+
}
1044+
1045+
TEST_F(WebsocketTest, SendImmediatelyAfterOpen_LargeMessage) {
1046+
EventSynchronizer connected_sync;
1047+
EventSynchronizer data_sync;
1048+
std::string received_data;
1049+
1050+
auto ws = std::make_shared<Websocket>(
1051+
"wss://ws.postman-echo.com/raw",
1052+
[&]() {
1053+
connected_sync.notify();
1054+
},
1055+
[&]() {},
1056+
[&](const char* data, std::size_t len) {
1057+
received_data.assign(data, len);
1058+
data_sync.notify();
1059+
},
1060+
[&](std::string) {}
1061+
);
1062+
1063+
ws->open();
1064+
1065+
// Send a large message immediately after open
1066+
std::string large_message(5120, 'X'); // 5KB message
1067+
ws->send(large_message.c_str(), large_message.size());
1068+
1069+
// Wait for connection
1070+
connected_sync.wait_for(std::chrono::milliseconds(10000));
1071+
1072+
EXPECT_TRUE(connected_sync.is_triggered());
1073+
if (connected_sync.is_triggered()) {
1074+
EXPECT_EQ(ws->status(), Websocket::Status::CONNECTED);
1075+
1076+
// Wait for the large message echo
1077+
data_sync.wait_for(std::chrono::milliseconds(10000));
1078+
1079+
if (data_sync.is_triggered()) {
1080+
EXPECT_EQ(received_data.size(), large_message.size());
1081+
EXPECT_EQ(received_data, large_message);
1082+
}
1083+
}
1084+
1085+
ws->close();
1086+
}
1087+
8841088
// Note: Main tests use wss://ws.postman-echo.com which is a public test server.
8851089
// Tests may fail if the server is down or network is unavailable.
8861090
//

0 commit comments

Comments
 (0)