No description
- Rust 100%
| .forgejo/workflows | ||
| src | ||
| .gitignore | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
estd-websockets
A WebSocket library built on estd and estd-tls.
Features
- WebSocket Server: Handle multiple connections with async support
- WebSocket Client: Connect to WebSocket servers
- Frame Protocol: Full RFC 6455 compliant frame handling
- Message Types: Text and binary messages
- Keep-Alive: Automatic ping/pong support
- TLS Support: Secure WebSocket connections (WSS) via estd-tls
- No External Dependencies: Only
estd, andestd-tls
Quick Start
WebSocket Server
use estd_websockets::{Server, Message};
use std::io::Cursor;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut server = Server::new("127.0.0.1:8080");
server.on_connect("/ws", |stream, path| {
// Handle WebSocket connection
loop {
match stream.read_message() {
Ok(msg) => {
match msg {
Message::Text(text) => {
println!("Received: {}", text);
stream.send_text("Echo: ".to_string() + &text)?;
}
Message::Binary(data) => {
println!("Received binary data: {} bytes", data.len());
stream.send_binary(data)?;
}
}
}
Err(_) => break,
}
}
Ok(())
})?;
// Listen for connections (integrate with estd-http server)
Ok(())
}
WebSocket Client
use estd_websockets::{Client, Message};
use std::io::Cursor;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to server (typically after TCP/TLS connection)
let socket = Cursor::new(Vec::new());
let mut client = Client::new(socket, "ws://example.com:8080/ws");
// Send a message
client.send_text("Hello, WebSocket!")?;
// Read a message
match client.read_message()? {
Message::Text(text) => println!("Server said: {}", text),
Message::Binary(data) => println!("Server sent binary: {} bytes", data.len()),
}
// Close connection
client.close()?;
Ok(())
}
Module Structure
Core Modules
error- Error types and result for WebSocket operationsframe- WebSocket frame parsing and serialization (RFC 6455)message- Message types (Text and Binary)handshake- HTTP upgrade handshake negotiationstream- WebSocket connection stream abstractionserver- WebSocket server implementationclient- WebSocket client implementation
Frame Opcodes
Continuation(0x0) - Continuation frame for fragmented messagesText(0x1) - Text frameBinary(0x2) - Binary frameClose(0x8) - Connection close framePing(0x9) - Ping control framePong(0xA) - Pong control frame
Message Types
use estd_websockets::Message;
// Text message
let msg = Message::text("Hello");
assert!(msg.is_text());
assert_eq!(msg.as_text(), Some("Hello"));
// Binary message
let msg = Message::binary(vec![1, 2, 3]);
assert!(msg.is_binary());
assert_eq!(msg.as_binary(), Some([1u8, 2, 3].as_ref()));
Stream Operations
use estd_websockets::Stream;
use std::io::Cursor;
let socket = Cursor::new(Vec::new());
let mut stream = Stream::new(socket, false); // false = server
// Send messages
stream.send_text("Hello")?;
stream.send_binary(vec![1, 2, 3])?;
// Keep-alive
stream.ping(b"ping")?;
stream.pong(b"pong")?;
// Close
stream.close()?;
Handshake Process
The handshake follows RFC 6455:
-
Client sends HTTP upgrade request with:
Upgrade: websocketConnection: UpgradeSec-WebSocket-Key(24-byte base64 encoded)Sec-WebSocket-Version: 13
-
Server validates request and responds with:
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept(SHA-1 hash of key)
use estd_websockets::Handshake;
use std::collections::HashMap;
let mut headers = HashMap::new();
headers.insert("upgrade".to_string(), "websocket".to_string());
headers.insert("connection".to_string(), "Upgrade".to_string());
headers.insert("sec-websocket-key".to_string(), "dGhlIHNhbXBsZSBub25jZQ==".to_string());
headers.insert("sec-websocket-version".to_string(), "13".to_string());
// Parse request
let key = Handshake::parse_upgrade_request(&headers)?;
// Generate accept response
let accept_key = Handshake::generate_accept_key(&key)?;
Integration with estd-http
The library integrates seamlessly with estd-http for HTTP upgrade handling:
use estd_http::{Server, StatusCode, ResponseBuilder};
use estd_websockets::{Server as WsServer, Handshake};
use std::collections::HashMap;
let http_server = Server::new("127.0.0.1:8080");
let ws_server = WsServer::new("127.0.0.1:8080");
http_server.get("/ws", |req, _ctx| {
// Extract headers from HTTP request
let mut headers = HashMap::new();
// Parse WebSocket upgrade
match ws_server.handle_upgrade("/ws", &headers) {
Ok(accept_key) => {
// Send 101 Switching Protocols response
ResponseBuilder::new()
.status(StatusCode::SwitchingProtocols)
.header("Upgrade", "websocket")
.header("Connection", "Upgrade")
.header("Sec-WebSocket-Accept", &accept_key)
.build()
}
Err(_) => {
ResponseBuilder::new()
.status(StatusCode::BadRequest)
.build()
}
}
})?;
Frame Masking
- Client frames must be masked (XOR with 4-byte mask key)
- Server frames must not be masked
- Masking is automatic in the
Framestruct
Async Support
The library works with estd::sync::go for async operations:
use estd::sync;
use estd_websockets::Client;
let handle = sync::go(|| {
// Blocking WebSocket operations in background
let mut client = Client::new(socket, "ws://example.com/ws");
client.send_text("Hello")?;
Ok::<(), Box<dyn std::error::Error>>(())
});
// Continue with other work
let result = handle.join();
TLS Support
Use with estd-tls for secure WebSocket connections (WSS):
use estd_tls::Connector;
use estd_websockets::Client;
// Create TLS connection
let connector = Connector::new();
let tls_stream = connector.connect("example.com:443")?;
// Wrap with WebSocket protocol
let mut client = Client::new(tls_stream, "wss://example.com/ws");
Build and Test
# Build the library
cargo build
# Run tests
cargo test
# Run examples
cargo run --example ws-server
cargo run --example ws-client
Architecture
Frame Serialization
Frames are serialized according to RFC 6455:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S|(4bits)|A| (7) | (0/16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
Message Fragmentation
Messages can be split across multiple frames:
- First frame with opcode
TextorBinaryandFIN=0 - Middle frames with opcode
ContinuationandFIN=0 - Final frame with opcode
ContinuationandFIN=1
(Fragmentation support is tracked as TODO)
Performance Considerations
- Zero-copy frame parsing where possible
- Efficient base64 and SHA-1 implementations
- No unnecessary allocations in hot paths
- Proper socket buffering through underlying stream
License
ISC License - See LICENSE for details
Author
Damien FLETY