|
CX Framework
Cross-platform C utility framework
|
Data Structures | |
| struct | NetFlow |
| A single ordering domain: one connection, or one datagram peer. More... | |
Macros | |
| #define | netflowSetHandlers(self, handlers, ctx) NetFlow_setHandlers(NetFlow(self), handlers, ctx) |
| #define | netflowSetHandlersObj(self, handlers, ctx) NetFlow_setHandlersObj(NetFlow(self), handlers, ObjInst(ctx)) |
| #define | netflowAddTimer(self, delay, flags) NetFlow_addTimer(NetFlow(self), delay, flags) |
| #define | netflowCancelTimer(self, id) NetFlow_cancelTimer(NetFlow(self), id) |
| #define | netflowRearmTimer(self, id, delay) NetFlow_rearmTimer(NetFlow(self), id, delay) |
| #define | netflowClose(self) (self)->_->close(NetFlow(self)) |
| #define | netflowSend(self, data, len, flags) (self)->_->send(NetFlow(self), data, len, flags) |
Typedefs | |
| typedef struct NetFlow | NetFlow |
| A single ordering domain: one connection, or one datagram peer. | |
A flow is the unit of event ordering.
For a TCP socket, the obvious ordering domain is the socket itself, and that works fine. It falls apart for a single UDP socket serving many clients: if the socket were the ordering domain, every client's events would serialize behind one another and the thread pool would sit idle. So the ordering domain is the flow instead:
The guarantee is: events belonging to one flow are strictly ordered and never run concurrently. Events on different flows may run at the same time on different workers, and nothing is promised about ordering between flows.
Both socket types dispatch through the same mechanism, so there is one code path rather than two. For a single stream connection, the flow is created automatically during socket setup and shows up only as a field on the event; consumers that only care about one connection can ignore it entirely.
A datagram flow is created automatically for the first packet from a new peer or the first packet sent to one, or explicitly with netqueuePromoteFlow(). Any of those fires NET_FlowOpen on the new flow, ordered ahead of its first NET_DataReceived – the place to set up flow->user state and register per-flow handlers before any packet is seen. When the queue is at its flow cap and nothing is reclaimable, no flow is created: a received packet goes to the NET_FlowRefused handler instead, which can validate it and admit the peer with netqueuePromoteFlow(), and a send to an unknown peer is refused.
Stream flows do not fire NET_FlowOpen; their session start is NET_Connection or NET_Accepted.
However a flow comes into being, if its socket has filters attached the flow's own filter chain is built before the flow becomes visible, so no data can ever reach the application unfiltered. See Filters.
flow->user is the place for per-peer application state – strings, buffers, objects, anything with a destructor. Because the flow is a real object, its generated destructor tears that state down automatically, so there's no need to hand-write cleanup for every way a flow can end.
Only flows carry a user array. Per-socket and per-queue state should go in the ctx pointer captured at handler registration instead, because sockets and queues don't have the same one-worker-at-a-time guarantee a flow does – a single datagram socket can have thousands of flows running on every worker at once, so a shared array there would need its own locking.
A flow is also the unit of timing. netflowAddTimer() arms a deadline and NET_Timer is delivered on the flow when it elapses – on a worker, ordered behind everything already pending, exactly like a packet.
Timers belong to the flow's lifetime as well as its ordering: teardown cancels whatever is still armed, so no NET_Timer follows NET_FlowClosed.
NET_FlowClosed is delivered as a terminal event on the flow's own queue, so it arrives after every packet the application has already been handed. It fires exactly once per flow, on every close cause, with the cause given in NetCloseReason.
A flow reclaimed under cap pressure can resurrect: if a packet arrives before its terminal event has been delivered, the flow comes back to life and continues as if nothing happened. This avoids tearing down and immediately recreating a client that never actually left. Only reclaim works this way – every other close reason is final, and a later packet cannot undo it.
| #define netflowAddTimer | ( | self, | |
| delay, | |||
| flags | |||
| ) | NetFlow_addTimer(NetFlow(self), delay, flags) |
NetTimerId netflowAddTimer(NetFlow* self, int64 delay, flags_t flags);
Arm a timer on this flow
NetEvent.timer.id on the delivered NET_Timer carries the returned id, so one handler can tell several timers apart.
| delay | Microseconds from now until the timer fires (see timeS() / timeMS()) |
| flags | Optional NetTimerFlags (NTF_Repeat to re-arm automatically) |
Example:
| #define netflowCancelTimer | ( | self, | |
| id | |||
| ) | NetFlow_cancelTimer(NetFlow(self), id) |
bool netflowCancelTimer(NetFlow* self, NetTimerId id);
Cancel a timer armed on this flow
| id | Timer id from netflowAddTimer() |
| #define netflowClose | ( | self | ) | (self)->_->close(NetFlow(self)) |
bool netflowClose(NetFlow* self);
Close this flow
Marks the flow dying and queues a terminal NET_FlowClosed event behind everything already pending for it. The flow is not freed here; it stays alive until its queued packets and its terminal event have been delivered.
A flow closed this way cannot resurrect – an arriving packet will not cancel the teardown, and the peer is treated as a new, unknown source afterwards.
| #define netflowRearmTimer | ( | self, | |
| id, | |||
| delay | |||
| ) | NetFlow_rearmTimer(NetFlow(self), id, delay) |
bool netflowRearmTimer(NetFlow* self, NetTimerId id, int64 delay);
Move an armed timer's deadline forward, keeping its id
| id | Timer id from netflowAddTimer() |
| delay | Microseconds from now until the timer fires |
| #define netflowSend | ( | self, | |
| data, | |||
| len, | |||
| flags | |||
| ) | (self)->_->send(NetFlow(self), data, len, flags) |
bool netflowSend(NetFlow* self, const uint8* data, size_t len, flags_t flags);
Send data on this flow
For a datagram flow this is a convenience wrapper that calls netsocketSend() on the owning socket with this flow's peer as the destination – filtering, backpressure, and queueing are exactly netsocketSend()'s. A QUIC stream flow sends on its stream instead, since a stream is named by its id and not by an address.
| data | Payload to send (copied; need not outlive the call) |
| len | Length of the payload in bytes |
| flags | Optional NetSocketOpFlags |
| #define netflowSetHandlers | ( | self, | |
| handlers, | |||
| ctx | |||
| ) | NetFlow_setHandlers(NetFlow(self), handlers, ctx) |
void netflowSetHandlers(NetFlow* self, const NetHandlers* handlers, void* ctx);
Register per-flow handler overrides
Fields left NULL fall through to the socket's set, and then to the queue-wide set. The handler struct is not copied – it must outlive the flow, which is why it is usually static const.
| handlers | Handler set, or NULL to clear |
| ctx | Context passed to these handlers on NetEvent.ctx |
| #define netflowSetHandlersObj | ( | self, | |
| handlers, | |||
| ctx | |||
| ) | NetFlow_setHandlersObj(NetFlow(self), handlers, ObjInst(ctx)) |
void netflowSetHandlersObj(NetFlow* self, const NetHandlers* handlers, ObjInst* ctx);
Register per-flow handler overrides, with an object as the context
Same as setHandlers(), except ctx is held weakly rather than borrowed.
| handlers | Handler set, or NULL to clear |
| ctx | Object passed to these handlers on NetEvent.ctx, held weakly; NULL to clear |