From 8e108655a06d9b82c1e6b9b381f788eff4c72e00 Mon Sep 17 00:00:00 2001 From: Dmitry Borchuk Date: Sun, 1 Feb 2026 22:52:00 +0330 Subject: [PATCH 01/10] [245] IpMreq wrapper structure implemented to be used for multicast. --- core/include/userver/engine/io/sockaddr.hpp | 53 +++++++++++++++++++++ core/src/engine/io/sockaddr.cpp | 30 ++++++++++++ 2 files changed, 83 insertions(+) diff --git a/core/include/userver/engine/io/sockaddr.hpp b/core/include/userver/engine/io/sockaddr.hpp index 01b14a50454e..d3a237a03bcb 100644 --- a/core/include/userver/engine/io/sockaddr.hpp +++ b/core/include/userver/engine/io/sockaddr.hpp @@ -3,6 +3,7 @@ /// @file userver/engine/io/sockaddr.hpp /// @brief @copybrief engine::io::Sockaddr +#include #include #include #include @@ -26,6 +27,12 @@ class AddrException : public std::runtime_error { using std::runtime_error::runtime_error; }; +/// Multicast request related exceptions +class IpMulticastRequestException : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + /// Communication domain enum class AddrDomain { kUnspecified = AF_UNSPEC, ///< Unspecified @@ -39,6 +46,52 @@ static_assert( "Your socket subsystem looks broken, please contact support chat." ); +/// Native ip multicast request wrapper +class IpMreq final { +public: + /// @brief Creates IPv4 multicast request. + /// @param imr_multiaddr IPv4 multicast group address (e.g., "239.255.0.1") + /// @param imr_interface IPv4 interface address (nullptr for INADDR_ANY) + IpMreq(const char* imr_multiaddr, const char* imr_interface = nullptr); + + /// @brief Creates IPv6 multicast request. + /// @param ipv6mr_multiaddr IPv6 multicast group address (e.g., "ff02::1") + /// @param ipv6mr_interface Interface index (0 for default) + IpMreq(const char* ipv6mr_multiaddr, unsigned int ipv6mr_interface = 0); + + /// @brief Native multicast request structure pointer. + void* Data() { return &data_; } + + /// @brief Native multicast request structure pointer. + const void* Data() const { return &data_; } + + /// @brief Returns socket option level. + int GetSocketOptionLevel() const noexcept { return (family_ == AF_INET ? IPPROTO_IP : IPPROTO_IPV6); } + + /// @brief Returns socket option name for joining multicast group. + int GetJoinSocketOptionName() const noexcept { return (family_ == AF_INET ? IP_ADD_MEMBERSHIP : IPV6_JOIN_GROUP); } + + /// @brief Returns socket option name for leaving multicast group. + int GetLeaveSocketOption() const noexcept { return (family_ == AF_INET ? IP_DROP_MEMBERSHIP : IPV6_LEAVE_GROUP); } + + /// Returns appropriate size for setsockopt based on address family. + /// @param domain Socket domain (AF_INET or AF_INET6) + size_t Size() const noexcept { return (family_ == AF_INET ? sizeof(struct ip_mreq) : sizeof(struct ipv6_mreq)); } + +private: + template + T* As() { + static_assert(sizeof(T) <= sizeof(data_), "Invalid ip multicast request type"); + return reinterpret_cast(&data_); + } + + union Storage { + struct ip_mreq ip_req; + struct ipv6_mreq ipv6_req; + } data_; + int family_; +}; + /// Native socket address wrapper class Sockaddr final { public: diff --git a/core/src/engine/io/sockaddr.cpp b/core/src/engine/io/sockaddr.cpp index d49696e63e44..526d1ea4ff03 100644 --- a/core/src/engine/io/sockaddr.cpp +++ b/core/src/engine/io/sockaddr.cpp @@ -18,6 +18,36 @@ USERVER_NAMESPACE_BEGIN namespace engine::io { +IpMreq::IpMreq(const char* imr_multiaddr, const char* imr_interface) : family_{AF_INET} { + memset(&data_, 0, sizeof(data_)); + struct ip_mreq* imr_ptr = As(); + if (::inet_pton(AF_INET, imr_multiaddr, &imr_ptr->imr_multiaddr) != 1) { + throw IpMulticastRequestException( + fmt::format("Invalid IPv4 multicast address: {}", imr_multiaddr) + ); + } + if (imr_interface != nullptr) { + if (::inet_pton(AF_INET, imr_interface, &imr_ptr->imr_interface) != 1) { + throw IpMulticastRequestException( + fmt::format("Invalid IPv4 interface address: {}", imr_interface) + ); + } + } else { + imr_ptr->imr_interface.s_addr = htonl(INADDR_ANY); + } +} + +IpMreq::IpMreq(const char* ipv6mr_multiaddr, unsigned int ipv6mr_interface) : family_{AF_INET6} { + memset(&data_, 0, sizeof(data_)); + struct ipv6_mreq* req = As(); + if (inet_pton(AF_INET6, ipv6mr_multiaddr, &req->ipv6mr_multiaddr) != 1) { + throw IpMulticastRequestException( + fmt::format("Invalid IPv6 address: {}", ipv6mr_multiaddr) + ); + } + req->ipv6mr_interface = ipv6mr_interface; +} + Sockaddr Sockaddr::MakeUnixSocketAddress(std::string_view path) { Sockaddr addr; auto* sa = addr.As(); From b1dc45023aa5dd8ea77c46b5a654bfe031b2b954 Mon Sep 17 00:00:00 2001 From: Dmitry Borchuk Date: Sun, 1 Feb 2026 22:59:42 +0330 Subject: [PATCH 02/10] [245] Generalized SetOption added to be used with non-trivial optval argument. --- core/include/userver/engine/io/socket.hpp | 3 +++ core/src/engine/io/socket.cpp | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/core/include/userver/engine/io/socket.hpp b/core/include/userver/engine/io/socket.hpp index 3bbfc9ddacb6..8c2e44b96508 100644 --- a/core/include/userver/engine/io/socket.hpp +++ b/core/include/userver/engine/io/socket.hpp @@ -154,6 +154,9 @@ class [[nodiscard]] Socket final : public RwBase { /// Sets a socket option. void SetOption(int layer, int optname, int optval); + /// Sets a socket option with non-trivial optval. + void SetOption(int layer, int optname, const void* optval, socklen_t optlen); + /// @brief Receives at least one byte from the socket. /// @returns 0 if connection is closed on one side and no data could be /// received any more, received bytes count otherwise. diff --git a/core/src/engine/io/socket.cpp b/core/src/engine/io/socket.cpp index e3915eafb1f1..e3885c8cc777 100644 --- a/core/src/engine/io/socket.cpp +++ b/core/src/engine/io/socket.cpp @@ -501,6 +501,17 @@ void Socket::SetOption(int layer, int optname, int optval) { ); } +void Socket::SetOption(int layer, int optname, const void* optval, socklen_t optlen) { + UASSERT(IsValid()); + utils::CheckSyscallCustomException( + ::setsockopt(Fd(), layer, optname, optval, optlen), + "setting socket option {},{} on fd {}", + layer, + optname, + Fd() + ); +} + } // namespace engine::io USERVER_NAMESPACE_END From beb6fbe99c3f05d8163ed5a9b657d247f04f341e Mon Sep 17 00:00:00 2001 From: Dmitry Borchuk Date: Sun, 1 Feb 2026 23:05:40 +0330 Subject: [PATCH 03/10] [245] Redundant include removed. --- core/include/userver/engine/io/sockaddr.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/include/userver/engine/io/sockaddr.hpp b/core/include/userver/engine/io/sockaddr.hpp index d3a237a03bcb..fccf1b950fc3 100644 --- a/core/include/userver/engine/io/sockaddr.hpp +++ b/core/include/userver/engine/io/sockaddr.hpp @@ -3,7 +3,6 @@ /// @file userver/engine/io/sockaddr.hpp /// @brief @copybrief engine::io::Sockaddr -#include #include #include #include From 424050857d84f5d80e255fd554d5cd7034ffb15f Mon Sep 17 00:00:00 2001 From: Dmitry Borchuk Date: Mon, 9 Feb 2026 17:52:51 +0300 Subject: [PATCH 04/10] [245] Inactive union member accessing fix. --- core/src/engine/io/sockaddr.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/engine/io/sockaddr.cpp b/core/src/engine/io/sockaddr.cpp index 526d1ea4ff03..b49f330d3e69 100644 --- a/core/src/engine/io/sockaddr.cpp +++ b/core/src/engine/io/sockaddr.cpp @@ -19,8 +19,8 @@ USERVER_NAMESPACE_BEGIN namespace engine::io { IpMreq::IpMreq(const char* imr_multiaddr, const char* imr_interface) : family_{AF_INET} { - memset(&data_, 0, sizeof(data_)); - struct ip_mreq* imr_ptr = As(); + data_.ip_req = {}; + struct ip_mreq* imr_ptr = &data_.ip_req; if (::inet_pton(AF_INET, imr_multiaddr, &imr_ptr->imr_multiaddr) != 1) { throw IpMulticastRequestException( fmt::format("Invalid IPv4 multicast address: {}", imr_multiaddr) @@ -38,8 +38,8 @@ IpMreq::IpMreq(const char* imr_multiaddr, const char* imr_interface) : family_{A } IpMreq::IpMreq(const char* ipv6mr_multiaddr, unsigned int ipv6mr_interface) : family_{AF_INET6} { - memset(&data_, 0, sizeof(data_)); - struct ipv6_mreq* req = As(); + data_.ipv6_req = {}; + struct ipv6_mreq* req = &data_.ipv6_req; if (inet_pton(AF_INET6, ipv6mr_multiaddr, &req->ipv6mr_multiaddr) != 1) { throw IpMulticastRequestException( fmt::format("Invalid IPv6 address: {}", ipv6mr_multiaddr) From f47055ccdea8b4e8c7434e6ae289cf330453c2f2 Mon Sep 17 00:00:00 2001 From: Dmitry Borchuk Date: Mon, 9 Feb 2026 17:53:52 +0300 Subject: [PATCH 05/10] [245] Remove unused As method. --- core/include/userver/engine/io/sockaddr.hpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/core/include/userver/engine/io/sockaddr.hpp b/core/include/userver/engine/io/sockaddr.hpp index fccf1b950fc3..767e632b7ba4 100644 --- a/core/include/userver/engine/io/sockaddr.hpp +++ b/core/include/userver/engine/io/sockaddr.hpp @@ -78,12 +78,6 @@ class IpMreq final { size_t Size() const noexcept { return (family_ == AF_INET ? sizeof(struct ip_mreq) : sizeof(struct ipv6_mreq)); } private: - template - T* As() { - static_assert(sizeof(T) <= sizeof(data_), "Invalid ip multicast request type"); - return reinterpret_cast(&data_); - } - union Storage { struct ip_mreq ip_req; struct ipv6_mreq ipv6_req; From 1c130460b7127e8f54c0b866fe3700ab743ebb34 Mon Sep 17 00:00:00 2001 From: Dmitry Borchuk Date: Mon, 16 Feb 2026 15:48:21 +0330 Subject: [PATCH 06/10] [245] Creating single ctor to handle both ipv4 and ipv6. --- core/include/userver/engine/io/sockaddr.hpp | 17 ++++------- core/src/engine/io/sockaddr.cpp | 34 ++++++++------------- 2 files changed, 18 insertions(+), 33 deletions(-) diff --git a/core/include/userver/engine/io/sockaddr.hpp b/core/include/userver/engine/io/sockaddr.hpp index 767e632b7ba4..c38c1824e47f 100644 --- a/core/include/userver/engine/io/sockaddr.hpp +++ b/core/include/userver/engine/io/sockaddr.hpp @@ -48,15 +48,10 @@ static_assert( /// Native ip multicast request wrapper class IpMreq final { public: - /// @brief Creates IPv4 multicast request. - /// @param imr_multiaddr IPv4 multicast group address (e.g., "239.255.0.1") - /// @param imr_interface IPv4 interface address (nullptr for INADDR_ANY) - IpMreq(const char* imr_multiaddr, const char* imr_interface = nullptr); - - /// @brief Creates IPv6 multicast request. - /// @param ipv6mr_multiaddr IPv6 multicast group address (e.g., "ff02::1") - /// @param ipv6mr_interface Interface index (0 for default) - IpMreq(const char* ipv6mr_multiaddr, unsigned int ipv6mr_interface = 0); + /// @brief Creates multicast request. IP version is chosen automatically from ip_multiaddr value. + /// @param ip_multiaddr IP multicast group address (e.g. 239.255.0.1" or "ff02::1") + /// @param interface_index Interface index (0 for default); + IpMreq(const char* ip_multiaddr, unsigned int interface_index); /// @brief Native multicast request structure pointer. void* Data() { return &data_; } @@ -75,11 +70,11 @@ class IpMreq final { /// Returns appropriate size for setsockopt based on address family. /// @param domain Socket domain (AF_INET or AF_INET6) - size_t Size() const noexcept { return (family_ == AF_INET ? sizeof(struct ip_mreq) : sizeof(struct ipv6_mreq)); } + size_t Size() const noexcept { return (family_ == AF_INET ? sizeof(struct ip_mreqn) : sizeof(struct ipv6_mreq)); } private: union Storage { - struct ip_mreq ip_req; + struct ip_mreqn ip_req; struct ipv6_mreq ipv6_req; } data_; int family_; diff --git a/core/src/engine/io/sockaddr.cpp b/core/src/engine/io/sockaddr.cpp index b49f330d3e69..cee4050d41d4 100644 --- a/core/src/engine/io/sockaddr.cpp +++ b/core/src/engine/io/sockaddr.cpp @@ -18,36 +18,26 @@ USERVER_NAMESPACE_BEGIN namespace engine::io { -IpMreq::IpMreq(const char* imr_multiaddr, const char* imr_interface) : family_{AF_INET} { +IpMreq::IpMreq(const char* ip_multiaddr, unsigned int interface_index) { data_.ip_req = {}; - struct ip_mreq* imr_ptr = &data_.ip_req; - if (::inet_pton(AF_INET, imr_multiaddr, &imr_ptr->imr_multiaddr) != 1) { - throw IpMulticastRequestException( - fmt::format("Invalid IPv4 multicast address: {}", imr_multiaddr) - ); + if (inet_pton(AF_INET, ip_multiaddr, &data_.ip_req.imr_multiaddr) == 1) { + // imr_address field is not set since it's not used if imr_ifindex presents + family_ = AF_INET; + data_.ip_req.imr_ifindex = interface_index; } - if (imr_interface != nullptr) { - if (::inet_pton(AF_INET, imr_interface, &imr_ptr->imr_interface) != 1) { + else { + data_.ipv6_req = {}; + if (inet_pton(AF_INET6, ip_multiaddr, &data_.ipv6_req.ipv6mr_multiaddr) == 1) { + family_ = AF_INET6; + data_.ipv6_req.ipv6mr_interface = interface_index; + } else { throw IpMulticastRequestException( - fmt::format("Invalid IPv4 interface address: {}", imr_interface) + fmt::format("Invalid IP address: {}", ip_multiaddr) ); } - } else { - imr_ptr->imr_interface.s_addr = htonl(INADDR_ANY); } } -IpMreq::IpMreq(const char* ipv6mr_multiaddr, unsigned int ipv6mr_interface) : family_{AF_INET6} { - data_.ipv6_req = {}; - struct ipv6_mreq* req = &data_.ipv6_req; - if (inet_pton(AF_INET6, ipv6mr_multiaddr, &req->ipv6mr_multiaddr) != 1) { - throw IpMulticastRequestException( - fmt::format("Invalid IPv6 address: {}", ipv6mr_multiaddr) - ); - } - req->ipv6mr_interface = ipv6mr_interface; -} - Sockaddr Sockaddr::MakeUnixSocketAddress(std::string_view path) { Sockaddr addr; auto* sa = addr.As(); From cb34bc2efd9375f2ac6aa1027da4b091667d75e5 Mon Sep 17 00:00:00 2001 From: Dmitry Borchuk Date: Sun, 22 Feb 2026 15:24:44 +0330 Subject: [PATCH 07/10] [245] UdpIpMreqMultipleReceiversIPv4 test added. --- core/src/engine/io/socket_test.cpp | 69 ++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/core/src/engine/io/socket_test.cpp b/core/src/engine/io/socket_test.cpp index 53174e3cdaab..ae0ab2835127 100644 --- a/core/src/engine/io/socket_test.cpp +++ b/core/src/engine/io/socket_test.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -464,6 +465,74 @@ UTEST_MT(Socket, ConcurrentReadWriteUdp, 2) { /// [send self concurrent] } +UTEST_MT(Socket, UdpIpMreqMultipleReceiversIPv4, 3) { + const auto deadline = Deadline::FromDuration(utest::kMaxTestWaitTime); + + static constexpr uint16_t kPort = 12345; + static constexpr const char* kGroup = "239.255.0.1"; + static constexpr int packets_count = 3; + + sockaddr_in raw_multiaddr{AF_INET, htons(kPort), {}, {}}; + inet_pton(AF_INET, kGroup, &raw_multiaddr.sin_addr); + io::Sockaddr multiaddr(&raw_multiaddr); + io::IpMreq mreq(kGroup, INADDR_ANY); + + std::vector> tasks; + std::vector> receivers; + for (int i = 0; i < 2; ++i) { + const auto& receiver = receivers.emplace_back( + std::make_shared(io::AddrDomain::kInet, io::SocketType::kDgram) + ); + int reuse = 1; + receiver->SetOption(SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); + + sockaddr_in any{AF_INET, htons(kPort), {}, {}}; + any.sin_addr.s_addr = htonl(INADDR_ANY); + receiver->Bind(io::Sockaddr(&any)); + receiver->SetOption(mreq.GetSocketOptionLevel(), mreq.GetJoinSocketOptionName(), mreq.Data(), mreq.Size()); + + tasks.push_back(engine::AsyncNoSpan([receiver, deadline] { + char c{}; + for (int packet_idx = 0; packet_idx < packets_count; ++packet_idx) { + const auto result = receiver->RecvSomeFrom(&c, 1, deadline); + EXPECT_EQ(result.bytes_received, 1); + EXPECT_EQ(c, 'a' + packet_idx); + } + })); + } + + io::Socket sender{io::AddrDomain::kInet, io::SocketType::kDgram}; + for (int packet_idx = 0; packet_idx < packets_count; ++packet_idx) { + const char data = 'a' + packet_idx; + EXPECT_EQ(sender.SendAllTo(multiaddr, &data, 1, deadline), 1); + } + + for (auto& t : tasks) { + t.Get(); + } + tasks.clear(); + + for (int i = 0; i < 2; ++i) { + const auto& receiver = receivers[i]; + receiver->SetOption(mreq.GetSocketOptionLevel(), mreq.GetLeaveSocketOption(), mreq.Data(), mreq.Size()); + + tasks.push_back(engine::AsyncNoSpan([receiver] { + auto short_deadline = Deadline::FromDuration(std::chrono::milliseconds(300)); + char c{}; + const auto result = receiver->RecvSomeFrom(&c, 1, short_deadline); + EXPECT_EQ(result.bytes_received, 0); + })); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + char data = 'x'; + EXPECT_EQ(sender.SendAllTo(multiaddr, &data, 1, deadline), 1); + for (auto& t : tasks) { + UEXPECT_THROW(t.Get(), io::IoTimeout); + } +} + UTEST(Socket, WriteALot) { const auto deadline = Deadline::FromDuration(utest::kMaxTestWaitTime); From 5c71b0a7a5eda1136ee58d890c2ae63d9757e867 Mon Sep 17 00:00:00 2001 From: Dmitry Borchuk Date: Thu, 26 Feb 2026 10:53:42 +0330 Subject: [PATCH 08/10] [245] Missing thread include in tests fix. --- core/src/engine/io/socket_test.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/engine/io/socket_test.cpp b/core/src/engine/io/socket_test.cpp index ae0ab2835127..1a5fd2d79224 100644 --- a/core/src/engine/io/socket_test.cpp +++ b/core/src/engine/io/socket_test.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include From 9dbc315ca388f142a029dd0b66fe11fc1e262d36 Mon Sep 17 00:00:00 2001 From: Dmitry Borchuk Date: Thu, 26 Feb 2026 13:03:16 +0330 Subject: [PATCH 09/10] [245] Socket methods for joining/leaving multicast group added. --- core/include/userver/engine/io/socket.hpp | 6 ++++++ core/src/engine/io/socket.cpp | 8 ++++++++ core/src/engine/io/socket_test.cpp | 4 ++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/core/include/userver/engine/io/socket.hpp b/core/include/userver/engine/io/socket.hpp index 8c2e44b96508..1e8534acf64a 100644 --- a/core/include/userver/engine/io/socket.hpp +++ b/core/include/userver/engine/io/socket.hpp @@ -68,6 +68,12 @@ class [[nodiscard]] Socket final : public RwBase { /// Starts listening for connections on a specified socket (must be bound). void Listen(int backlog = SOMAXCONN); + /// @brief Joins multicast group to receive multicast datagrams. + void AddMembership(const IpMreq& mreq); + + /// @brief Leaves multicast group previously joined with AddMembership. + void DropMembership(const IpMreq& mreq); + /// Suspends current task until the socket has data available. /// @returns false on timeout or on task cancellations; true otherwise. [[nodiscard]] bool WaitReadable(Deadline) override; diff --git a/core/src/engine/io/socket.cpp b/core/src/engine/io/socket.cpp index e3885c8cc777..110f9c641652 100644 --- a/core/src/engine/io/socket.cpp +++ b/core/src/engine/io/socket.cpp @@ -233,6 +233,14 @@ void Socket::Listen(int backlog) { IoSystemError>(::listen(Fd(), backlog), "listening on a socket, fd={}, backlog={}", Fd(), backlog); } +void Socket::AddMembership(const IpMreq& mreq) { + SetOption(mreq.GetSocketOptionLevel(), mreq.GetJoinSocketOptionName(), mreq.Data(), mreq.Size()); +} + +void Socket::DropMembership(const IpMreq& mreq) { + SetOption(mreq.GetSocketOptionLevel(), mreq.GetLeaveSocketOption(), mreq.Data(), mreq.Size()); +} + bool Socket::WaitReadable(Deadline deadline) { UASSERT(IsValid()); return fd_control_->Read().Wait(deadline); diff --git a/core/src/engine/io/socket_test.cpp b/core/src/engine/io/socket_test.cpp index 1a5fd2d79224..c7c157787a2a 100644 --- a/core/src/engine/io/socket_test.cpp +++ b/core/src/engine/io/socket_test.cpp @@ -490,7 +490,7 @@ UTEST_MT(Socket, UdpIpMreqMultipleReceiversIPv4, 3) { sockaddr_in any{AF_INET, htons(kPort), {}, {}}; any.sin_addr.s_addr = htonl(INADDR_ANY); receiver->Bind(io::Sockaddr(&any)); - receiver->SetOption(mreq.GetSocketOptionLevel(), mreq.GetJoinSocketOptionName(), mreq.Data(), mreq.Size()); + receiver->AddMembership(mreq); tasks.push_back(engine::AsyncNoSpan([receiver, deadline] { char c{}; @@ -515,7 +515,7 @@ UTEST_MT(Socket, UdpIpMreqMultipleReceiversIPv4, 3) { for (int i = 0; i < 2; ++i) { const auto& receiver = receivers[i]; - receiver->SetOption(mreq.GetSocketOptionLevel(), mreq.GetLeaveSocketOption(), mreq.Data(), mreq.Size()); + receiver->DropMembership(mreq); tasks.push_back(engine::AsyncNoSpan([receiver] { auto short_deadline = Deadline::FromDuration(std::chrono::milliseconds(300)); From 8541224de81e8c95af040aa2352507ebf39db925 Mon Sep 17 00:00:00 2001 From: Dmitry Borchuk Date: Thu, 26 Feb 2026 14:03:58 +0330 Subject: [PATCH 10/10] [245] Multicast socket creation sample added. --- core/include/userver/engine/io/socket.hpp | 1 + core/src/engine/io/socket_test.cpp | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/include/userver/engine/io/socket.hpp b/core/include/userver/engine/io/socket.hpp index 1e8534acf64a..68a179d56163 100644 --- a/core/include/userver/engine/io/socket.hpp +++ b/core/include/userver/engine/io/socket.hpp @@ -69,6 +69,7 @@ class [[nodiscard]] Socket final : public RwBase { void Listen(int backlog = SOMAXCONN); /// @brief Joins multicast group to receive multicast datagrams. + /// @snippet src/engine/io/socket_test.cpp multicast socket creation sample void AddMembership(const IpMreq& mreq); /// @brief Leaves multicast group previously joined with AddMembership. diff --git a/core/src/engine/io/socket_test.cpp b/core/src/engine/io/socket_test.cpp index c7c157787a2a..e6ef6eef261f 100644 --- a/core/src/engine/io/socket_test.cpp +++ b/core/src/engine/io/socket_test.cpp @@ -481,9 +481,8 @@ UTEST_MT(Socket, UdpIpMreqMultipleReceiversIPv4, 3) { std::vector> tasks; std::vector> receivers; for (int i = 0; i < 2; ++i) { - const auto& receiver = receivers.emplace_back( - std::make_shared(io::AddrDomain::kInet, io::SocketType::kDgram) - ); + /// [[multicast socket creation sample]] + const auto& receiver = std::make_shared(io::AddrDomain::kInet, io::SocketType::kDgram); int reuse = 1; receiver->SetOption(SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); @@ -491,6 +490,8 @@ UTEST_MT(Socket, UdpIpMreqMultipleReceiversIPv4, 3) { any.sin_addr.s_addr = htonl(INADDR_ANY); receiver->Bind(io::Sockaddr(&any)); receiver->AddMembership(mreq); + /// [[multicast socket creation sample]] + receivers.emplace_back(receiver); tasks.push_back(engine::AsyncNoSpan([receiver, deadline] { char c{};