feat(tcp): TCPStreamer DataSources, in-process TCPStreamService, stream multiplexing and TCPStreamJoin

Add TCPStreamer/TCPStreamerClient (TCP transport behind the UDPSServerI/UDPSClientI
seam), an optional single in-process TCPStreamService serving many streams over
worker threads, multiplexed SUBSCRIBE (comma list, stream index in fragmentIdx),
and TCPStreamJoin, which merges several streams into one sample-and-hold stream
paced by a master. StreamHub takes a comma list of streams over one connection.
Includes GTests, E2E scenarios s58-s73 and docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-11-06 13:41:27 +01:00
1 parent 5562877c99
commit 50d85363a3
79 files changed
+12348 -138

No files matched your search

@@ -0,0 +1,160 @@
#ifndef TCPS_SERVER_H_
#define TCPS_SERVER_H_
/**
* @file TCPSServer.h
* @brief TCP transport for the UDPS wire format (producer side).
*
* TCPSServer carries the normal UDPS framing (17-byte header + CONFIG / DATA
* payload, totalFragments always 1) over TCP byte streams. It implements
* UDPSServerI so UDPStreamer can publish through it unchanged.
*
* Threading model: NOT thread-safe, exactly like UDPSServer. Every call must
* come from the owner's sender thread. There is no thread per client: sockets
* are non-blocking and everything is driven from ServiceClients() / SendData().
*
* Slow consumers: every client owns a bounded queue of ClientQueueDepth DATA
* frames. When it is full the oldest queued DATA frame that is not already
* (partially) on the wire is discarded and counted; other clients and the
* producer are never affected. A client that makes no progress for
* ClientStallTimeout seconds with data queued is disconnected.
*
* Ordering: a client always sees CONFIG before any DATA. A CONFIG is never
* dropped. When a new CONFIG is published, the frames queued behind the one
* currently on the wire are stale (encoded for the old layout) and are
* discarded, so DATA is never decoded against the wrong CONFIG.
*/
#include "StructuredDataI.h"
#include "UDPSProtocol.h"
#include "UDPSTransportI.h"
namespace MARTe {
class TCPSServer : public UDPSServerI {
public:
/** Default TCP listen port. */
static const uint32 TCPS_DEFAULT_PORT = 44500u;
/** Default maximum payload bytes per frame. */
static const uint32 TCPS_DEFAULT_MAX_PAYLOAD = 60000u;
/** Hard ceiling on a frame payload (matches UDPSClient's packet limit). */
static const uint32 TCPS_MAX_FRAME_PAYLOAD = 1048576u;
/** Default simultaneous clients. */
static const uint32 TCPS_DEFAULT_MAX_CLIENTS = 16u;
/** Upper bound accepted for MaxClients. */
static const uint32 TCPS_MAX_CLIENTS_LIMIT = 1024u;
/** Default DATA frames queued per client. */
static const uint32 TCPS_DEFAULT_QUEUE_DEPTH = 8u;
/** Upper bound accepted for ClientQueueDepth. */
static const uint32 TCPS_QUEUE_DEPTH_LIMIT = 4096u;
/** Default stall timeout (seconds); 0 disables. */
static const uint32 TCPS_DEFAULT_STALL_TIMEOUT_S = 5u;
TCPSServer();
virtual ~TCPSServer();
/**
* @brief Read configuration.
*
* Keys (all optional):
* - Port (uint32) TCP listen port, default 44500. 0 picks an
* ephemeral port (see GetPort() after Start()).
* - MaxPayloadSize (uint32) 1..1 MiB, default 60000. Sizes Accumulate
* batches in the owner; frames larger than 1 MiB are
* refused by SendData().
* - MaxClients (uint32) 1..1024, default 16. Extra connections
* are accepted and closed immediately.
* - ClientQueueDepth (uint32) 1..4096, default 8.
* - ClientStallTimeout (uint32) Seconds, default 5; 0 disables.
* - SendBufferSize (uint32) SO_SNDBUF for client sockets; 0 (default)
* keeps the OS default.
*/
virtual bool Initialise(StructuredDataI &data);
/** Open the listening socket (idempotent). */
virtual bool Start();
/** Close every socket and free all buffers (idempotent). */
virtual bool Stop();
/**
* Accept new clients, drop closed ones, flush queues, evict stalled
* clients. Never blocks.
*/
virtual void ServiceClients();
/**
* Cache the CONFIG and queue it on every connected client. New clients
* receive the cached CONFIG on accept.
*/
virtual bool SendConfig(const uint8 *payload, uint32 payloadSize);
/**
* Queue a DATA frame on every connected client and try to flush.
* @return false only if the frame is larger than TCPS_MAX_FRAME_PAYLOAD.
*/
virtual bool SendData(uint32 counter, const uint8 *payload, uint32 payloadSize);
virtual bool HasClients() const;
virtual bool IsMulticast() const;
/** @return the bound port once started, otherwise the configured port. */
virtual uint16 GetPort() const;
/** @return number of connected clients. */
uint32 GetClientCount() const;
/** @return DATA frames discarded (all clients, all time) because a client queue was full. */
uint64 GetClientDrops() const;
/** @return clients disconnected by the stall timeout (all time). */
uint32 GetStallEvictions() const;
uint32 GetMaxPayloadSize() const;
private:
struct Slot;
struct Client;
bool CacheConfig(const uint8 *payload, uint32 payloadSize);
void AcceptPending();
bool Enqueue(Client &c, bool isConfig, const uint8 *frame, uint32 frameSize);
bool Flush(Client &c);
bool PeerAlive(Client &c);
void Evict(Client &c);
bool BuildFrame(uint8 type, uint32 counter, const uint8 *payload, uint32 payloadSize);
void ReportDrops();
uint32 port;
uint16 boundPort;
uint32 maxPayloadSize;
uint32 maxClients;
uint32 queueDepth;
uint32 stallTimeoutS;
uint32 sendBufferSize;
int listenFd;
bool started;
Client *clients;
uint32 numClients;
uint8 *cachedConfig;
uint32 cachedConfigSize;
uint32 configCounter;
uint8 *frameBuf; ///< scratch: header + payload built once per call
uint32 frameCapacity;
uint32 frameSize;
uint64 stallTicks;
uint64 totalDrops;
uint64 loggedDrops;
uint64 lastDropLogTicks;
uint32 stallEvictions;
};
} // namespace MARTe
#endif // TCPS_SERVER_H_