Skip to main content

monocoque_zmtp/
stream.rs

1//! STREAM socket  -  raw TCP bridging without ZMTP handshake.
2//!
3//! STREAM sockets bridge ZeroMQ traffic to plain (non-ZMTP) TCP peers such as
4//! HTTP servers, legacy protocols, and `nc`/`curl` clients.
5//!
6//! ## Message format
7//!
8//! ### Received messages (inbound from TCP peer)
9//! ```text
10//! Frame 0: routing-id  (8 bytes, uniquely identifies the TCP connection)
11//! Frame 1: empty       (separator, matches ROUTER convention)
12//! Frame 2: data        (raw bytes from the TCP stream)
13//! ```
14//!
15//! **Connection notifications** arrive with an empty data frame:
16//! - On connect: `[routing_id, "", ""]`
17//! - On disconnect: `[routing_id, "", ""]`
18//!
19//! ### Sent messages (outbound to TCP peer)
20//! ```text
21//! Frame 0: routing-id  (selects the destination peer)
22//! Frame 1: empty       (ignored / stripped)
23//! Frame 2: data        (raw bytes written to that peer's TCP stream)
24//! ```
25//!
26//! ## Example
27//!
28//! ```rust,no_run
29//! use monocoque_zmtp::stream::StreamSocket;
30//! use bytes::Bytes;
31//!
32//! # async fn example() -> std::io::Result<()> {
33//! let mut srv = StreamSocket::bind("127.0.0.1:5555").await?;
34//!
35//! // Accept one raw TCP connection.
36//! let peer_id = srv.accept_raw().await?;
37//!
38//! // Receive data (or a connection notification).
39//! while let Some(msg) = srv.recv().await? {
40//!     let routing_id = &msg[0];
41//!     let data       = &msg[2];
42//!     if data.is_empty() {
43//!         // connection / disconnection notification
44//!         continue;
45//!     }
46//!     // Echo back
47//!     srv.send(vec![routing_id.clone(), Bytes::new(), data.clone()]).await?;
48//! }
49//! # Ok(())
50//! # }
51//! ```
52
53use bytes::{Bytes, BytesMut};
54use flume::{Receiver, Sender};
55use monocoque_core::options::SocketOptions;
56use monocoque_core::rt::{OwnedReadHalf, OwnedWriteHalf, TcpListener};
57use std::collections::HashMap;
58use std::io;
59use std::sync::Arc;
60use std::sync::atomic::{AtomicU64, Ordering};
61use tracing::{debug, trace, warn};
62
63// ─────────────────────────────────────────────────────────────────────────────
64// Internal types
65// ─────────────────────────────────────────────────────────────────────────────
66
67/// A routing ID is a unique 8-byte handle per TCP connection.
68type RoutingId = Bytes;
69
70/// Messages flowing from peer-reader tasks to the application.
71type InboundMsg = Vec<Bytes>; // [routing_id, empty, data]
72
73// ─────────────────────────────────────────────────────────────────────────────
74// Background tasks
75// ─────────────────────────────────────────────────────────────────────────────
76
77/// Reads raw bytes from a TCP connection and forwards them to the inbound channel.
78///
79/// Sends a connection notification `[id, "", ""]` before the first byte, and a
80/// disconnect notification `[id, "", ""]` when the connection closes.
81async fn peer_reader(
82    routing_id: RoutingId,
83    mut reader: OwnedReadHalf,
84    inbound: Sender<InboundMsg>,
85    read_buffer_size: usize,
86    shutdown: Receiver<()>,
87) {
88    use compio_buf::BufResult;
89    use compio_io::AsyncRead;
90    use futures::{FutureExt, select_biased};
91    use monocoque_core::io::take_read_buffer;
92
93    // Connection notification. send_async blocks when the inbound channel is at
94    // its HWM, which throttles readers into TCP backpressure (finding B).
95    let _ = inbound
96        .send_async(vec![routing_id.clone(), Bytes::new(), Bytes::new()])
97        .await;
98
99    // Lazily grown on the first read, so a connected-but-silent peer holds no
100    // read slab.
101    let mut read_buf = BytesMut::new();
102    loop {
103        // SAFETY: `buf` is passed straight to `read`; the data path truncates
104        // it to `n` before freezing, and EOF/error drop it without reading it.
105        let buf = unsafe { take_read_buffer(&mut read_buf, read_buffer_size) };
106
107        // Multiplex the read against the shutdown signal so close_peer() or
108        // dropping the socket cancels this task promptly, releasing the read fd
109        // instead of blocking on read until the remote closes.
110        //
111        // select_biased! with the shutdown branch first makes cancellation
112        // deterministic: once shutdown is signalled, we stop even if a read has
113        // also become ready, so no further frame is delivered after close_peer.
114        // (An unbiased select could pick the ready read and leak one more frame.)
115        let BufResult(result, mut buf) = select_biased! {
116            _ = shutdown.recv_async().fuse() => {
117                debug!("[STREAM] Peer {:?} reader cancelled", routing_id);
118                break;
119            }
120            res = reader.read(buf).fuse() => res,
121        };
122        match result {
123            Ok(0) => {
124                debug!("[STREAM] Peer {:?} disconnected (EOF)", routing_id);
125                break;
126            }
127            Ok(n) => {
128                debug_assert!(n <= read_buffer_size);
129                buf.truncate(n);
130                let data = buf.freeze();
131                trace!("[STREAM] Received {} bytes from peer {:?}", n, routing_id);
132                let msg = vec![routing_id.clone(), Bytes::new(), data];
133                if inbound.send_async(msg).await.is_err() {
134                    break; // socket dropped
135                }
136            }
137            Err(e) => {
138                debug!("[STREAM] Peer {:?} read error: {}", routing_id, e);
139                break;
140            }
141        }
142    }
143
144    // Disconnect notification
145    let _ = inbound.try_send(vec![routing_id, Bytes::new(), Bytes::new()]);
146}
147
148/// Per-peer routing-table entry.
149///
150/// Holds the outbound sender (feeds the writer task) and a shutdown sender.
151/// Dropping this entry (via [`StreamSocket::close_peer`], `disconnect`, or the
152/// socket itself being dropped) closes both channels: the writer's `recv`
153/// returns `Err` and the reader's shutdown branch fires, so both background
154/// tasks exit and release their TCP halves and fds.
155struct PeerHandle {
156    out_tx: Sender<Bytes>,
157    /// Dropping this cancels the peer's reader task.
158    _shutdown: Sender<()>,
159}
160
161/// Writes raw bytes from the per-peer send channel to the TCP connection.
162async fn peer_writer(mut writer: OwnedWriteHalf, outbound: Receiver<Bytes>) {
163    use compio_buf::BufResult;
164    use compio_io::AsyncWriteExt;
165
166    while let Ok(data) = outbound.recv_async().await {
167        let BufResult(res, _) = writer.write_all(data).await;
168        if res.is_err() {
169            break;
170        }
171    }
172}
173
174// ─────────────────────────────────────────────────────────────────────────────
175// StreamSocket
176// ─────────────────────────────────────────────────────────────────────────────
177
178/// STREAM socket  -  raw TCP bridging without ZMTP handshake.
179///
180/// Accepts plain TCP connections and multiplexes them through a ZeroMQ-style
181/// routing-ID interface.  Each accepted connection is assigned a unique 8-byte
182/// routing ID; all subsequent sends and receives for that connection use the
183/// same ID to route messages.
184///
185/// Unlike other socket types, `StreamSocket` performs **no ZMTP handshake**  -
186/// it speaks plain TCP bytes, making it suitable for bridging to HTTP servers,
187/// legacy services, and command-line tools such as `nc` and `curl`.
188pub struct StreamSocket {
189    /// TCP listener (held after `bind`, until dropped).
190    listener: TcpListener,
191    /// Channel from background reader tasks to the application.
192    inbound_rx: Receiver<InboundMsg>,
193    /// Shared sender half for reader tasks.
194    inbound_tx: Sender<InboundMsg>,
195    /// Per-peer routing table (routing_id → outbound + shutdown handles).
196    peers: HashMap<RoutingId, PeerHandle>,
197    /// Monotonically increasing routing-ID counter.
198    next_id: Arc<AtomicU64>,
199    /// Socket options.
200    options: SocketOptions,
201}
202
203impl StreamSocket {
204    /// Bind a STREAM socket to a TCP address.
205    ///
206    /// The returned socket is ready to accept raw (non-ZMTP) TCP connections
207    /// via [`accept_raw()`][Self::accept_raw].
208    ///
209    /// # Errors
210    ///
211    /// Returns an error if the address cannot be bound (e.g., port in use).
212    pub async fn bind(addr: impl monocoque_core::rt::ToSocketAddrs) -> io::Result<Self> {
213        let listener = TcpListener::bind(addr).await?;
214        debug!("[STREAM] Bound to {}", listener.local_addr()?);
215        let options = SocketOptions::default();
216        // Bound the inbound channel by recv_hwm so a fast peer against a slow
217        // consumer cannot grow it without bound; readers backpressure into TCP
218        // once it fills (finding B). recv_hwm == 0 means unbounded.
219        let (tx, rx) = if options.recv_hwm == 0 {
220            flume::unbounded()
221        } else {
222            flume::bounded(options.recv_hwm)
223        };
224        Ok(Self {
225            listener,
226            inbound_rx: rx,
227            inbound_tx: tx,
228            peers: HashMap::new(),
229            next_id: Arc::new(AtomicU64::new(1)),
230            options,
231        })
232    }
233
234    /// Accept the next raw TCP connection and register it as a new peer.
235    ///
236    /// Spawns background reader and writer tasks for the connection.  Returns
237    /// the routing ID assigned to this peer; the same ID is used to address
238    /// messages to this peer via [`send()`][Self::send].
239    ///
240    /// The caller will also receive a connection notification from
241    /// [`recv()`][Self::recv]: `[routing_id, "", ""]` with an empty data frame.
242    ///
243    /// # Errors
244    ///
245    /// Returns an error if the `accept()` system call fails.
246    pub async fn accept_raw(&mut self) -> io::Result<RoutingId> {
247        let (stream, addr) = match self.listener.accept().await {
248            Ok(pair) => pair,
249            Err(e) => {
250                // Throttle on fd exhaustion so a caller's accept loop cannot
251                // livelock while no descriptors are available.
252                crate::utils::backoff_on_fd_exhaustion(&e).await;
253                return Err(e);
254            }
255        };
256        crate::utils::configure_tcp_stream(&stream, &self.options, "STREAM")?;
257        debug!("[STREAM] Accepted raw connection from {}", addr);
258
259        // Generate a compact 8-byte routing ID.
260        let id_u64 = self.next_id.fetch_add(1, Ordering::Relaxed);
261        let routing_id = Bytes::copy_from_slice(&id_u64.to_be_bytes());
262
263        let (read_half, write_half) = stream.into_split();
264
265        // Per-peer outbound channel.
266        let (out_tx, out_rx) = if self.options.send_hwm == 0 {
267            flume::unbounded::<Bytes>()
268        } else {
269            flume::bounded::<Bytes>(self.options.send_hwm)
270        };
271        // Per-peer shutdown channel: dropping out_tx/shutdown_tx (on close_peer,
272        // disconnect, or socket drop) cancels the writer and reader tasks.
273        let (shutdown_tx, shutdown_rx) = flume::bounded::<()>(1);
274        self.peers.insert(
275            routing_id.clone(),
276            PeerHandle {
277                out_tx,
278                _shutdown: shutdown_tx,
279            },
280        );
281
282        // Spawn reader.
283        let inbound = self.inbound_tx.clone();
284        let rid = routing_id.clone();
285        monocoque_core::rt::spawn_detached(peer_reader(
286            rid,
287            read_half,
288            inbound,
289            self.options.read_buffer_size,
290            shutdown_rx,
291        ));
292
293        // Spawn writer.
294        monocoque_core::rt::spawn_detached(peer_writer(write_half, out_rx));
295
296        debug!("[STREAM] Peer {:?} registered", routing_id);
297        Ok(routing_id)
298    }
299
300    /// Receive the next message from any connected peer.
301    ///
302    /// Returns `[routing_id, empty, data]`.  An empty data frame signals a
303    /// connection event (connect or disconnect) for `routing_id`.
304    ///
305    /// Returns `Ok(None)` only if the socket's inbound channel has been
306    /// closed (i.e., the `StreamSocket` itself is being dropped).
307    ///
308    /// # Errors
309    ///
310    /// Returns an error if the underlying channel has an unexpected failure.
311    pub async fn recv(&mut self) -> io::Result<Option<InboundMsg>> {
312        match self.inbound_rx.recv_async().await {
313            Ok(msg) => {
314                trace!("[STREAM] Dequeued message from peer {:?}", msg[0]);
315                Ok(Some(msg))
316            }
317            Err(_) => Ok(None),
318        }
319    }
320
321    /// Send raw bytes to a specific peer.
322    ///
323    /// `msg` must contain at least one frame (the routing ID).  A 3-frame
324    /// layout is expected: `[routing_id, empty, data]`.  The routing ID
325    /// selects the destination; remaining frames are flattened and written as
326    /// raw bytes to the TCP stream.
327    ///
328    /// If the peer is not found (e.g., already disconnected), the message is
329    /// silently dropped.
330    ///
331    /// # Errors
332    ///
333    /// Returns an error if the message has no frames or if the peer's send
334    /// channel has disconnected.
335    ///
336    /// Returns `WouldBlock` when the peer's send queue has reached the
337    /// configured `send_hwm` limit.
338    pub async fn send(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
339        if msg.is_empty() {
340            return Err(io::Error::new(
341                io::ErrorKind::InvalidInput,
342                "STREAM send requires at least a routing-id frame",
343            ));
344        }
345
346        let routing_id = msg[0].clone();
347
348        // Collect all non-routing-id, non-empty frames as raw data.
349        let data: Bytes = msg
350            .iter()
351            .skip(1)
352            .find(|f| !f.is_empty())
353            .cloned()
354            .unwrap_or_default();
355
356        if data.is_empty() {
357            // Sending [routing_id, ""] is a disconnect hint (libzmq semantics).
358            self.disconnect(&routing_id);
359            return Ok(());
360        }
361
362        match self.peers.get(&routing_id) {
363            Some(peer) => {
364                peer.out_tx.try_send(data).map_err(|e| match e {
365                    flume::TrySendError::Full(_) => io::Error::new(
366                        io::ErrorKind::WouldBlock,
367                        format!("Peer {:?} send queue reached send_hwm", routing_id),
368                    ),
369                    flume::TrySendError::Disconnected(_) => io::Error::new(
370                        io::ErrorKind::BrokenPipe,
371                        format!("Peer {:?} send channel disconnected", routing_id),
372                    ),
373                })?;
374                trace!("[STREAM] Queued data for peer {:?}", routing_id);
375            }
376            None => {
377                warn!(
378                    "[STREAM] Unknown routing-id {:?}, dropping message",
379                    routing_id
380                );
381            }
382        }
383        Ok(())
384    }
385
386    /// Close a single peer, removing it from the routing table and cancelling
387    /// its background reader and writer tasks.
388    ///
389    /// Dropping the peer's internal `PeerHandle` closes both the outbound and the
390    /// shutdown channels, so the writer exits and the reader's shutdown branch
391    /// fires. Both tasks then release their TCP halves and file descriptors,
392    /// rather than the reader lingering until the remote closes the connection.
393    ///
394    /// After this call, messages addressed to `routing_id` are silently dropped.
395    /// Returns `true` if a peer was present and removed.
396    pub fn close_peer(&mut self, routing_id: &Bytes) -> bool {
397        if self.peers.remove(routing_id).is_some() {
398            debug!("[STREAM] Peer {:?} closed and removed", routing_id);
399            true
400        } else {
401            false
402        }
403    }
404
405    /// Disconnect a peer explicitly. Alias for [`close_peer`][Self::close_peer].
406    pub fn disconnect(&mut self, routing_id: &Bytes) {
407        self.close_peer(routing_id);
408    }
409
410    /// Number of currently tracked (connected) peers.
411    #[inline]
412    pub fn peer_count(&self) -> usize {
413        self.peers.len()
414    }
415
416    /// The local address this socket is bound to.
417    pub fn local_addr(&self) -> io::Result<std::net::SocketAddr> {
418        self.listener.local_addr()
419    }
420
421    /// Get a reference to the socket options.
422    #[inline]
423    pub const fn options(&self) -> &SocketOptions {
424        &self.options
425    }
426
427    /// Get a mutable reference to the socket options.
428    #[inline]
429    pub fn options_mut(&mut self) -> &mut SocketOptions {
430        &mut self.options
431    }
432}