feat(ws): duplex wiring + handler API + echo example (v0.4 chunk 3)
Topology (b) as agreed: ONE actor per connection via smarm RFC 008 fd
arms. After the 101, ws::duplex::run_duplex replaces the HTTP loop:
try_select(&[&outbound_rx, &FdArm::readable(fd)]) per iteration,
outbound at index 0 (priority order = owed writes drain before reads;
honest backpressure). Accepted gap documented: mid-write of a large
outbound frame the actor isn't reading.
Handler API (deferred questions, as answered this session):
- WsHandler { on_message, on_close } runs IN the conn actor's select
loop; concurrency = spawn an actor with a WsSender clone (the SSE
producer pattern). Conn::upgrade(self, handler) flat signature;
WsUpgrade grows from marker to Box<dyn WsHandler> payload.
- WsSender: Clone; send/text/binary/ping/close -> Err(WsClosed).
Unbounded channel like SSE; slow client bounded by write_timeout.
- Control frames invisible v1: auto-pong inline (5.5.2), peer close
auto-echoed (5.5.1; server closes TCP first per 7.1.1) surfacing
only as on_close(Some(code), reason); wordless endings (EOF, write
failure, drain timeout) = on_close(None, ""). on_ping/on_pong can
land later as default methods, non-breaking.
- Server-initiated close (WsSender::close, FrameError->1002/1009/1007,
handler panic->1011): close out, bounded drain (one write_timeout
budget) for the peer echo, data discarded (1.4). Handler panics
re-raise smarm's stop sentinel first (the pipeline catch_unwind
dance, replicated).
- Caps: Config.max_frame_payload (1 MiB) / max_message_bytes (4 MiB).
Conn actor: 101 branch now hands fd + leftover buf (pipelined first
frame carries over; tested) + boxed handler to run_duplex; registry
entry stays Busy for the ws lifetime, so graceful shutdown force-stops
the conn out of the select park at the drain deadline (tested — the
in-runtime request_stop wake covers select parks too).
Tests: chunk-1 EOF test rewritten into a 9-test duplex suite (echo,
pipelined first frame, ping/pong, both close directions, 1002 unmasked,
1009 header-cap, fragmentation with interleaved ping, shutdown
force-stop). Suite 59u+40i+2d. Hammer: 35x lifecycle+ws subset + 3 full
+ 1 full under smarm-trace, all green; no AlreadyExists out of
try_select (the fresh eager-cleanup path held). Validated against
python websocket-client (echo, pong payload, close 1000).
examples/ws_echo.rs: /echo in-actor + /clock producer-spawning.
This commit is contained in:
+15
-9
@@ -366,25 +366,31 @@ impl Conn {
|
||||
self
|
||||
}
|
||||
|
||||
/// Accept a WebSocket upgrade on this request (RFC 6455 §4.2).
|
||||
/// Accept a WebSocket upgrade on this request (RFC 6455 §4.2),
|
||||
/// handing `handler` to the duplex loop that takes the socket over.
|
||||
///
|
||||
/// On a valid handshake: status `101`, the `upgrade`/`connection`/
|
||||
/// `sec-websocket-accept` response headers, the [`WsUpgrade`] marker
|
||||
/// the connection actor acts on, and the pipeline is halted.
|
||||
/// `sec-websocket-accept` response headers, the [`WsUpgrade`] payload
|
||||
/// the connection actor acts on, and the pipeline is halted. After
|
||||
/// the 101 the connection actor leaves HTTP and drives `handler` —
|
||||
/// see [`WsHandler`](crate::ws::WsHandler) for the callback contract.
|
||||
///
|
||||
/// On an invalid handshake: the appropriate rejection (`400`, or
|
||||
/// `426` + `sec-websocket-version: 13` for a version mismatch),
|
||||
/// empty body, pipeline halted; the connection stays plain HTTP with
|
||||
/// normal keep-alive semantics. Pre-check with
|
||||
/// On an invalid handshake: `handler` is dropped untouched and the
|
||||
/// appropriate rejection goes out (`400`, or `426` +
|
||||
/// `sec-websocket-version: 13` for a version mismatch), empty body,
|
||||
/// pipeline halted; the connection stays plain HTTP with normal
|
||||
/// keep-alive semantics. Pre-check with
|
||||
/// [`ws::handshake::is_upgrade_request`](crate::ws::handshake::is_upgrade_request)
|
||||
/// to branch before committing.
|
||||
///
|
||||
/// [`WsUpgrade`]: crate::ws::WsUpgrade
|
||||
pub fn upgrade(mut self) -> Self {
|
||||
pub fn upgrade(mut self, handler: impl crate::ws::WsHandler) -> Self {
|
||||
use crate::ws::{handshake, Rejection};
|
||||
match handshake::validate(&self) {
|
||||
Ok(accept) => {
|
||||
self.upgrade = Some(crate::ws::WsUpgrade { _priv: () });
|
||||
self.upgrade = Some(crate::ws::WsUpgrade {
|
||||
handler: Box::new(handler),
|
||||
});
|
||||
self.put_status(101)
|
||||
.put_header("upgrade", "websocket")
|
||||
.put_header("connection", "Upgrade")
|
||||
|
||||
Reference in New Issue
Block a user