This commit is contained in:
2026-09-30 18:16:13 +03:00
parent 47c6d39569
commit af04681b3c
6 changed files with 223 additions and 47 deletions

View File

@@ -5,6 +5,12 @@ option(SNPLIB_BUILD_EXAMPLES "Build examples" ON)
option(SNPLIB_USE_SPDLOG "Use of spdlog library" ON) option(SNPLIB_USE_SPDLOG "Use of spdlog library" ON)
option(
SNPLIB_USE_ASIO
"add network related implementation using ASIO library"
ON
)
if(SNPLIB_USE_SPDLOG) if(SNPLIB_USE_SPDLOG)
include(FetchContent) include(FetchContent)
# find_package(spdlog CONFIG) # find_package(spdlog CONFIG)
@@ -45,8 +51,47 @@ set(LIB_HEADERS
include/snipplib/containers/snplib_hvec.h include/snipplib/containers/snplib_hvec.h
include/snipplib/serialization/snplib_serialization.h include/snipplib/serialization/snplib_serialization.h
include/snipplib/network/snplib_endpoint.h include/snipplib/network/snplib_endpoint.h
include/snipplib/network/snplib_network.h
) )
if(SNPLIB_USE_ASIO)
find_package(PkgConfig REQUIRED)
pkg_check_modules(ASIOLIB IMPORTED_TARGET GLOBAL asio)
if(NOT ASIOLIB_FOUND)
message(STATUS "\tfetch asio-lib ...")
FetchContent_Declare(
asiolib_project
PREFIX
${CMAKE_BINARY_DIR}/asio
GIT_REPOSITORY "https://github.com/chriskohlhoff/asio.git"
GIT_TAG "asio-1-38-0"
GIT_SHALLOW 1
)
FetchContent_MakeAvailable(asiolib_project)
execute_process(
WORKING_DIRECTORY ${asiolib_project_SOURCE_DIR}/asio
COMMAND ./autogen.sh
)
execute_process(
WORKING_DIRECTORY ${asiolib_project_SOURCE_DIR}/asio
COMMAND ./configure --prefix=${asiolib_project_SOURCE_DIR}/asio
)
set(ENV{PKG_CONFIG_PATH} "${asiolib_project_SOURCE_DIR}/asio")
pkg_check_modules(ASIOLIB IMPORTED_TARGET GLOBAL asio)
message(STATUS "ASIO INC PATHS: ${ASIOLIB_INCLUDE_DIRS}")
endif()
list(APPEND LIB_HEADERS include/snipplib/network/snplib_network_asio.h)
endif(SNPLIB_USE_ASIO)
add_library(${PROJECT_NAME} INTERFACE ${LIB_HEADERS}) add_library(${PROJECT_NAME} INTERFACE ${LIB_HEADERS})
target_compile_features(${PROJECT_NAME} INTERFACE cxx_std_23) target_compile_features(${PROJECT_NAME} INTERFACE cxx_std_23)
target_include_directories( target_include_directories(
@@ -55,6 +100,9 @@ target_include_directories(
"$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/include>" "$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/include>"
"$<INSTALL_INTERFACE:include/${PROJECT_NAME}>" "$<INSTALL_INTERFACE:include/${PROJECT_NAME}>"
) )
# add dependencies
if(SNPLIB_USE_SPDLOG) if(SNPLIB_USE_SPDLOG)
target_compile_definitions( target_compile_definitions(
${PROJECT_NAME} ${PROJECT_NAME}
@@ -64,7 +112,11 @@ if(SNPLIB_USE_SPDLOG)
SNPLIB_SPDLOGGER=1 SNPLIB_SPDLOGGER=1
) )
target_link_libraries(${PROJECT_NAME} INTERFACE spdlog::spdlog_header_only) target_link_libraries(${PROJECT_NAME} INTERFACE spdlog::spdlog_header_only)
endif() endif(SNPLIB_USE_SPDLOG)
if(SNPLIB_USE_ASIO)
target_link_libraries(${PROJECT_NAME} INTERFACE PkgConfig::ASIOLIB)
endif(SNPLIB_USE_ASIO)
if(SNPLIB_BUILD_EXAMPLES) if(SNPLIB_BUILD_EXAMPLES)
set(EXAM_STRING str_exam) set(EXAM_STRING str_exam)

View File

@@ -17,7 +17,7 @@ int main()
std::println("BYTES VEC: {}", bytes_vec); std::println("BYTES VEC: {}", bytes_vec);
auto msg = proto.fromBytes(bytes_vec); auto msg = proto.fromTransport(bytes_vec);
if (msg) { if (msg) {
std::println("MSG (from std::vector): {}", msg.value()); std::println("MSG (from std::vector): {}", msg.value());
@@ -25,7 +25,7 @@ int main()
std::println("MSG ERR: {}", msg.error().message()); std::println("MSG ERR: {}", msg.error().message());
} }
auto msg_l = proto.fromBytes(bytes_lst); auto msg_l = proto.fromTransport(bytes_lst);
if (msg_l) { if (msg_l) {
std::println("MSG (from std::list): {}", msg_l.value()); std::println("MSG (from std::list): {}", msg_l.value());
@@ -33,7 +33,7 @@ int main()
std::println("MSG ERR: {}", msg_l.error().message()); std::println("MSG ERR: {}", msg_l.error().message());
} }
auto vmsg = proto.fromBytes<std::vector<char>>(bytes_lst); auto vmsg = proto.fromTransport<std::vector<char>>(bytes_lst);
if (vmsg) { if (vmsg) {
std::println("VMSG (from std::list): {}", vmsg.value()); std::println("VMSG (from std::list): {}", vmsg.value());
} else { } else {
@@ -49,13 +49,13 @@ int main()
std::println("MSG: {}", bytes_vec); std::println("MSG: {}", bytes_vec);
auto bs = proto.toBytes(bytes_vec); auto bs = proto.toTransport(bytes_vec);
std::println("BYTES (from std::vector): {}", bs); std::println("BYTES (from std::vector): {}", bs);
auto bs1 = proto.toBytes(bytes_lst); auto bs1 = proto.toTransport(bytes_lst);
std::println("BYTES (from std::list): {}", bs1); std::println("BYTES (from std::list): {}", bs1);
bytes_vec = proto.toBytes<decltype(bytes_vec)>(bytes_lst); bytes_vec = proto.toTransport<decltype(bytes_vec)>(bytes_lst);
std::println("BYTES: {}", bytes_vec); std::println("BYTES: {}", bytes_vec);
@@ -71,24 +71,24 @@ int main()
std::println("BLOB: {}", bytes); std::println("BLOB: {}", bytes);
bs = bb_proto.toBytes(bytes_vec); bs = bb_proto.toTransport(bytes_vec);
std::println("BYTES (from std::vector): {}", bs); std::println("BYTES (from std::vector): {}", bs);
auto bs2 = bb_proto.toBytes(bytes_lst); auto bs2 = bb_proto.toTransport(bytes_lst);
std::println("BYTES (from std::list): {}", bs2); std::println("BYTES (from std::list): {}", bs2);
auto blob = std::vector(std::from_range, bs | std::views::join); auto blob = std::vector(std::from_range, bs | std::views::join);
std::println("\nBYTES: {}", blob); std::println("\nBYTES: {}", blob);
vmsg = bb_proto.fromBytes<std::vector<char>>(blob); vmsg = bb_proto.fromTransport<std::vector<char>>(blob);
if (vmsg) { if (vmsg) {
std::println("BLOB (from std::vector): {}", vmsg.value()); std::println("BLOB (from std::vector): {}", vmsg.value());
} else { } else {
std::println("VMSG ERR: {}", vmsg.error().message()); std::println("VMSG ERR: {}", vmsg.error().message());
} }
// std::array<char, 30> arr = bb_proto.toBytes<decltype(arr)>(bytes_lst); // std::array<char, 30> arr = bb_proto.toTransport<decltype(arr)>(bytes_lst);
return 0; return 0;
} }

View File

@@ -6,6 +6,9 @@
#include <expected> #include <expected>
#include <ranges> #include <ranges>
#include <asio/awaitable.hpp>
namespace snplib namespace snplib
{ {
@@ -194,12 +197,22 @@ concept ret_expected_type_c = requires {
// result of asynchronous callable // result of asynchronous callable
template <typename, typename = std::void_t<>>
struct has_co_await_operator : std::false_type {};
template <typename T>
struct has_co_await_operator<T, std::void_t<decltype(std::declval<T>().operator co_await())>> : std::true_type {};
template <typename T>
constexpr bool has_co_await_operator_v = has_co_await_operator<T>::value;
template <typename T, typename UT = void> template <typename T, typename UT = void>
concept result_awaiter_c = requires(T t, std::coroutine_handle<> hndl) { concept result_awaiter_c = requires(T t, std::coroutine_handle<> hndl) {
{ t.await_ready() } -> std::convertible_to<bool>; { t.await_ready() } -> std::convertible_to<bool>;
// await_suspend can return void, bool, or a coroutine_handle // await_suspend can return void, bool, or a coroutine_handle
t.await_suspend(hndl); // t.await_suspend(hndl);
// await_resume must return the specific type UT // await_resume must return the specific type UT
{ t.await_resume() } -> std::same_as<UT>; { t.await_resume() } -> std::same_as<UT>;
@@ -207,13 +220,37 @@ concept result_awaiter_c = requires(T t, std::coroutine_handle<> hndl) {
template <typename T, typename UT = void> template <typename T, typename UT = void>
concept result_awaitable_c = result_awaiter_c<T, UT> || requires(T t) { concept result_awaitable_c = requires(T t) {
{ t.operator co_await() } -> result_awaiter_c<UT>; // { t.operator co_await() } -> result_awaiter_c<UT>;
t.operator co_await();
}; };
// template <typename T, typename UT = void>
// concept result_awaitable_c = result_awaiter_c<T, UT> || requires(T t) {
// { t.operator co_await() } -> result_awaiter_c<UT>;
// };
template <typename T, typename RT = void>
concept awaitable_type_c =
(!has_co_await_operator_v<T> ||
std::same_as<decltype(std::declval<decltype(std::declval<T>().operator co_await())>().await_resume()), RT>) &&
(has_co_await_operator_v<T> || std::same_as<decltype(std::declval<T>().await_resume()), RT>);
static_assert(awaitable_type_c<asio::awaitable<void>>, "!!!");
template <typename T, typename UT = void> template <typename T, typename UT = void>
concept async_retval_c = result_awaitable_c<T, UT> && ret_expected_c<UT>; concept async_retval_c = result_awaitable_c<decltype(std::declval<T>().operator co_await())> || requires {
[] -> auto {
T t;
co_await t;
}();
};
// template <typename T, typename UT = void>
// concept async_retval_c = result_awaiter_c<decltype(std::declval<T>().operator co_await()), UT>;
// template <typename T, typename UT = void>
// concept async_retval_c = result_awaitable_c<T, UT> && ret_expected_c<UT>;
}; // namespace snplib

View File

@@ -33,7 +33,7 @@ struct netproto_stopseq_t {
// returns 2-element range of the input messsage and stop-sequence // returns 2-element range of the input messsage and stop-sequence
template <std::ranges::range OR, input_char_range_c IR> template <std::ranges::range OR, input_char_range_c IR>
requires char_range_c<std::ranges::range_value_t<OR>> // range of char ranges requires char_range_c<std::ranges::range_value_t<OR>> // range of char ranges
OR toBytes(IR&& message) OR toTransport(IR&& message)
{ {
OR res; OR res;
@@ -45,7 +45,7 @@ struct netproto_stopseq_t {
// returns a copy of the input message + tralling stop-sequence // returns a copy of the input message + tralling stop-sequence
template <output_char_range_c OR, input_char_range_c IR> template <output_char_range_c OR, input_char_range_c IR>
OR toBytes(IR&& message) OR toTransport(IR&& message)
{ {
OR res; OR res;
@@ -56,7 +56,7 @@ struct netproto_stopseq_t {
} }
template <input_char_range_c IR> template <input_char_range_c IR>
auto toBytes(IR&& message) auto toTransport(IR&& message)
{ {
if constexpr (std::ranges::contiguous_range<IR>) { if constexpr (std::ranges::contiguous_range<IR>) {
return std::array{std::span<const char>{message}, std::span<const char>{NETPROTO_STOP_SEQ}}; return std::array{std::span<const char>{message}, std::span<const char>{NETPROTO_STOP_SEQ}};
@@ -67,7 +67,7 @@ struct netproto_stopseq_t {
} }
template <std::ranges::range OR, input_char_range_c IR> template <std::ranges::range OR, input_char_range_c IR>
std::expected<OR, std::error_code> fromBytes(IR&& bytes) std::expected<OR, std::error_code> fromTransport(IR&& bytes)
{ {
auto found = std::ranges::search(std::forward<IR>(bytes), NETPROTO_STOP_SEQ); auto found = std::ranges::search(std::forward<IR>(bytes), NETPROTO_STOP_SEQ);
if (found.empty()) { if (found.empty()) {
@@ -122,22 +122,22 @@ struct netproto_stopseq_t {
// deduces a view-type from the input range-type // deduces a view-type from the input range-type
template <typename IR> template <typename IR>
auto fromBytes(IR&& bytes) auto fromTransport(IR&& bytes)
requires input_char_range_c<std::remove_cvref_t<IR>> requires input_char_range_c<std::remove_cvref_t<IR>>
{ {
using or_t = using or_t =
std::conditional_t<std::ranges::contiguous_range<IR>, std::span<const char>, std::conditional_t<std::ranges::contiguous_range<IR>, std::span<const char>,
std::ranges::subrange<std::ranges::iterator_t<IR>, std::ranges::sentinel_t<IR>>>; std::ranges::subrange<std::ranges::iterator_t<IR>, std::ranges::sentinel_t<IR>>>;
return fromBytes<or_t>(std::forward<IR>(bytes)); return fromTransport<or_t>(std::forward<IR>(bytes));
} }
// to satisfy 'net_proto_c' concept // to satisfy 'net_proto_c' concept
template <typename CharT = char> template <typename CharT = char>
requires(std::same_as<CharT, char> || std::same_as<CharT, const char>) requires(std::same_as<CharT, char> || std::same_as<CharT, const char>)
std::expected<std::span<CharT>, std::error_code> fromBytes(std::span<CharT> bytes) std::expected<std::span<CharT>, std::error_code> fromTransport(std::span<CharT> bytes)
{ {
return fromBytes<std::span<CharT>>(bytes); return fromTransport<std::span<CharT>>(bytes);
} }
}; };
@@ -148,11 +148,11 @@ static_assert(net_proto_c<netproto_stopseq_t<>>, "!!!");
/* BINARY BLOB PROTOCOL */ /* BINARY BLOB PROTOCOL */
/* /*
* a sequence of bytes in format: a sequence of bytes in format:
* <blob size field><binary blob bytes> <blob size field><binary blob bytes>
*
blob size field: a symbolic representation of a HEXIMAL UNSIGNED INTEGER (e.g. "000000FA", MUST BE IN blob size field: a symbolic representation of a HEXIMAL UNSIGNED INTEGER
UPPER-CASE!!!) (e.g. "000000FA", MUST BE IN UPPER-CASE!!!)
*/ */
template <size_t SZ_FIELD_WIDTH = 8> template <size_t SZ_FIELD_WIDTH = 8>
@@ -164,7 +164,7 @@ struct netproto_binary_blob_t {
static constexpr std::array<char, 6> BLOB_SIZE_FIELD_FORMAT{'{', ':', '0', '0' + BLOB_SIZE_FIELD, 'X', '}'}; static constexpr std::array<char, 6> BLOB_SIZE_FIELD_FORMAT{'{', ':', '0', '0' + BLOB_SIZE_FIELD, 'X', '}'};
template <input_char_range_c IR> template <input_char_range_c IR>
auto toBytes(IR&& message) auto toTransport(IR&& message)
{ {
static thread_local std::string Nbytes{}; static thread_local std::string Nbytes{};
@@ -182,7 +182,7 @@ struct netproto_binary_blob_t {
template <char_range_c OR, input_char_range_c IR> template <char_range_c OR, input_char_range_c IR>
std::expected<OR, std::error_code> fromBytes(IR&& bytes) std::expected<OR, std::error_code> fromTransport(IR&& bytes)
{ {
using inp_t = std::remove_cvref_t<IR>; using inp_t = std::remove_cvref_t<IR>;
@@ -255,6 +255,32 @@ struct netproto_binary_blob_t {
static_assert(false, "Unsupported output byte sequence type!"); static_assert(false, "Unsupported output byte sequence type!");
} }
} }
// deduces a view-type from the input range-type
template <typename IR>
auto fromTransport(IR&& bytes)
requires input_char_range_c<std::remove_cvref_t<IR>>
{
using or_t =
std::conditional_t<std::ranges::contiguous_range<IR>, std::span<const char>,
std::ranges::subrange<std::ranges::iterator_t<IR>, std::ranges::sentinel_t<IR>>>;
return fromTransport<or_t>(std::forward<IR>(bytes));
}
// to satisfy 'net_proto_c' concept
template <typename CharT = char>
requires(std::same_as<CharT, char> || std::same_as<CharT, const char>)
std::expected<std::span<CharT>, std::error_code> fromTransport(std::span<CharT> bytes)
{
return fromTransport<std::span<CharT>>(bytes);
}
}; };
static_assert(net_proto_c<netproto_binary_blob_t<>>, "!!!");
} // namespace snplib } // namespace snplib

View File

@@ -12,7 +12,7 @@ namespace snplib
template <typename T> template <typename T>
concept net_endpoint_c = requires(T t, const T t_const) { concept net_endpoint_c = requires(T t, const T t_const) {
// static constexpr member // static constexpr member
std::formattable<decltype(T::id), char>; requires std::formattable<decltype(T::id), char>;
[]() { [[maybe_unused]] static constexpr auto val = T::id; }(); []() { [[maybe_unused]] static constexpr auto val = T::id; }();
{ t.endpoint() } -> char_range_c; { t.endpoint() } -> char_range_c;
@@ -27,21 +27,19 @@ concept net_endpoint_c = requires(T t, const T t_const) {
template <typename T, typename CharRangeT = std::vector<char>> template <typename T, typename CharRangeT = std::vector<char>>
concept net_proto_c = requires(T t) { concept net_proto_c = requires(T t) {
// from session-level representation to transport-level one // from session-level representation to transport-level one
{ t.toBytes(std::declval<CharRangeT>()) } -> char_range_or_range_of_char_ranges_c; { t.toTransport(std::declval<CharRangeT>()) } -> char_range_or_range_of_char_ranges_c;
// from transport-level representation to session-level one // from transport-level representation to session-level one
requires( requires(
requires { requires {
// { t.fromBytes(std::declval<std::span<const char>>()) } -> char_range_c; { t.fromTransport(std::declval<std::span<const char>>()) } -> ret_expected_c;
{ t.fromBytes(std::declval<std::span<const char>>()) } -> ret_expected_c;
requires char_range_c<std::remove_cvref_t< requires char_range_c<std::remove_cvref_t<
decltype(std::declval<T>().fromBytes(std::declval<std::span<const char>>()).value())>>; decltype(std::declval<T>().fromTransport(std::declval<std::span<const char>>()).value())>>;
} || } ||
requires { requires {
// { t.fromBytes(std::declval<std::span<char>>()) } -> char_range_c; { t.fromTransport(std::declval<std::span<char>>()) } -> ret_expected_c;
{ t.fromBytes(std::declval<std::span<char>>()) } -> ret_expected_c; requires char_range_c<std::remove_cvref_t<
requires char_range_c< decltype(std::declval<T>().fromTransport(std::declval<std::span<char>>()).value())>>;
std::remove_cvref_t<decltype(std::declval<T>().fromBytes(std::declval<std::span<char>>()).value())>>;
}); });
}; };
@@ -66,13 +64,21 @@ concept net_message_c = requires(const T t_const) {
template <typename T> template <typename T>
concept net_connection_c = requires(T t) { concept net_connection_c = requires(T t) {
// static constexpr member // static constexpr member
std::formattable<decltype(T::id), char>; requires std::formattable<decltype(T::id), char>;
[]() { [[maybe_unused]] static constexpr auto val = T::id; }(); []() { [[maybe_unused]] static constexpr auto val = T::id; }();
typename T::message_t; // minimal allowed unit of read/write operations typename T::message_t; // minimal allowed unit of read/write operations
typename T::error_t;
{ t.send(std::declval<typename T::message_t>()) } -> async_retval_c; // { t.send(std::declval<typename T::message_t>()) } -> async_retval_c<typename T::error_t>;
{ t.recv() } -> async_retval_c<typename T::message_t>; { t.send(std::declval<typename T::message_t>()) } -> awaitable_type_c<typename T::error_t>;
// { t.recv() } -> async_retval_c<typename T::message_t>;
{ t.recv() } -> awaitable_type_c<typename T::message_t>;
// [] {
// typename T::message_t msg;
// co_await t.send(msg);
// }();
t.close(); t.close();
}; };
@@ -85,7 +91,7 @@ concept net_session_c =
typename T::ident_t; typename T::ident_t;
typename T::context_t; typename T::context_t;
net_connection_c<typename T::connection_t>; requires net_connection_c<typename T::connection_t>;
{ t_const.ident() } -> std::same_as<typename T::ident_t>; { t_const.ident() } -> std::same_as<typename T::ident_t>;
@@ -104,10 +110,10 @@ concept net_session_c =
template <typename T> template <typename T>
concept net_listener_c = requires(T t, const T t_const) { concept net_listener_c = requires(T t, const T t_const) {
// static constexpr member // static constexpr member
std::formattable<decltype(T::id), char>; requires std::formattable<decltype(T::id), char>;
[]() { [[maybe_unused]] static constexpr auto val = T::id; }(); []() { [[maybe_unused]] static constexpr auto val = T::id; }();
net_connection_c<typename T::connection_t>; requires net_connection_c<typename T::connection_t>;
{ t.accept() } -> async_retval_c<typename T::connection_t>; { t.accept() } -> async_retval_c<typename T::connection_t>;
@@ -144,7 +150,7 @@ concept net_initiator_c =
// to be sure 'ROLE' is compile-time constant // to be sure 'ROLE' is compile-time constant
[]() { [[maybe_unused]] static constexpr auto val = T::ROLE; }(); []() { [[maybe_unused]] static constexpr auto val = T::ROLE; }();
net_connection_c<typename T::connection_t>; requires net_connection_c<typename T::connection_t>;
{ t_const.endpoint() } -> net_endpoint_c; { t_const.endpoint() } -> net_endpoint_c;
} && ((T::ROLE != decltype(T::ROLE)::ROLE_SERVER) || requires(T t) { // use of short-circuiting logic!!! } && ((T::ROLE != decltype(T::ROLE)::ROLE_SERVER) || requires(T t) { // use of short-circuiting logic!!!
@@ -165,7 +171,7 @@ concept net_initiator_c =
template <typename T> template <typename T>
concept netserver_c = requires(T t) { concept netserver_c = requires(T t) {
// static constexpr member // static constexpr member
std::formattable<decltype(T::id), char>; requires std::formattable<decltype(T::id), char>;
[]() { [[maybe_unused]] static constexpr auto val = T::id; }(); []() { [[maybe_unused]] static constexpr auto val = T::id; }();

View File

@@ -0,0 +1,55 @@
#pragma once
#include <asio/buffer.hpp>
#include <asio/experimental/coro.hpp>
#include <asio/ip/tcp.hpp>
#include <asio/use_awaitable.hpp>
#include <asio/write.hpp>
#include "snplib_netmsg.h"
#include "snplib_netproto.h"
#include "snplib_network.h"
namespace snplib::asio
{
template <net_message_c NETMSG_T, net_proto_c NETPROTO_T>
class asio_net_connection_t
{
public:
typedef NETMSG_T message_t;
typedef NETPROTO_T netproto_t;
typedef std::error_code error_t;
typedef std::expected<message_t, error_t> result_t;
static constexpr std::string_view id{"ASIO-NETCONN"};
// ::asio::awaitable<void> send(asio_net_connection_t::message_t msg)
// {
// char data[1024];
// size_t n = 1024;
// auto executor = co_await ::asio::this_coro::executor;
// ::asio::ip::tcp::acceptor acceptor(executor, {::asio::ip::tcp::v4(), 55555});
// ::asio::ip::tcp::socket socket = co_await acceptor.async_accept(::asio::use_awaitable);
// co_await ::asio::async_write(socket, ::asio::buffer(data, n), ::asio::use_awaitable);
// }
// ::asio::awaitable<void> recv() {}
// ::asio::awaitable<void> close() {}
// ::asio::experimental::coro<error_t> send(asio_net_connection_t::message_t msg) {}
::asio::awaitable<error_t> send(asio_net_connection_t::message_t msg) {}
::asio::awaitable<message_t> recv() {}
::asio::awaitable<error_t> close() {}
};
static_assert(net_connection_c<asio_net_connection_t<keyval_netmsg_t<>, netproto_stopseq_t<>>>, "!!!");
} // namespace snplib::asio