WebSocket
Add WebSocket endpoints with RFC 6455 frame-level I/O built in — no third-party WebSocket crates required.
Overview
rust-web-server includes a complete WebSocket implementation in
src/websocket/mod.rs. It covers upgrade detection, the RFC 6455 opening
handshake (SHA-1 + base64 key derivation), and frame-level read/write for all
standard opcodes.
Integration pattern
WebSocket connections require taking over the raw TCP stream after the 101
upgrade response. They are handled in a custom accept loop rather than through
the normal Controller / Application pipeline (which has no access to the
underlying stream).
use std::io::{Read, Write};
use std::net::TcpListener;
use rust_web_server::request::Request;
use rust_web_server::websocket::{WebSocket, Frame};
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let mut stream = stream.unwrap();
// Read the HTTP upgrade request.
let mut buf = vec![0u8; 4096];
let n = stream.read(&mut buf).unwrap();
let request = Request::parse(&buf[..n]).unwrap();
if WebSocket::is_upgrade_request(&request) {
// Send 101 Switching Protocols.
let mut response = WebSocket::handshake_response(&request).unwrap();
stream.write_all(&response.generate()).unwrap();
// Enter the frame loop.
loop {
match WebSocket::read_frame(&mut stream) {
Ok(Frame::Text(msg)) => {
WebSocket::write_frame(&mut stream, Frame::Text(msg)).unwrap();
}
Ok(Frame::Ping(data)) => {
WebSocket::write_frame(&mut stream, Frame::Pong(data)).unwrap();
}
Ok(Frame::Close(code, reason)) => {
WebSocket::send_close(&mut stream, code.unwrap_or(1000), &reason).ok();
break;
}
Ok(_) => {}
Err(_) => break,
}
}
}
}Detecting upgrade requests
WebSocket::is_upgrade_request(request: &Request) -> boolReturns true when the request has all three required headers:
Upgrade: websocketConnection: Upgrade(or any value containing "upgrade")Sec-WebSocket-Key: <base64>
Handshake response
WebSocket::handshake_response(request: &Request) -> Result<Response, String>Builds an HTTP 101 Switching Protocols response with the correct
Sec-WebSocket-Accept value. The accept value is computed as:
base64(SHA-1(client_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))Both SHA-1 and base64 are implemented from scratch in src/websocket/mod.rs
with no external dependencies.
If the request includes a Sec-WebSocket-Protocol header, the first listed
sub-protocol is echoed back in the response.
Frame types
Frame is an enum covering all RFC 6455 opcodes:
| Variant | Opcode | Description |
|---|---|---|
Frame::Text(String) |
0x1 |
A complete UTF-8 text message |
Frame::Binary(Vec<u8>) |
0x2 |
A binary message |
Frame::Ping(Vec<u8>) |
0x9 |
Ping — respond with a matching Pong |
Frame::Pong(Vec<u8>) |
0xA |
Pong |
Frame::Close(Option<u16>, String) |
0x8 |
Close with optional status code and reason |
Frame::Continuation { fin, data } |
0x0 |
Continuation fragment for fragmented messages |
Reading frames
WebSocket::read_frame(stream: &mut impl Read) -> Result<Frame, String>Reads one complete frame. Client-to-server masking (required by RFC 6455) is handled automatically — the payload is unmasked before being returned. Extended payload lengths (16-bit and 64-bit) are supported.
A frame whose declared payload is larger than WebSocket::MAX_FRAME_PAYLOAD
(16 MiB) is an error, returned before any of the payload is read, and the
payload is read as it arrives rather than allocated at the size the client
claims (since 18.1.0; before, a frame claiming an enormous length made the
process abort). For another limit:
WebSocket::read_frame_limited(stream: &mut impl Read, max_payload: u64) -> Result<Frame, String>Writing frames
WebSocket::write_frame(stream: &mut impl Write, frame: Frame) -> Result<(), String>Writes one frame. Server-to-client frames are sent unmasked per the RFC. Payload length encoding (7-bit, 16-bit, 64-bit) is chosen automatically.
Convenience methods
// Send a text message
WebSocket::send_text(&mut stream, "hello")?;
// Send a close frame
WebSocket::send_close(&mut stream, 1000, "normal closure")?;
// Reply to a Ping with a Pong
WebSocket::send_pong(&mut stream, ping_payload)?;Complete echo server example
use std::net::TcpListener;
use std::io::Read;
use rust_web_server::request::Request;
use rust_web_server::websocket::{WebSocket, Frame};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
println!("WebSocket echo server listening on ws://127.0.0.1:7878");
for stream in listener.incoming() {
std::thread::spawn(|| {
let mut stream = stream.unwrap();
let mut buf = vec![0u8; 4096];
let n = stream.read(&mut buf).unwrap();
let request = Request::parse(&buf[..n]).unwrap();
if !WebSocket::is_upgrade_request(&request) { return; }
let mut handshake = WebSocket::handshake_response(&request).unwrap();
use std::io::Write;
stream.write_all(&handshake.generate()).unwrap();
loop {
match WebSocket::read_frame(&mut stream) {
Ok(Frame::Text(msg)) => {
WebSocket::write_frame(&mut stream, Frame::Text(msg)).unwrap();
}
Ok(Frame::Binary(data)) => {
WebSocket::write_frame(&mut stream, Frame::Binary(data)).unwrap();
}
Ok(Frame::Ping(data)) => {
WebSocket::send_pong(&mut stream, data).unwrap();
}
Ok(Frame::Close(code, reason)) => {
WebSocket::send_close(&mut stream, code.unwrap_or(1000), &reason).ok();
break;
}
_ => break,
}
}
});
}
}src/ws_proxy/mod.rs provides WsProxy — a standalone WebSocket proxy that
forwards upgrade requests and relays raw bytes bidirectionally between the
client and a backend server.