Files
MARTe-Integrated-Components/Common/Client/c/udps_client.c
T
Martino FerrariandClaude Opus 4.6 7c5eb31a52 feat: standalone C/C++ UDPS client library
Consuming a UDPStreamer feed so far meant either linking MARTe2 (UDPSClient)
or writing Go (Common/Client/go/udpsprotocol). Common/Client/c fills the gap
for plain C/C++ integrators: two files depending on nothing but libc and BSD
sockets, covering the whole receive path — CONNECT, fragment reassembly,
CONFIG/DATA decoding with dequantisation, keepalives and silence-triggered
reconnect. No threads are spawned; udps_client_poll() does all the work and
runs every callback, so it drops into an existing event loop unsynchronised.

Verified against run_udp_producer.sh at 1 Msps: unicast (120 MiB, no loss) and
multicast with 12-fragment cycles (116k datagrams, no loss).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-22 16:51:40 +02:00

1210 lines
39 KiB
C

/**
* @file udps_client.c
* @brief Implementation of the standalone UDPS receiver library.
*
* Layering, bottom up: little-endian readers, fragment reassembly, CONFIG/DATA
* decoding, socket transport, and finally udps_client_poll() which stitches
* them together into the connect/receive/reconnect state machine.
*/
#define _POSIX_C_SOURCE 200809L
/* struct ip_mreq is not in strict POSIX; _BSD_SOURCE is the pre-2.19 glibc name. */
#define _DEFAULT_SOURCE 1
#define _BSD_SOURCE 1
#include "udps_client.h"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
/*---------------------------------------------------------------------------*/
/* Tunable limits */
/*---------------------------------------------------------------------------*/
/** Concurrent in-flight reassemblies. The server interleaves at most a couple. */
#define UDPS_MAX_SLOTS 4
/** Fragments per update; also bounds the receive bitmask below. */
#define UDPS_MAX_FRAGMENTS 512
#define UDPS_FRAG_MASK_BYTES (UDPS_MAX_FRAGMENTS / 8)
/** Largest datagram we can receive, header included. */
#define UDPS_RX_BUF_BYTES (65535 + 17)
/** An incomplete reassembly older than this is abandoned. */
#define UDPS_SLOT_STALE_S 2.0
/** Datagrams drained per poll before housekeeping runs again. */
#define UDPS_DRAIN_LIMIT 256
/** Refuse CONFIGs claiming more signals than this. */
#define UDPS_MAX_SIGNALS 4096
/*---------------------------------------------------------------------------*/
/* Little-endian primitives */
/*---------------------------------------------------------------------------*/
static uint16_t rd_u16(const uint8_t *p) {
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}
static uint32_t rd_u32(const uint8_t *p) {
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) |
((uint32_t)p[3] << 24);
}
static uint64_t rd_u64(const uint8_t *p) {
return (uint64_t)rd_u32(p) | ((uint64_t)rd_u32(p + 4) << 32);
}
static float rd_f32(const uint8_t *p) {
uint32_t bits = rd_u32(p);
float f;
memcpy(&f, &bits, sizeof f);
return f;
}
static double rd_f64(const uint8_t *p) {
uint64_t bits = rd_u64(p);
double d;
memcpy(&d, &bits, sizeof d);
return d;
}
static void wr_u16(uint8_t *p, uint16_t v) {
p[0] = (uint8_t)(v & 0xFFu);
p[1] = (uint8_t)(v >> 8);
}
static void wr_u32(uint8_t *p, uint32_t v) {
p[0] = (uint8_t)(v & 0xFFu);
p[1] = (uint8_t)((v >> 8) & 0xFFu);
p[2] = (uint8_t)((v >> 16) & 0xFFu);
p[3] = (uint8_t)((v >> 24) & 0xFFu);
}
/** Serialises a header into @p buf, which must hold UDPS_HEADER_SIZE bytes. */
static void build_header(uint8_t *buf, uint8_t type, uint32_t counter,
uint16_t frag_idx, uint16_t total_frags,
uint32_t payload_bytes) {
wr_u32(buf, UDPS_MAGIC);
buf[4] = type;
wr_u32(buf + 5, counter);
wr_u16(buf + 9, frag_idx);
wr_u16(buf + 11, total_frags);
wr_u32(buf + 13, payload_bytes);
}
/*---------------------------------------------------------------------------*/
/* Clocks */
/*---------------------------------------------------------------------------*/
static double now_mono(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
}
static double now_wall(void) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
}
static void sleep_s(double s) {
struct timespec ts;
if (s <= 0.0) {
return;
}
ts.tv_sec = (time_t)s;
ts.tv_nsec = (long)((s - (double)ts.tv_sec) * 1e9);
(void)nanosleep(&ts, NULL);
}
/*---------------------------------------------------------------------------*/
/* Internal state */
/*---------------------------------------------------------------------------*/
typedef struct {
int active;
uint32_t counter;
uint8_t type;
uint16_t total_fragments;
uint16_t received_fragments;
uint8_t mask[UDPS_FRAG_MASK_BYTES];
uint8_t *payload;
size_t payload_cap;
uint32_t chunk_size; /**< Learned from fragment 0; fixes placement. */
uint32_t assembled_bytes;/**< Highest byte written; the true payload size. */
double first_seen;
} udps_slot_t;
struct udps_client {
udps_client_config_t cfg;
/* Owned copies: the caller's strings need not outlive the create call. */
char server_addr[128];
char mcast_group[128];
char iface_addr[128];
struct sockaddr_in server_sa;
int udp_fd; /**< Unicast receive socket, or joined multicast socket. */
int tcp_fd; /**< Multicast control channel; -1 in unicast mode. */
int connected;
int ever_connected;
double last_data; /**< Monotonic; drives the silence timeout. */
double last_keepalive;
double disconnect_t; /**< Monotonic; 0 before the first attempt. */
udps_slot_t slots[UDPS_MAX_SLOTS];
udps_signal_t *sigs;
uint32_t num_sigs;
size_t sigs_cap;
uint8_t publish_mode;
double *valbuf; /**< Arena holding one frame's doubles. */
size_t valbuf_cap;
udps_signal_values_t *vals;
size_t vals_cap;
uint8_t *rxbuf;
uint32_t last_counter;
int have_counter;
udps_stats_t stats;
char err[256];
udps_config_cb on_config;
udps_data_cb on_data;
udps_event_cb on_event;
void *user;
};
static void emit_event(udps_client_t *c, udps_event_t ev, const char *detail) {
if (c->on_event != NULL) {
c->on_event(ev, detail, c->user);
}
}
/** Records an error, reports it, and always returns -1 for tail calls. */
static int fail(udps_client_t *c, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vsnprintf(c->err, sizeof c->err, fmt, ap);
va_end(ap);
emit_event(c, UDPS_EVENT_ERROR, c->err);
return -1;
}
/** Grows @p *buf to at least @p need bytes, preserving nothing. */
static int ensure_cap(void **buf, size_t *cap, size_t need) {
void *p;
if (*cap >= need) {
return 0;
}
p = realloc(*buf, need);
if (p == NULL) {
return -1;
}
*buf = p;
*cap = need;
return 0;
}
/*---------------------------------------------------------------------------*/
/* Stateless helpers (public) */
/*---------------------------------------------------------------------------*/
uint32_t udps_signal_num_elements(const udps_signal_t *s) {
uint32_t r, c, n;
if (s == NULL) {
return 0u;
}
r = (s->num_rows == 0u) ? 1u : s->num_rows;
c = (s->num_cols == 0u) ? 1u : s->num_cols;
if (r > UDPS_MAX_ELEMENTS || c > UDPS_MAX_ELEMENTS) {
return UDPS_MAX_ELEMENTS;
}
n = r * c;
return (n > UDPS_MAX_ELEMENTS) ? UDPS_MAX_ELEMENTS : n;
}
const char *udps_type_name(uint8_t tc) {
switch (tc) {
case UDPS_T_UINT8: return "uint8";
case UDPS_T_INT8: return "int8";
case UDPS_T_UINT16: return "uint16";
case UDPS_T_INT16: return "int16";
case UDPS_T_UINT32: return "uint32";
case UDPS_T_INT32: return "int32";
case UDPS_T_UINT64: return "uint64";
case UDPS_T_INT64: return "int64";
case UDPS_T_FLOAT32: return "float32";
case UDPS_T_FLOAT64: return "float64";
default: return "unknown";
}
}
int udps_parse_header(const void *buf, size_t len, udps_header_t *out) {
const uint8_t *b = (const uint8_t *)buf;
if (b == NULL || out == NULL || len < UDPS_HEADER_SIZE) {
return -1;
}
out->magic = rd_u32(b);
if (out->magic != UDPS_MAGIC) {
return -1;
}
out->type = b[4];
out->counter = rd_u32(b + 5);
out->fragment_idx = rd_u16(b + 9);
out->total_fragments = rd_u16(b + 11);
out->payload_bytes = rd_u32(b + 13);
return 0;
}
int udps_parse_config(const void *payload, size_t len, udps_signal_t *sigs,
uint32_t max_signals, uint32_t *num_signals,
uint8_t *publish_mode) {
const uint8_t *p = (const uint8_t *)payload;
uint32_t n, i;
size_t off;
if (p == NULL || sigs == NULL || num_signals == NULL || len < 4u) {
return -1;
}
n = rd_u32(p);
/* A CONFIG cannot describe more signals than its payload can hold. */
if (n > (len - 4u) / UDPS_SIGNAL_DESC_SIZE || n > max_signals) {
return -1;
}
off = 4u;
for (i = 0u; i < n; i++) {
const uint8_t *d = p + off;
udps_signal_t *s = &sigs[i];
memcpy(s->name, d, 64);
s->name[64] = '\0';
s->type_code = d[64];
s->quant_type = d[65];
s->num_dimensions = d[66];
s->num_rows = rd_u32(d + 67);
s->num_cols = rd_u32(d + 71);
s->range_min = rd_f64(d + 75);
s->range_max = rd_f64(d + 83);
s->time_mode = d[91];
s->sampling_rate = rd_f64(d + 92);
s->time_signal_idx = rd_u32(d + 100);
memcpy(s->unit, d + 104, 32);
s->unit[32] = '\0';
off += UDPS_SIGNAL_DESC_SIZE;
}
*num_signals = n;
if (publish_mode != NULL) {
/* Trailing byte, absent in streams from older producers. */
*publish_mode = (off < len) ? p[off] : (uint8_t)UDPS_PUBLISH_STRICT;
}
return 0;
}
double udps_frame_value(const udps_frame_t *f, uint32_t sig, uint32_t sample,
uint32_t elem) {
const udps_signal_values_t *v;
uint32_t idx;
if (f == NULL || sig >= f->num_signals) {
return 0.0;
}
v = &f->values[sig];
/* Accumulated scalars hold one value per batch slot; everything else holds
* one value per element and repeats across slots. */
idx = (v->count == f->num_samples && udps_signal_num_elements(&f->signals[sig]) == 1u)
? sample
: elem;
return (idx < v->count) ? v->values[idx] : 0.0;
}
double udps_frame_element_time(const udps_frame_t *f, uint32_t sig,
uint32_t elem) {
const udps_signal_t *s;
uint32_t n;
double dt;
if (f == NULL || sig >= f->num_signals) {
return 0.0;
}
s = &f->signals[sig];
n = udps_signal_num_elements(s);
if (n <= 1u || s->sampling_rate <= 0.0 ||
(s->time_mode != UDPS_TIME_FIRST_SAMPLE &&
s->time_mode != UDPS_TIME_LAST_SAMPLE)) {
return f->recv_time;
}
if (elem >= n) {
elem = n - 1u;
}
dt = 1.0 / s->sampling_rate;
/* Anchor on arrival == the newest element, and walk backwards. */
return f->recv_time - (double)(n - 1u - elem) * dt;
}
/*---------------------------------------------------------------------------*/
/* Payload decoding */
/*---------------------------------------------------------------------------*/
static size_t raw_type_size(uint8_t tc) {
switch (tc) {
case UDPS_T_UINT8: case UDPS_T_INT8: return 1u;
case UDPS_T_UINT16: case UDPS_T_INT16: return 2u;
case UDPS_T_UINT32: case UDPS_T_INT32: case UDPS_T_FLOAT32: return 4u;
case UDPS_T_UINT64: case UDPS_T_INT64: case UDPS_T_FLOAT64: return 8u;
default: return 0u;
}
}
static size_t quant_size(uint8_t qt) {
switch (qt) {
case UDPS_QUANT_UINT8: case UDPS_QUANT_INT8: return 1u;
case UDPS_QUANT_UINT16: case UDPS_QUANT_INT16: return 2u;
default: return 0u;
}
}
static double read_raw(const uint8_t *p, uint8_t tc) {
switch (tc) {
case UDPS_T_UINT8: return (double)p[0];
case UDPS_T_INT8: return (double)(int8_t)p[0];
case UDPS_T_UINT16: return (double)rd_u16(p);
case UDPS_T_INT16: return (double)(int16_t)rd_u16(p);
case UDPS_T_UINT32: return (double)rd_u32(p);
case UDPS_T_INT32: return (double)(int32_t)rd_u32(p);
case UDPS_T_UINT64: return (double)rd_u64(p);
case UDPS_T_INT64: return (double)(int64_t)rd_u64(p);
case UDPS_T_FLOAT32: return (double)rd_f32(p);
case UDPS_T_FLOAT64: return rd_f64(p);
default: return 0.0;
}
}
/** Expands a quantised integer back onto the signal's physical range. */
static double dequantise(uint8_t qt, uint16_t raw, double lo, double hi) {
double span = hi - lo;
switch (qt) {
case UDPS_QUANT_UINT8:
return lo + ((double)(uint8_t)raw / 255.0) * span;
case UDPS_QUANT_INT8:
return lo + (((double)(int8_t)(uint8_t)raw + 127.0) / 254.0) * span;
case UDPS_QUANT_UINT16:
return lo + ((double)raw / 65535.0) * span;
case UDPS_QUANT_INT16:
return lo + (((double)(int16_t)raw + 32767.0) / 65534.0) * span;
default:
return 0.0;
}
}
/** Reads @p n consecutive elements of @p s, advancing @p off. */
static int parse_elems(const uint8_t *pl, size_t len, size_t *off, uint32_t n,
const udps_signal_t *s, double *out) {
int quantised = (s->quant_type != UDPS_QUANT_NONE);
size_t sz = quantised ? quant_size(s->quant_type)
: raw_type_size(s->type_code);
size_t base = *off;
uint32_t i;
if (sz == 0u || base > len) {
return -1;
}
/* Division rather than multiplication: no overflow on a crafted count. */
if ((size_t)n > (len - base) / sz) {
return -1;
}
for (i = 0u; i < n; i++) {
const uint8_t *p = pl + base + (size_t)i * sz;
if (quantised) {
uint16_t raw = (sz == 1u) ? (uint16_t)p[0] : rd_u16(p);
out[i] = dequantise(s->quant_type, raw, s->range_min, s->range_max);
} else {
out[i] = read_raw(p, s->type_code);
}
}
*off = base + (size_t)n * sz;
return 0;
}
static int decode_config(udps_client_t *c, const uint8_t *pl, size_t len) {
uint32_t claimed;
if (len < 4u) {
return fail(c, "CONFIG payload too short (%lu bytes)", (unsigned long)len);
}
claimed = rd_u32(pl);
if (claimed > (len - 4u) / UDPS_SIGNAL_DESC_SIZE || claimed > UDPS_MAX_SIGNALS) {
return fail(c, "CONFIG claims %lu signals, payload holds %lu",
(unsigned long)claimed,
(unsigned long)((len - 4u) / UDPS_SIGNAL_DESC_SIZE));
}
if (ensure_cap((void **)&c->sigs, &c->sigs_cap,
(claimed ? claimed : 1u) * sizeof(udps_signal_t)) != 0) {
return fail(c, "out of memory for %lu signals", (unsigned long)claimed);
}
if (udps_parse_config(pl, len, c->sigs, claimed, &c->num_sigs,
&c->publish_mode) != 0) {
c->num_sigs = 0u;
return fail(c, "malformed CONFIG payload");
}
if (ensure_cap((void **)&c->vals, &c->vals_cap,
(c->num_sigs ? c->num_sigs : 1u) * sizeof(udps_signal_values_t)) != 0) {
c->num_sigs = 0u;
return fail(c, "out of memory for signal value table");
}
c->stats.config_updates++;
if (c->on_config != NULL) {
c->on_config(c->sigs, c->num_sigs, c->publish_mode, c->user);
}
return 0;
}
static int decode_data(udps_client_t *c, const uint8_t *pl, size_t len,
uint32_t counter, double recv_time) {
uint32_t nsamples = 1u;
size_t off = 8u;
size_t total = 0u;
size_t written = 0u;
uint32_t i;
udps_frame_t frame;
if (c->num_sigs == 0u) {
return 0; /* DATA before CONFIG: nothing to decode against. */
}
if (len < 8u) {
return fail(c, "DATA payload too short (%lu bytes)", (unsigned long)len);
}
if (c->publish_mode == UDPS_PUBLISH_ACCUMULATE) {
if (len < 12u) {
return fail(c, "accumulate DATA payload missing sample count");
}
nsamples = rd_u32(pl + 8);
off = 12u;
if (nsamples == 0u) {
return 0;
}
if (nsamples > UDPS_MAX_ELEMENTS) {
return fail(c, "accumulate sample count %lu out of range",
(unsigned long)nsamples);
}
}
for (i = 0u; i < c->num_sigs; i++) {
uint32_t n = udps_signal_num_elements(&c->sigs[i]);
total += (n == 1u) ? nsamples : n;
}
if (ensure_cap((void **)&c->valbuf, &c->valbuf_cap,
(total ? total : 1u) * sizeof(double)) != 0) {
return fail(c, "out of memory for %lu decoded values",
(unsigned long)total);
}
for (i = 0u; i < c->num_sigs; i++) {
uint32_t n = udps_signal_num_elements(&c->sigs[i]);
uint32_t count = (n == 1u) ? nsamples : n;
if (parse_elems(pl, len, &off, count, &c->sigs[i],
c->valbuf + written) != 0) {
return fail(c, "DATA payload truncated at signal '%s'",
c->sigs[i].name);
}
c->vals[i].values = c->valbuf + written;
c->vals[i].count = count;
written += count;
}
if (c->have_counter && counter > c->last_counter + 1u) {
c->stats.counter_gaps += counter - c->last_counter - 1u;
}
c->last_counter = counter;
c->have_counter = 1;
c->stats.frames_delivered++;
if (c->on_data != NULL) {
frame.counter = counter;
frame.hrt = rd_u64(pl);
frame.recv_time = recv_time;
frame.publish_mode = c->publish_mode;
frame.num_samples = nsamples;
frame.num_signals = c->num_sigs;
frame.signals = c->sigs;
frame.values = c->vals;
c->on_data(&frame, c->user);
}
return 0;
}
/*---------------------------------------------------------------------------*/
/* Fragment reassembly */
/*---------------------------------------------------------------------------*/
static void slot_reset_all(udps_client_t *c) {
int i;
for (i = 0; i < UDPS_MAX_SLOTS; i++) {
c->slots[i].active = 0;
}
}
static void gc_slots(udps_client_t *c, double now) {
int i;
for (i = 0; i < UDPS_MAX_SLOTS; i++) {
if (c->slots[i].active && (now - c->slots[i].first_seen) > UDPS_SLOT_STALE_S) {
c->slots[i].active = 0;
c->stats.fragments_dropped += c->slots[i].received_fragments;
}
}
}
static void deliver_payload(udps_client_t *c, uint8_t type, const uint8_t *pl,
size_t len, uint32_t counter, double recv_time) {
if (type == UDPS_PKT_CONFIG) {
(void)decode_config(c, pl, len);
} else {
(void)decode_data(c, pl, len, counter, recv_time);
}
}
/**
* @brief Files one fragment of a multi-fragment update.
*
* Fragments carry no offset, only an index, so placement relies on every
* fragment but the last being the same size — learned from fragment 0.
*/
static void place_fragment(udps_client_t *c, const udps_header_t *h,
const uint8_t *payload, size_t payload_bytes,
double recv_time) {
int slot = -1;
int i;
udps_slot_t *s;
size_t byte_idx;
uint8_t bit;
size_t offset;
if (h->fragment_idx >= h->total_fragments ||
h->total_fragments > UDPS_MAX_FRAGMENTS) {
c->stats.fragments_dropped++;
return;
}
for (i = 0; i < UDPS_MAX_SLOTS; i++) {
if (c->slots[i].active && c->slots[i].counter == h->counter &&
c->slots[i].type == h->type) {
slot = i;
break;
}
}
if (slot < 0) {
double oldest = 0.0;
for (i = 0; i < UDPS_MAX_SLOTS; i++) {
if (!c->slots[i].active) {
slot = i;
break;
}
if (slot < 0 || c->slots[i].first_seen < oldest) {
oldest = c->slots[i].first_seen;
slot = i;
}
}
s = &c->slots[slot];
if (s->active) {
/* All slots busy: the oldest update is never going to complete. */
c->stats.fragments_dropped += s->received_fragments;
}
if (ensure_cap((void **)&s->payload, &s->payload_cap,
c->cfg.max_packet_bytes) != 0) {
(void)fail(c, "out of memory for a %lu byte reassembly buffer",
(unsigned long)c->cfg.max_packet_bytes);
s->active = 0;
return;
}
s->active = 1;
s->counter = h->counter;
s->type = h->type;
s->total_fragments = h->total_fragments;
s->received_fragments = 0u;
s->chunk_size = 0u;
s->assembled_bytes = 0u;
s->first_seen = recv_time;
memset(s->mask, 0, sizeof s->mask);
}
s = &c->slots[slot];
byte_idx = (size_t)h->fragment_idx / 8u;
bit = (uint8_t)(1u << (h->fragment_idx % 8u));
if ((s->mask[byte_idx] & bit) != 0u) {
c->stats.fragments_dropped++; /* duplicate */
return;
}
if (s->chunk_size == 0u) {
if (h->fragment_idx != 0u) {
/* Fragment 0 was lost, so no offset can be computed for this one. */
c->stats.fragments_dropped++;
return;
}
s->chunk_size = (uint32_t)payload_bytes;
}
offset = (size_t)h->fragment_idx * s->chunk_size;
if (offset + payload_bytes > s->payload_cap) {
c->stats.fragments_dropped++;
return;
}
if (payload_bytes > 0u) {
memcpy(s->payload + offset, payload, payload_bytes);
}
if (offset + payload_bytes > s->assembled_bytes) {
s->assembled_bytes = (uint32_t)(offset + payload_bytes);
}
s->mask[byte_idx] |= bit;
s->received_fragments++;
if (s->received_fragments >= s->total_fragments) {
s->active = 0;
deliver_payload(c, s->type, s->payload, s->assembled_bytes, s->counter,
recv_time);
}
}
/** Validates one datagram (or TCP frame) and routes it. */
static void handle_packet(udps_client_t *c, const uint8_t *buf, size_t len) {
udps_header_t h;
double recv_time = now_wall();
if (udps_parse_header(buf, len, &h) != 0) {
return; /* Not ours: stray traffic on a shared multicast port. */
}
if (h.type != UDPS_PKT_DATA && h.type != UDPS_PKT_CONFIG) {
return;
}
if ((size_t)h.payload_bytes + UDPS_HEADER_SIZE > len) {
return; /* truncated */
}
c->stats.packets_received++;
c->stats.bytes_received += len;
if (h.total_fragments <= 1u) {
deliver_payload(c, h.type, buf + UDPS_HEADER_SIZE, h.payload_bytes,
h.counter, recv_time);
} else {
place_fragment(c, &h, buf + UDPS_HEADER_SIZE, h.payload_bytes, recv_time);
}
}
/*---------------------------------------------------------------------------*/
/* Transport */
/*---------------------------------------------------------------------------*/
/** Resolves a dotted quad, falling back to a name lookup. */
static int resolve_ipv4(const char *host, struct in_addr *out) {
struct addrinfo hints;
struct addrinfo *res = NULL;
if (inet_pton(AF_INET, host, out) == 1) {
return 0;
}
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
if (getaddrinfo(host, NULL, &hints, &res) != 0 || res == NULL) {
return -1;
}
*out = ((struct sockaddr_in *)(void *)res->ai_addr)->sin_addr;
freeaddrinfo(res);
return 0;
}
static void set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags >= 0) {
(void)fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
}
/** Raises SO_RCVBUF; a small kernel buffer silently drops burst traffic. */
static void set_recv_buffer(int fd, uint32_t bytes) {
int v = (int)bytes;
if (v > 0) {
(void)setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &v, (socklen_t)sizeof v);
}
}
static int send_control(udps_client_t *c, uint8_t type) {
uint8_t pkt[UDPS_HEADER_SIZE];
ssize_t n;
build_header(pkt, type, 0u, 0u, 1u, 0u);
if (c->tcp_fd >= 0) {
n = send(c->tcp_fd, pkt, sizeof pkt, 0);
} else if (c->udp_fd >= 0) {
n = sendto(c->udp_fd, pkt, sizeof pkt, 0,
(struct sockaddr *)&c->server_sa, (socklen_t)sizeof c->server_sa);
} else {
return -1;
}
return (n == (ssize_t)sizeof pkt) ? 0 : -1;
}
static void close_sockets(udps_client_t *c) {
if (c->udp_fd >= 0) {
(void)close(c->udp_fd);
c->udp_fd = -1;
}
if (c->tcp_fd >= 0) {
(void)close(c->tcp_fd);
c->tcp_fd = -1;
}
}
static int connect_unicast(udps_client_t *c) {
struct sockaddr_in local;
c->udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (c->udp_fd < 0) {
return fail(c, "socket() failed: %s", strerror(errno));
}
set_recv_buffer(c->udp_fd, c->cfg.recv_buffer_bytes);
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_port = 0; /* ephemeral: the server replies to this port */
if (bind(c->udp_fd, (struct sockaddr *)&local, (socklen_t)sizeof local) != 0) {
int e = errno;
close_sockets(c);
return fail(c, "bind() failed: %s", strerror(e));
}
set_nonblocking(c->udp_fd);
if (send_control(c, UDPS_PKT_CONNECT) != 0) {
int e = errno;
close_sockets(c);
return fail(c, "CONNECT to %s:%u failed: %s", c->server_addr,
(unsigned)c->cfg.server_port, strerror(e));
}
return 0;
}
static int connect_multicast(udps_client_t *c) {
struct sockaddr_in local;
struct ip_mreq mreq;
struct timeval tv;
int on = 1;
/* Join before announcing: the server multicasts CONFIG the moment it sees
* CONNECT, and a group we have not joined yet drops it in the kernel. */
c->udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (c->udp_fd < 0) {
return fail(c, "socket() failed: %s", strerror(errno));
}
(void)setsockopt(c->udp_fd, SOL_SOCKET, SO_REUSEADDR, &on, (socklen_t)sizeof on);
set_recv_buffer(c->udp_fd, c->cfg.recv_buffer_bytes);
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_port = htons(c->cfg.data_port);
if (bind(c->udp_fd, (struct sockaddr *)&local, (socklen_t)sizeof local) != 0) {
int e = errno;
close_sockets(c);
return fail(c, "bind() on data port %u failed: %s",
(unsigned)c->cfg.data_port, strerror(e));
}
memset(&mreq, 0, sizeof mreq);
if (resolve_ipv4(c->mcast_group, &mreq.imr_multiaddr) != 0) {
close_sockets(c);
return fail(c, "bad multicast group '%s'", c->mcast_group);
}
if (c->iface_addr[0] != '\0') {
if (resolve_ipv4(c->iface_addr, &mreq.imr_interface) != 0) {
close_sockets(c);
return fail(c, "bad interface address '%s'", c->iface_addr);
}
} else {
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
}
if (setsockopt(c->udp_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq,
(socklen_t)sizeof mreq) != 0) {
int e = errno;
close_sockets(c);
return fail(c, "joining %s failed: %s", c->mcast_group, strerror(e));
}
set_nonblocking(c->udp_fd);
/* Control channel: CONNECT out, CONFIG back. */
c->tcp_fd = socket(AF_INET, SOCK_STREAM, 0);
if (c->tcp_fd < 0) {
int e = errno;
close_sockets(c);
return fail(c, "TCP socket() failed: %s", strerror(e));
}
if (connect(c->tcp_fd, (struct sockaddr *)&c->server_sa,
(socklen_t)sizeof c->server_sa) != 0) {
int e = errno;
close_sockets(c);
return fail(c, "TCP connect to %s:%u failed: %s", c->server_addr,
(unsigned)c->cfg.server_port, strerror(e));
}
/* Bound wait so a half-received control frame cannot wedge the poll loop. */
tv.tv_sec = 1;
tv.tv_usec = 0;
(void)setsockopt(c->tcp_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, (socklen_t)sizeof tv);
if (send_control(c, UDPS_PKT_CONNECT) != 0) {
int e = errno;
close_sockets(c);
return fail(c, "CONNECT over TCP failed: %s", strerror(e));
}
return 0;
}
static int do_connect(udps_client_t *c) {
int rc = (c->mcast_group[0] != '\0') ? connect_multicast(c)
: connect_unicast(c);
if (rc != 0) {
c->disconnect_t = now_mono();
return rc;
}
c->connected = 1;
c->last_data = now_mono();
c->last_keepalive = c->last_data;
c->have_counter = 0;
slot_reset_all(c);
if (c->ever_connected) {
c->stats.reconnects++;
}
c->ever_connected = 1;
emit_event(c, UDPS_EVENT_CONNECTED, c->server_addr);
return 0;
}
static void do_disconnect(udps_client_t *c, const char *why) {
if (!c->connected) {
return;
}
(void)send_control(c, UDPS_PKT_DISCONNECT);
close_sockets(c);
c->connected = 0;
c->disconnect_t = now_mono();
slot_reset_all(c);
emit_event(c, UDPS_EVENT_DISCONNECTED, why);
}
/** Reads exactly @p n bytes from the control connection. */
static int read_exact_tcp(udps_client_t *c, uint8_t *dst, size_t n) {
size_t got = 0u;
while (got < n) {
ssize_t r = recv(c->tcp_fd, dst + got, n - got, 0);
if (r == 0) {
return -1; /* orderly close */
}
if (r < 0) {
if (errno == EINTR) {
continue;
}
return -1;
}
got += (size_t)r;
}
return 0;
}
/** Reads one framed UDPS packet off the TCP control connection. */
static int read_tcp_frame(udps_client_t *c) {
udps_header_t h;
if (read_exact_tcp(c, c->rxbuf, UDPS_HEADER_SIZE) != 0) {
return -1;
}
if (udps_parse_header(c->rxbuf, UDPS_HEADER_SIZE, &h) != 0) {
return -1; /* desynchronised: the stream cannot be resynced */
}
if (h.payload_bytes > (uint32_t)(UDPS_RX_BUF_BYTES - UDPS_HEADER_SIZE)) {
return -1;
}
if (h.payload_bytes > 0u &&
read_exact_tcp(c, c->rxbuf + UDPS_HEADER_SIZE, h.payload_bytes) != 0) {
return -1;
}
handle_packet(c, c->rxbuf, UDPS_HEADER_SIZE + h.payload_bytes);
return 0;
}
/*---------------------------------------------------------------------------*/
/* Public client API */
/*---------------------------------------------------------------------------*/
void udps_client_config_init(udps_client_config_t *cfg) {
if (cfg == NULL) {
return;
}
memset(cfg, 0, sizeof *cfg);
cfg->silence_timeout_s = 1.0;
cfg->reconnect_delay_s = 2.0;
cfg->keepalive_interval_s = 15.0; /* server evicts silent clients at 30 s */
cfg->recv_buffer_bytes = 4u * 1024u * 1024u;
cfg->max_packet_bytes = 1u * 1024u * 1024u;
}
static void copy_str(char *dst, size_t cap, const char *src) {
if (src == NULL) {
dst[0] = '\0';
return;
}
strncpy(dst, src, cap - 1u);
dst[cap - 1u] = '\0';
}
udps_client_t *udps_client_create(const udps_client_config_t *cfg) {
udps_client_t *c;
if (cfg == NULL || cfg->server_addr == NULL || cfg->server_port == 0u) {
return NULL;
}
c = (udps_client_t *)calloc(1u, sizeof *c);
if (c == NULL) {
return NULL;
}
c->cfg = *cfg;
c->udp_fd = -1;
c->tcp_fd = -1;
copy_str(c->server_addr, sizeof c->server_addr, cfg->server_addr);
copy_str(c->mcast_group, sizeof c->mcast_group, cfg->multicast_group);
copy_str(c->iface_addr, sizeof c->iface_addr, cfg->interface_addr);
/* The config's string pointers must not be used again: they belong to the
* caller and the copies above are what the client works from. */
c->cfg.server_addr = c->server_addr;
c->cfg.multicast_group = (c->mcast_group[0] != '\0') ? c->mcast_group : NULL;
c->cfg.interface_addr = (c->iface_addr[0] != '\0') ? c->iface_addr : NULL;
if (c->cfg.max_packet_bytes == 0u) {
c->cfg.max_packet_bytes = 1u * 1024u * 1024u;
}
if (c->cfg.data_port == 0u) {
c->cfg.data_port = (uint16_t)(cfg->server_port + 1u);
}
memset(&c->server_sa, 0, sizeof c->server_sa);
c->server_sa.sin_family = AF_INET;
c->server_sa.sin_port = htons(cfg->server_port);
if (resolve_ipv4(c->server_addr, &c->server_sa.sin_addr) != 0) {
free(c);
return NULL;
}
c->rxbuf = (uint8_t *)malloc(UDPS_RX_BUF_BYTES);
if (c->rxbuf == NULL) {
free(c);
return NULL;
}
strcpy(c->err, "no error");
return c;
}
void udps_client_destroy(udps_client_t *c) {
int i;
if (c == NULL) {
return;
}
do_disconnect(c, "closed by application");
close_sockets(c);
for (i = 0; i < UDPS_MAX_SLOTS; i++) {
free(c->slots[i].payload);
}
free(c->sigs);
free(c->valbuf);
free(c->vals);
free(c->rxbuf);
free(c);
}
void udps_client_set_callbacks(udps_client_t *c, udps_config_cb on_config,
udps_data_cb on_data, udps_event_cb on_event,
void *user) {
if (c == NULL) {
return;
}
c->on_config = on_config;
c->on_data = on_data;
c->on_event = on_event;
c->user = user;
}
int udps_client_is_connected(const udps_client_t *c) {
return (c != NULL) && c->connected;
}
const udps_signal_t *udps_client_signals(const udps_client_t *c,
uint32_t *num_signals) {
if (c == NULL) {
if (num_signals != NULL) {
*num_signals = 0u;
}
return NULL;
}
if (num_signals != NULL) {
*num_signals = c->num_sigs;
}
return (c->num_sigs > 0u) ? c->sigs : NULL;
}
uint8_t udps_client_publish_mode(const udps_client_t *c) {
return (c != NULL) ? c->publish_mode : (uint8_t)UDPS_PUBLISH_STRICT;
}
void udps_client_stats(const udps_client_t *c, udps_stats_t *out) {
if (c != NULL && out != NULL) {
*out = c->stats;
}
}
const char *udps_client_last_error(const udps_client_t *c) {
return (c != NULL) ? c->err : "invalid client";
}
/** Clamps @p budget so a pending deadline is not slept through. */
static double clamp_deadline(double budget, double interval, double elapsed) {
double left;
if (interval <= 0.0) {
return budget;
}
left = interval - elapsed;
if (left < 0.0) {
left = 0.0;
}
return (left < budget) ? left : budget;
}
int udps_client_poll(udps_client_t *c, int timeout_ms) {
double now;
double budget;
fd_set rset;
struct timeval tv;
int maxfd;
int nready;
int processed = 0;
if (c == NULL) {
return -1;
}
if (!c->connected) {
now = now_mono();
if (c->disconnect_t > 0.0) {
double wait = c->cfg.reconnect_delay_s - (now - c->disconnect_t);
if (wait > 0.0) {
/* Idle out the retry delay rather than hammering the server. */
double cap = (timeout_ms < 0) ? wait : (double)timeout_ms / 1000.0;
sleep_s((wait < cap) ? wait : cap);
return 0;
}
}
if (do_connect(c) != 0) {
return -1;
}
}
now = now_mono();
budget = (timeout_ms < 0) ? 1.0 : (double)timeout_ms / 1000.0;
/* Only unicast sends keepalives; clamping on them in multicast mode would
* spin on a deadline that never gets refreshed. */
if (c->tcp_fd < 0) {
budget = clamp_deadline(budget, c->cfg.keepalive_interval_s,
now - c->last_keepalive);
}
budget = clamp_deadline(budget, c->cfg.silence_timeout_s, now - c->last_data);
FD_ZERO(&rset);
maxfd = -1;
if (c->udp_fd >= 0 && c->udp_fd < FD_SETSIZE) {
FD_SET(c->udp_fd, &rset);
maxfd = c->udp_fd;
}
if (c->tcp_fd >= 0 && c->tcp_fd < FD_SETSIZE) {
FD_SET(c->tcp_fd, &rset);
if (c->tcp_fd > maxfd) {
maxfd = c->tcp_fd;
}
}
if (maxfd < 0) {
do_disconnect(c, "no usable socket");
return fail(c, "socket descriptor outside FD_SETSIZE");
}
tv.tv_sec = (time_t)budget;
tv.tv_usec = (suseconds_t)((budget - (double)tv.tv_sec) * 1e6);
nready = select(maxfd + 1, &rset, NULL, NULL, &tv);
if (nready < 0) {
if (errno == EINTR) {
return 0;
}
do_disconnect(c, "select failed");
return fail(c, "select() failed: %s", strerror(errno));
}
if (nready > 0 && c->tcp_fd >= 0 && FD_ISSET(c->tcp_fd, &rset)) {
if (read_tcp_frame(c) != 0) {
do_disconnect(c, "control connection lost");
return fail(c, "TCP control connection lost");
}
c->last_data = now_mono();
processed++;
}
if (nready > 0 && c->udp_fd >= 0 && FD_ISSET(c->udp_fd, &rset)) {
int drained = 0;
while (drained < UDPS_DRAIN_LIMIT) {
ssize_t n = recv(c->udp_fd, c->rxbuf, UDPS_RX_BUF_BYTES, 0);
if (n < 0) {
if (errno == EINTR) {
continue;
}
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break; /* socket drained */
}
do_disconnect(c, "receive failed");
return fail(c, "recv() failed: %s", strerror(errno));
}
drained++;
c->last_data = now_mono();
if ((size_t)n >= UDPS_HEADER_SIZE) {
handle_packet(c, c->rxbuf, (size_t)n);
}
}
processed += drained;
}
now = now_mono();
if (c->cfg.keepalive_interval_s > 0.0 && c->tcp_fd < 0 &&
(now - c->last_keepalive) >= c->cfg.keepalive_interval_s) {
/* ACK, not CONNECT: it refreshes the server's last-seen without making
* it resend CONFIG. */
(void)send_control(c, UDPS_PKT_ACK);
c->last_keepalive = now;
}
gc_slots(c, now_wall());
if (c->cfg.silence_timeout_s > 0.0 &&
(now - c->last_data) >= c->cfg.silence_timeout_s) {
do_disconnect(c, "server went silent");
return fail(c, "no data for %.3f s", c->cfg.silence_timeout_s);
}
return processed;
}