This guide helps you migrate from tokio-tungstenite to yawc. Both libraries are RFC 6455 compliant and pass the Autobahn test suite, but they have different APIs and feature sets.
- Quick Comparison
- Basic Client Connection
- Basic Server Connection
- Message Handling
- Compression
- Splitting Streams
- Axum Integration
- Configuration Options
- Feature Flags
| Feature | tokio-tungstenite | yawc |
|---|---|---|
| RFC 6455 Compliance | Yes | Yes |
| Autobahn Tests | Yes | Yes |
| Compression | No | Yes |
| WebAssembly | No | Yes |
| Stream Access | Direct | Abstracted |
| reqwest Integration | No | Yes |
| Zero-copy Design | No | Yes |
use tokio_tungstenite::{connect_async, tungstenite::Message};
use futures::{StreamExt, SinkExt};
let (ws_stream, _) = connect_async("wss://echo.websocket.org").await?;
let (mut write, mut read) = ws_stream.split();
write.send(Message::Text("Hello".to_string())).await?;
while let Some(msg) = read.next().await {
let msg = msg?;
match msg {
Message::Text(text) => println!("Received: {}", text),
Message::Binary(data) => println!("Binary: {} bytes", data.len()),
_ => {}
}
}use yawc::{WebSocket, Frame, OpCode};
use futures::{StreamExt, SinkExt};
let mut ws = WebSocket::connect("wss://echo.websocket.org".parse()?).await?;
ws.send(Frame::text("Hello")).await?;
while let Some(frame) = ws.next().await {
let (opcode, _is_fin, payload) = frame.into_parts();
match opcode {
OpCode::Text => {
let text = std::str::from_utf8(&payload)?;
println!("Received: {}", text);
}
OpCode::Binary => println!("Binary: {} bytes", payload.len()),
_ => {}
}
}Key Differences:
- yawc uses
Frameinstead ofMessage - URL parsing is explicit in yawc (
.parse()?) - yawc returns
Framedirectly (noResultwrapping each frame) - Use
frame.into_parts()to get(OpCode, bool, Bytes)or accessors likeframe.payload(),frame.as_str()
use tokio_tungstenite::accept_async;
use futures::{StreamExt, SinkExt};
let stream = tokio::net::TcpStream::connect("localhost:8080").await?;
let ws_stream = accept_async(stream).await?;
while let Some(msg) = ws_stream.next().await {
let msg = msg?;
ws_stream.send(msg).await?; // Echo
}use yawc::WebSocket;
use hyper::{Request, Response, body::Incoming};
use futures::{StreamExt, SinkExt};
async fn handle_upgrade(req: Request<Incoming>) -> Result<Response<_>> {
let (response, upfn) = WebSocket::upgrade(req)?;
tokio::spawn(async move {
let mut ws = upfn.await.expect("upgrade");
while let Some(frame) = ws.next().await {
let _ = ws.send(frame).await; // Echo
}
});
Ok(response)
}Key Differences:
- yawc integrates with hyper's HTTP upgrade mechanism
- The upgrade returns a response and a future
- Response must be sent before awaiting the upgrade future
use tokio_tungstenite::tungstenite::Message;
// Creating messages
let text = Message::Text("Hello".to_string());
let binary = Message::Binary(vec![1, 2, 3]);
let ping = Message::Ping(vec![]);
let pong = Message::Pong(vec![]);
let close = Message::Close(Some(CloseFrame {
code: CloseCode::Normal,
reason: "Goodbye".into(),
}));
// Handling messages
match msg {
Message::Text(text) => { /* ... */ }
Message::Binary(data) => { /* ... */ }
Message::Ping(_) => { /* auto-responded */ }
Message::Pong(_) => { /* ... */ }
Message::Close(_) => { /* ... */ }
Message::Frame(_) => { /* raw frame */ }
}use yawc::{Frame, OpCode};
use yawc::close::CloseCode;
// Creating frames
let text = Frame::text("Hello");
let binary = Frame::binary(vec![1, 2, 3]);
let ping = Frame::ping("");
let pong = Frame::pong("");
let close = Frame::close(CloseCode::Normal, b"Goodbye");
// Handling frames - Option 1: Using accessors
match frame.opcode() {
OpCode::Text => {
let text = frame.as_str(); // Direct &str access
}
OpCode::Binary => {
let data = frame.payload(); // Bytes reference
}
OpCode::Ping => {
// Pong automatically sent, but ping frame is still returned
// so you can observe/log it if needed
}
OpCode::Pong => { /* ... */ }
OpCode::Close => {
let code = frame.close_code();
let reason = frame.close_reason()?;
}
_ => {}
}
// Handling frames - Option 2: Using into_parts() for ownership
let (opcode, _is_fin, payload) = frame.into_parts();
match opcode {
OpCode::Text => {
let text = std::str::from_utf8(&payload)?;
}
OpCode::Binary => {
// payload is now owned Bytes
}
_ => {}
}Key Differences:
Frameinstead ofMessageOpCodeenum instead ofMessagevariants- Access payload via
payload()method orinto_parts()for ownership - Helper methods like
as_str(),close_code(),close_reason()
use tokio_tungstenite::{
tungstenite::protocol::WebSocketConfig,
connect_async_with_config,
};
let config = WebSocketConfig {
max_message_size: Some(64 << 20),
max_frame_size: Some(16 << 20),
..Default::default()
};
let (ws, _) = connect_async_with_config(url, Some(config), false).await?;Note: tokio-tungstenite doesn't directly support compression - you need tungstenite with compression features.
use yawc::{WebSocket, Options, CompressionLevel};
let ws = WebSocket::connect("wss://example.com".parse()?)
.with_options(
Options::default()
.with_compression_level(CompressionLevel::default())
.server_no_context_takeover() // Reduce memory
.with_max_payload_read(64 * 1024 * 1024)
)
.await?;Key Differences:
- yawc has built-in compression support (permessage-deflate)
- More granular control over compression settings
- Context takeover options for memory management
- Window size control with
zlibfeature
let (mut write, mut read) = ws_stream.split();
// Read and write independently
tokio::spawn(async move {
while let Some(msg) = read.next().await {
// Handle message
}
});
tokio::spawn(async move {
write.send(Message::text("Hello")).await?;
});use futures::StreamExt;
let (mut write, mut read) = ws.split();
// Read and write independently
tokio::spawn(async move {
while let Some(frame) = read.next().await {
// Handle frame
}
});
tokio::spawn(async move {
write.send(Frame::text("Hello")).await?;
});Key Differences:
- Same
futures::StreamExt::split()approach - Both maintain protocol handling (ping/pong)
- yawc also has low-level
split_stream()but it's rarely needed
use axum::{
extract::ws::{WebSocketUpgrade, WebSocket},
response::Response,
};
async fn ws_handler(ws: WebSocketUpgrade) -> Response {
ws.on_upgrade(|socket| handle_socket(socket))
}
async fn handle_socket(mut socket: WebSocket) {
while let Some(msg) = socket.recv().await {
let msg = msg.unwrap();
socket.send(msg).await.unwrap();
}
}use yawc::{IncomingUpgrade, Options};
use axum::response::Response;
async fn ws_handler(ws: IncomingUpgrade) -> Response {
let (response, upgrade) = ws
.upgrade(Options::default())
.unwrap();
tokio::spawn(async move {
let mut ws = upgrade.await.unwrap();
while let Some(frame) = ws.next().await {
let _ = ws.send(frame).await;
}
});
response
}Key Differences:
- yawc uses
IncomingUpgradeextractor withaxumfeature - Explicit
Optionsconfiguration at upgrade time - Must spawn task manually (more control)
- Returns response directly from handler
use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
let config = WebSocketConfig {
max_send_queue: None,
max_message_size: Some(64 << 20),
max_frame_size: Some(16 << 20),
accept_unmasked_frames: false,
};use yawc::{Options, CompressionLevel};
let options = Options::default()
.with_max_payload_read(64 * 1024 * 1024)
.with_max_read_buffer(128 * 1024 * 1024)
.with_compression_level(CompressionLevel::fast())
.with_utf8() // Validate UTF-8
.with_no_delay(); // Disable Nagle's algorithmKey Differences:
- yawc focuses on memory limits and compression
- Built-in UTF-8 validation option
- TCP_NODELAY configuration
- Separate read payload and buffer limits
[dependencies]
tokio-tungstenite = { version = "0.20", features = [
"native-tls", # or "rustls-tls-native-roots"
] }[dependencies]
yawc = { version = "0.3", features = [
"axum", # Axum integration
"reqwest", # reqwest client support
"logging", # Debug logging
"zlib", # Advanced compression options
"json", # JSON serialization helpers
"rustls-ring", # Default: ring crypto
# "rustls-aws-lc-rs", # Alternative: AWS-LC crypto
] }Key Differences:
- yawc uses rustls by default (tokio-tungstenite requires choosing TLS)
- yawc has framework integrations (axum, reqwest)
- yawc has JSON helpers
- Crypto provider selection for rustls
Both libraries automatically handle ping/pong, but with a key difference:
Ping frames trigger automatic pong responses and are not returned via the stream iterator.
Ping frames trigger automatic pong responses and are still returned to your application, allowing you to observe them:
use yawc::frame::OpCode;
while let Some(frame) = ws.next().await {
match frame.opcode() {
OpCode::Ping => {
// Pong is sent automatically
// But you can still see the ping
log::debug!("Received ping from server");
}
OpCode::Text | OpCode::Binary => {
// Handle data frames
}
_ => {}
}
}This design gives you visibility into connection health checks while still handling the protocol automatically.
tokio-tungstenite:
ws.send(Message::Text("text".into())).await?;
ws.send(Message::Binary(vec![1,2,3])).await?;
ws.send(Message::Ping(vec![])).await?;yawc:
ws.send(Frame::text("text")).await?;
ws.send(Frame::binary(vec![1,2,3])).await?;
ws.send(Frame::ping("")).await?;tokio-tungstenite:
ws.send(Message::Close(None)).await?;
ws.close(None).await?;yawc:
use yawc::close::CloseCode;
ws.send(Frame::close(CloseCode::Normal, b"Goodbye")).await?;
// Connection closes automaticallytokio-tungstenite:
match ws.send(msg).await {
Err(tokio_tungstenite::tungstenite::Error::ConnectionClosed) => {
// Handle closed connection
}
Err(e) => return Err(e.into()),
Ok(()) => {}
}yawc:
match ws.send(frame).await {
Err(yawc::WebSocketError::ConnectionClosed) => {
// Handle closed connection
}
Err(e) => return Err(e),
Ok(()) => {}
}- Update
Cargo.tomldependencies - Replace
MessagewithFrame - Replace
Message::Text/BinarywithOpCode::Text/Binary - Update message creation to use
Frameconstructors - Change URL string to explicit
.parse()? - Update server upgrade to use hyper integration
- Configure compression if needed
- Update Axum handlers if using axum feature
- Test with your existing test suite
- Run Autobahn test suite if doing custom protocol work
yawc is designed for high-performance scenarios:
- Zero-copy frame processing where possible
- Efficient handling of fragmented messages
- Built-in compression with configurable memory/CPU trade-offs
- Proven in 24/7 production trading systems
If you're migrating for performance reasons, benchmark your specific use case to validate improvements.