#include "udp_client.h" #include #include #include #include #include #include #include #include #include #include #include #include UDPClient::UDPClient(std::string host, uint16_t port, std::string multicast_group, uint16_t data_port) : host_(std::move(host)) , port_(port) , multicast_group_(std::move(multicast_group)) , data_port_(data_port) {} UDPClient::~UDPClient() { stop(); } void UDPClient::start() { running_ = true; thread_ = std::thread(&UDPClient::run, this); } void UDPClient::stop() { running_ = false; if (thread_.joinable()) thread_.join(); } UDPClient::Stats UDPClient::stats() const { Stats s; s.packets_rx = rx_packets_.load(std::memory_order_relaxed); s.packets_lost = seq_gaps_.load(std::memory_order_relaxed); s.bytes_rx = rx_bytes_.load(std::memory_order_relaxed); s.connected = connected_.load(std::memory_order_relaxed); return s; } // ─── Fragmentation reassembly ──────────────────────────────────────── namespace { struct FragSet { uint32_t total = 0; uint32_t count = 0; std::vector> parts; std::chrono::steady_clock::time_point created; }; using ReassemblyMap = std::map; std::vector reassemble(ReassemblyMap& m, const UDPSPacketHeader& hdr, const uint8_t* payload, size_t plen) { if (hdr.total_fragments == 1 && hdr.fragment_idx == 0) return std::vector(payload, payload + plen); uint64_t key = (uint64_t(hdr.counter) << 8) | hdr.type; auto& fs = m[key]; if (fs.parts.empty()) { fs.total = hdr.total_fragments; fs.parts.resize(fs.total); fs.created = std::chrono::steady_clock::now(); } if (hdr.fragment_idx < fs.total && fs.parts[hdr.fragment_idx].empty()) { fs.parts[hdr.fragment_idx].assign(payload, payload + plen); ++fs.count; } if (fs.count < fs.total) return {}; size_t sz = 0; for (auto& p : fs.parts) sz += p.size(); std::vector out; out.reserve(sz); for (auto& p : fs.parts) out.insert(out.end(), p.begin(), p.end()); m.erase(key); return out; } // Drop fragments older than 2 seconds to avoid unbounded map growth void gc_fragments(ReassemblyMap& m) { auto now = std::chrono::steady_clock::now(); auto it = m.begin(); while (it != m.end()) { if (std::chrono::duration(now - it->second.created).count() > 2.0) it = m.erase(it); else ++it; } } } // anonymous namespace // ─── Ingest thread ─────────────────────────────────────────────────── void UDPClient::run() { if (!multicast_group_.empty()) run_multicast(); else run_unicast(); } void UDPClient::run_unicast() { constexpr double SILENCE_SEC = 5.0; constexpr double CONNECT_RETRY= 1.0; // re-send CONNECT if no CONFIG yet constexpr double RETRY_SEC = 2.0; // wait before full reconnect constexpr size_t BUF_SZ = 65536; uint8_t buf[BUF_SZ]; while (running_) { // ── Resolve server address ──────────────────────────────────── addrinfo hints{}; hints.ai_family = AF_INET; hints.ai_socktype = SOCK_DGRAM; char port_str[8]; std::snprintf(port_str, sizeof(port_str), "%u", port_); addrinfo* res = nullptr; if (getaddrinfo(host_.c_str(), port_str, &hints, &res) != 0 || !res) { std::fprintf(stderr, "[udp_client %s:%u] cannot resolve host\n", host_.c_str(), port_); std::this_thread::sleep_for(std::chrono::duration(RETRY_SEC)); continue; } sockaddr_in server{}; std::memcpy(&server, res->ai_addr, sizeof(server)); freeaddrinfo(res); // ── Open socket ─────────────────────────────────────────────── int sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock < 0) { std::fprintf(stderr, "[udp_client %s:%u] socket: %s\n", host_.c_str(), port_, std::strerror(errno)); std::this_thread::sleep_for(std::chrono::duration(RETRY_SEC)); continue; } // Large receive buffer { int rcv = 8 * 1024 * 1024; setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcv, sizeof(rcv)); } // Bind to any local port so we have a stable source address { sockaddr_in local{}; local.sin_family = AF_INET; local.sin_port = 0; // OS picks port local.sin_addr.s_addr = INADDR_ANY; if (bind(sock, reinterpret_cast(&local), sizeof(local)) < 0) { std::fprintf(stderr, "[udp_client %s:%u] bind: %s\n", host_.c_str(), port_, std::strerror(errno)); close(sock); std::this_thread::sleep_for(std::chrono::duration(RETRY_SEC)); continue; } } // ── Send CONNECT to server ──────────────────────────────────── { auto pkt = build_connect(); sendto(sock, pkt.data(), pkt.size(), 0, reinterpret_cast(&server), sizeof(server)); std::fprintf(stderr, "[udp_client %s:%u] CONNECT sent\n", host_.c_str(), port_); } // ── Receive loop (poll-based, non-blocking recv) ────────────── ReassemblyMap fragments; std::vector signal_defs; uint32_t last_counter = 0; bool seen_first = false; bool config_rx = false; using clock = std::chrono::steady_clock; auto last_rx = clock::now(); auto last_connect = clock::now(); uint32_t gc_counter = 0; while (running_) { // poll() with 200 ms timeout — keeps reconnect logic responsive pollfd pfd{sock, POLLIN, 0}; int ready = poll(&pfd, 1, 200 /*ms*/); if (!running_) break; if (ready < 0) { if (errno == EINTR) continue; std::fprintf(stderr, "[udp_client %s:%u] poll: %s\n", host_.c_str(), port_, std::strerror(errno)); break; } auto now = clock::now(); double elapsed = std::chrono::duration(now - last_rx).count(); if (ready == 0) { // Timeout — check timers if (elapsed > SILENCE_SEC) { std::fprintf(stderr, "[udp_client %s:%u] silence timeout, reconnecting\n", host_.c_str(), port_); connected_ = false; break; } // If CONFIG not yet received, re-send CONNECT periodically if (!config_rx) { double since_connect = std::chrono::duration( now - last_connect).count(); if (since_connect >= CONNECT_RETRY) { auto pkt = build_connect(); sendto(sock, pkt.data(), pkt.size(), 0, reinterpret_cast(&server), sizeof(server)); last_connect = now; } } continue; } // ── Read one datagram ───────────────────────────────────── ssize_t n = recvfrom(sock, buf, BUF_SZ, 0, nullptr, nullptr); if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) continue; std::fprintf(stderr, "[udp_client %s:%u] recv: %s\n", host_.c_str(), port_, std::strerror(errno)); break; } last_rx = clock::now(); rx_packets_++; rx_bytes_ += static_cast(n); UDPSPacketHeader hdr; if (!parse_header(buf, static_cast(n), hdr)) continue; size_t plen = static_cast(n) - UDPS_HEADER_SIZE; const uint8_t* payload = buf + UDPS_HEADER_SIZE; auto complete = reassemble(fragments, hdr, payload, plen); if (complete.empty()) continue; // Periodic GC of stale fragment sets if (++gc_counter % 1000 == 0) gc_fragments(fragments); switch (hdr.type) { case UDPS_TYPE_CONFIG: signal_defs = parse_config(complete.data(), complete.size()); config_rx = true; connected_ = true; std::fprintf(stderr, "[udp_client %s:%u] CONFIG: %zu signals\n", host_.c_str(), port_, signal_defs.size()); if (on_config) on_config(signal_defs); break; case UDPS_TYPE_DATA: { if (seen_first && hdr.counter != last_counter + 1) seq_gaps_ += hdr.counter - last_counter - 1; seen_first = true; last_counter = hdr.counter; if (signal_defs.empty()) break; auto dp = parse_data(complete.data(), complete.size(), signal_defs); if (on_data) on_data(dp, signal_defs); break; } case UDPS_TYPE_DISCONNECT: std::fprintf(stderr, "[udp_client %s:%u] server DISCONNECT\n", host_.c_str(), port_); connected_ = false; goto reconnect; default: break; } } reconnect: { auto pkt = build_disconnect(); sendto(sock, pkt.data(), pkt.size(), 0, reinterpret_cast(&server), sizeof(server)); } close(sock); if (running_) std::this_thread::sleep_for(std::chrono::duration(RETRY_SEC)); } std::fprintf(stderr, "[udp_client %s:%u] stopped\n", host_.c_str(), port_); } // ─── Multicast ingest thread ────────────────────────────────────────── /** Returns true if the session ran normally (even if the server disconnected), * false if a fatal socket error occurred before data could be received. */ bool UDPClient::run_multicast_session() { constexpr double SILENCE_SEC = 5.0; constexpr size_t BUF_SZ = 65536; uint8_t buf[BUF_SZ]; // ── Resolve server address ──────────────────────────────────────── addrinfo hints{}; hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; char port_str[8]; std::snprintf(port_str, sizeof(port_str), "%u", port_); addrinfo* res = nullptr; if (getaddrinfo(host_.c_str(), port_str, &hints, &res) != 0 || !res) { std::fprintf(stderr, "[udp_client %s:%u] cannot resolve host\n", host_.c_str(), port_); return false; } sockaddr_in server{}; std::memcpy(&server, res->ai_addr, sizeof(server)); freeaddrinfo(res); // ── Open and connect TCP control socket ─────────────────────────── int tcp_sock = socket(AF_INET, SOCK_STREAM, 0); if (tcp_sock < 0) return false; { int one = 1; setsockopt(tcp_sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)); } if (connect(tcp_sock, reinterpret_cast(&server), sizeof(server)) < 0) { std::fprintf(stderr, "[udp_client %s:%u] tcp connect: %s\n", host_.c_str(), port_, std::strerror(errno)); close(tcp_sock); return false; } // RAII wrapper so tcp_sock is always closed on return struct TcpGuard { int& fd; std::function on_close; ~TcpGuard() { if (on_close) on_close(); close(fd); } } tcp_guard{tcp_sock, [&]() { auto pkt = build_disconnect(); send(tcp_sock, pkt.data(), pkt.size(), 0); }}; // ── Send CONNECT via TCP ────────────────────────────────────────── { auto pkt = build_connect(); if (send(tcp_sock, pkt.data(), pkt.size(), 0) < 0) { std::fprintf(stderr, "[udp_client %s:%u] tcp send CONNECT: %s\n", host_.c_str(), port_, std::strerror(errno)); return false; } std::fprintf(stderr, "[udp_client %s:%u] tcp: CONNECT sent\n", host_.c_str(), port_); } // ── Read CONFIG header via TCP ──────────────────────────────────── uint8_t hdr_buf[UDPS_HEADER_SIZE]; { size_t total = 0; while (total < UDPS_HEADER_SIZE) { ssize_t n = recv(tcp_sock, hdr_buf + total, UDPS_HEADER_SIZE - total, 0); if (n <= 0) return false; total += static_cast(n); } } UDPSPacketHeader cfg_hdr{}; if (!parse_header(hdr_buf, UDPS_HEADER_SIZE, cfg_hdr) || cfg_hdr.type != UDPS_TYPE_CONFIG) { std::fprintf(stderr, "[udp_client %s:%u] expected CONFIG header\n", host_.c_str(), port_); return false; } // ── Read CONFIG payload via TCP ─────────────────────────────────── std::vector cfg_payload(cfg_hdr.payload_bytes); { size_t total = 0; while (total < cfg_hdr.payload_bytes) { ssize_t n = recv(tcp_sock, cfg_payload.data() + total, cfg_hdr.payload_bytes - total, 0); if (n <= 0) return false; total += static_cast(n); } } auto signal_defs = parse_config(cfg_payload.data(), cfg_payload.size()); std::fprintf(stderr, "[udp_client %s:%u] tcp: CONFIG (%zu signals)\n", host_.c_str(), port_, signal_defs.size()); connected_ = true; if (on_config) on_config(signal_defs); // ── Determine multicast data port ───────────────────────────────── uint16_t mcast_port = (data_port_ > 0) ? data_port_ : static_cast(port_ + 1u); // ── Open UDP socket and join multicast group ────────────────────── int udp_sock = socket(AF_INET, SOCK_DGRAM, 0); if (udp_sock < 0) return false; // RAII for udp_sock struct UdpGuard { int& fd; std::string& mgroup; ~UdpGuard() { ip_mreq mreq{}; mreq.imr_multiaddr.s_addr = inet_addr(mgroup.c_str()); mreq.imr_interface.s_addr = INADDR_ANY; setsockopt(fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq)); close(fd); } } udp_guard{udp_sock, multicast_group_}; { int reuse = 1; setsockopt(udp_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); int rcv = 8 * 1024 * 1024; setsockopt(udp_sock, SOL_SOCKET, SO_RCVBUF, &rcv, sizeof(rcv)); } { sockaddr_in mcast_addr{}; mcast_addr.sin_family = AF_INET; mcast_addr.sin_port = htons(mcast_port); mcast_addr.sin_addr.s_addr = INADDR_ANY; if (bind(udp_sock, reinterpret_cast(&mcast_addr), sizeof(mcast_addr)) < 0) { std::fprintf(stderr, "[udp_client %s:%u] udp bind: %s\n", host_.c_str(), port_, std::strerror(errno)); return false; } } { ip_mreq mreq{}; mreq.imr_multiaddr.s_addr = inet_addr(multicast_group_.c_str()); mreq.imr_interface.s_addr = INADDR_ANY; if (setsockopt(udp_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { std::fprintf(stderr, "[udp_client %s:%u] IP_ADD_MEMBERSHIP(%s): %s\n", host_.c_str(), port_, multicast_group_.c_str(), std::strerror(errno)); return false; } } std::fprintf(stderr, "[udp_client %s:%u] joined multicast %s:%u\n", host_.c_str(), port_, multicast_group_.c_str(), mcast_port); // ── Main DATA loop ──────────────────────────────────────────────── ReassemblyMap fragments; uint32_t last_counter = 0; bool seen_first = false; uint32_t gc_counter = 0; using clock = std::chrono::steady_clock; auto last_rx = clock::now(); while (running_) { pollfd pfds[2]; pfds[0] = {udp_sock, POLLIN, 0}; pfds[1] = {tcp_sock, POLLIN, 0}; int ready = poll(pfds, 2, 200 /*ms*/); if (!running_) break; if (ready < 0) { if (errno == EINTR) continue; break; } // TCP event: DISCONNECT or closed connection if (pfds[1].revents & (POLLIN | POLLHUP | POLLERR)) { ssize_t n = recv(tcp_sock, buf, BUF_SZ, MSG_DONTWAIT); if (n <= 0) { std::fprintf(stderr, "[udp_client %s:%u] tcp control closed\n", host_.c_str(), port_); break; } UDPSPacketHeader ctrl_hdr{}; if (parse_header(buf, static_cast(n), ctrl_hdr) && ctrl_hdr.type == UDPS_TYPE_DISCONNECT) { std::fprintf(stderr, "[udp_client %s:%u] server DISCONNECT\n", host_.c_str(), port_); break; } } // Silence timeout on UDP DATA { auto now = clock::now(); double elapsed = std::chrono::duration(now - last_rx).count(); if (!(pfds[0].revents & POLLIN)) { if (elapsed > SILENCE_SEC) { std::fprintf(stderr, "[udp_client %s:%u] multicast silence timeout\n", host_.c_str(), port_); break; } continue; } } ssize_t n = recvfrom(udp_sock, buf, BUF_SZ, 0, nullptr, nullptr); if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) continue; std::fprintf(stderr, "[udp_client %s:%u] udp recv: %s\n", host_.c_str(), port_, std::strerror(errno)); break; } last_rx = clock::now(); rx_packets_++; rx_bytes_ += static_cast(n); UDPSPacketHeader hdr{}; if (!parse_header(buf, static_cast(n), hdr)) continue; size_t plen = static_cast(n) - UDPS_HEADER_SIZE; const uint8_t* payload = buf + UDPS_HEADER_SIZE; auto complete = reassemble(fragments, hdr, payload, plen); if (complete.empty()) continue; if (++gc_counter % 1000 == 0) gc_fragments(fragments); if (hdr.type == UDPS_TYPE_DATA) { if (seen_first && hdr.counter != last_counter + 1) seq_gaps_ += hdr.counter - last_counter - 1; seen_first = true; last_counter = hdr.counter; if (!signal_defs.empty()) { auto dp = parse_data(complete.data(), complete.size(), signal_defs); if (on_data) on_data(dp, signal_defs); } } } connected_ = false; return true; } void UDPClient::run_multicast() { constexpr double RETRY_SEC = 2.0; while (running_) { connected_ = false; run_multicast_session(); connected_ = false; if (running_) std::this_thread::sleep_for(std::chrono::duration(RETRY_SEC)); } std::fprintf(stderr, "[udp_client %s:%u] multicast stopped\n", host_.c_str(), port_); }