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