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) {
87    use compio_buf::BufResult;
88    use compio_io::AsyncRead;
89    use monocoque_core::io::take_read_buffer;
90
91    // Connection notification
92    let _ = inbound
93        .send_async(vec![routing_id.clone(), Bytes::new(), Bytes::new()])
94        .await;
95
96    // Lazily grown on the first read, so a connected-but-silent peer holds no
97    // read slab.
98    let mut read_buf = BytesMut::new();
99    loop {
100        // SAFETY: `buf` is passed straight to `read`; the data path truncates
101        // it to `n` before freezing, and EOF/error drop it without reading it.
102        let buf = unsafe { take_read_buffer(&mut read_buf, read_buffer_size) };
103        let BufResult(result, mut buf) = reader.read(buf).await;
104        match result {
105            Ok(0) => {
106                debug!("[STREAM] Peer {:?} disconnected (EOF)", routing_id);
107                break;
108            }
109            Ok(n) => {
110                debug_assert!(n <= read_buffer_size);
111                buf.truncate(n);
112                let data = buf.freeze();
113                trace!("[STREAM] Received {} bytes from peer {:?}", n, routing_id);
114                let msg = vec![routing_id.clone(), Bytes::new(), data];
115                if inbound.send_async(msg).await.is_err() {
116                    break; // socket dropped
117                }
118            }
119            Err(e) => {
120                debug!("[STREAM] Peer {:?} read error: {}", routing_id, e);
121                break;
122            }
123        }
124    }
125
126    // Disconnect notification
127    let _ = inbound.try_send(vec![routing_id, Bytes::new(), Bytes::new()]);
128}
129
130/// Writes raw bytes from the per-peer send channel to the TCP connection.
131async fn peer_writer(mut writer: OwnedWriteHalf, outbound: Receiver<Bytes>) {
132    use compio_buf::BufResult;
133    use compio_io::AsyncWriteExt;
134
135    while let Ok(data) = outbound.recv_async().await {
136        let BufResult(res, _) = writer.write_all(data).await;
137        if res.is_err() {
138            break;
139        }
140    }
141}
142
143// ─────────────────────────────────────────────────────────────────────────────
144// StreamSocket
145// ─────────────────────────────────────────────────────────────────────────────
146
147/// STREAM socket  -  raw TCP bridging without ZMTP handshake.
148///
149/// Accepts plain TCP connections and multiplexes them through a ZeroMQ-style
150/// routing-ID interface.  Each accepted connection is assigned a unique 8-byte
151/// routing ID; all subsequent sends and receives for that connection use the
152/// same ID to route messages.
153///
154/// Unlike other socket types, `StreamSocket` performs **no ZMTP handshake**  -
155/// it speaks plain TCP bytes, making it suitable for bridging to HTTP servers,
156/// legacy services, and command-line tools such as `nc` and `curl`.
157pub struct StreamSocket {
158    /// TCP listener (held after `bind`, until dropped).
159    listener: TcpListener,
160    /// Channel from background reader tasks to the application.
161    inbound_rx: Receiver<InboundMsg>,
162    /// Shared sender half for reader tasks.
163    inbound_tx: Sender<InboundMsg>,
164    /// Per-peer outbound channels (routing_id → sender).
165    peers: HashMap<RoutingId, Sender<Bytes>>,
166    /// Monotonically increasing routing-ID counter.
167    next_id: Arc<AtomicU64>,
168    /// Socket options.
169    options: SocketOptions,
170}
171
172impl StreamSocket {
173    /// Bind a STREAM socket to a TCP address.
174    ///
175    /// The returned socket is ready to accept raw (non-ZMTP) TCP connections
176    /// via [`accept_raw()`][Self::accept_raw].
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if the address cannot be bound (e.g., port in use).
181    pub async fn bind(addr: impl monocoque_core::rt::ToSocketAddrs) -> io::Result<Self> {
182        let listener = TcpListener::bind(addr).await?;
183        debug!("[STREAM] Bound to {}", listener.local_addr()?);
184        let (tx, rx) = flume::unbounded();
185        Ok(Self {
186            listener,
187            inbound_rx: rx,
188            inbound_tx: tx,
189            peers: HashMap::new(),
190            next_id: Arc::new(AtomicU64::new(1)),
191            options: SocketOptions::default(),
192        })
193    }
194
195    /// Accept the next raw TCP connection and register it as a new peer.
196    ///
197    /// Spawns background reader and writer tasks for the connection.  Returns
198    /// the routing ID assigned to this peer; the same ID is used to address
199    /// messages to this peer via [`send()`][Self::send].
200    ///
201    /// The caller will also receive a connection notification from
202    /// [`recv()`][Self::recv]: `[routing_id, "", ""]` with an empty data frame.
203    ///
204    /// # Errors
205    ///
206    /// Returns an error if the `accept()` system call fails.
207    pub async fn accept_raw(&mut self) -> io::Result<RoutingId> {
208        let (stream, addr) = self.listener.accept().await?;
209        crate::utils::configure_tcp_stream(&stream, &self.options, "STREAM")?;
210        debug!("[STREAM] Accepted raw connection from {}", addr);
211
212        // Generate a compact 8-byte routing ID.
213        let id_u64 = self.next_id.fetch_add(1, Ordering::Relaxed);
214        let routing_id = Bytes::copy_from_slice(&id_u64.to_be_bytes());
215
216        let (read_half, write_half) = stream.into_split();
217
218        // Per-peer outbound channel.
219        let (out_tx, out_rx) = if self.options.send_hwm == 0 {
220            flume::unbounded::<Bytes>()
221        } else {
222            flume::bounded::<Bytes>(self.options.send_hwm)
223        };
224        self.peers.insert(routing_id.clone(), out_tx);
225
226        // Spawn reader.
227        let inbound = self.inbound_tx.clone();
228        let rid = routing_id.clone();
229        monocoque_core::rt::spawn_detached(peer_reader(
230            rid,
231            read_half,
232            inbound,
233            self.options.read_buffer_size,
234        ));
235
236        // Spawn writer.
237        monocoque_core::rt::spawn_detached(peer_writer(write_half, out_rx));
238
239        debug!("[STREAM] Peer {:?} registered", routing_id);
240        Ok(routing_id)
241    }
242
243    /// Receive the next message from any connected peer.
244    ///
245    /// Returns `[routing_id, empty, data]`.  An empty data frame signals a
246    /// connection event (connect or disconnect) for `routing_id`.
247    ///
248    /// Returns `Ok(None)` only if the socket's inbound channel has been
249    /// closed (i.e., the `StreamSocket` itself is being dropped).
250    ///
251    /// # Errors
252    ///
253    /// Returns an error if the underlying channel has an unexpected failure.
254    pub async fn recv(&mut self) -> io::Result<Option<InboundMsg>> {
255        match self.inbound_rx.recv_async().await {
256            Ok(msg) => {
257                trace!("[STREAM] Dequeued message from peer {:?}", msg[0]);
258                Ok(Some(msg))
259            }
260            Err(_) => Ok(None),
261        }
262    }
263
264    /// Send raw bytes to a specific peer.
265    ///
266    /// `msg` must contain at least one frame (the routing ID).  A 3-frame
267    /// layout is expected: `[routing_id, empty, data]`.  The routing ID
268    /// selects the destination; remaining frames are flattened and written as
269    /// raw bytes to the TCP stream.
270    ///
271    /// If the peer is not found (e.g., already disconnected), the message is
272    /// silently dropped.
273    ///
274    /// # Errors
275    ///
276    /// Returns an error if the message has no frames or if the peer's send
277    /// channel has disconnected.
278    ///
279    /// Returns `WouldBlock` when the peer's send queue has reached the
280    /// configured `send_hwm` limit.
281    pub async fn send(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
282        if msg.is_empty() {
283            return Err(io::Error::new(
284                io::ErrorKind::InvalidInput,
285                "STREAM send requires at least a routing-id frame",
286            ));
287        }
288
289        let routing_id = msg[0].clone();
290
291        // Collect all non-routing-id, non-empty frames as raw data.
292        let data: Bytes = msg
293            .iter()
294            .skip(1)
295            .find(|f| !f.is_empty())
296            .cloned()
297            .unwrap_or_default();
298
299        if data.is_empty() {
300            // Sending [routing_id, ""] is a disconnect hint (libzmq semantics).
301            self.disconnect(&routing_id);
302            return Ok(());
303        }
304
305        match self.peers.get(&routing_id) {
306            Some(tx) => {
307                tx.try_send(data).map_err(|e| match e {
308                    flume::TrySendError::Full(_) => io::Error::new(
309                        io::ErrorKind::WouldBlock,
310                        format!("Peer {:?} send queue reached send_hwm", routing_id),
311                    ),
312                    flume::TrySendError::Disconnected(_) => io::Error::new(
313                        io::ErrorKind::BrokenPipe,
314                        format!("Peer {:?} send channel disconnected", routing_id),
315                    ),
316                })?;
317                trace!("[STREAM] Queued data for peer {:?}", routing_id);
318            }
319            None => {
320                warn!(
321                    "[STREAM] Unknown routing-id {:?}, dropping message",
322                    routing_id
323                );
324            }
325        }
326        Ok(())
327    }
328
329    /// Disconnect a peer explicitly, removing it from the routing table.
330    ///
331    /// After this call, messages addressed to `routing_id` are silently dropped.
332    /// The background reader task will detect the closed write half and exit.
333    pub fn disconnect(&mut self, routing_id: &Bytes) {
334        if self.peers.remove(routing_id).is_some() {
335            debug!("[STREAM] Peer {:?} removed from routing table", routing_id);
336        }
337    }
338
339    /// Number of currently tracked (connected) peers.
340    #[inline]
341    pub fn peer_count(&self) -> usize {
342        self.peers.len()
343    }
344
345    /// The local address this socket is bound to.
346    pub fn local_addr(&self) -> io::Result<std::net::SocketAddr> {
347        self.listener.local_addr()
348    }
349
350    /// Get a reference to the socket options.
351    #[inline]
352    pub const fn options(&self) -> &SocketOptions {
353        &self.options
354    }
355
356    /// Get a mutable reference to the socket options.
357    #[inline]
358    pub fn options_mut(&mut self) -> &mut SocketOptions {
359        &mut self.options
360    }
361}