fastwebsockets

repository·main·Indexed 22 days ago

https://github.com/denoland/fastwebsockets

A high-performance RFC6455 WebSocket protocol implementation in Rust. It provides a low-level frame parser and a high-level client/server implementation with support for integration with hyper and Axum. Key features include a FragmentCollector for handling fragmented messages, server-side HTTP upgrades, and comprehensive CloseCode and WebSocketError handling.

Tokens
8.6K
Snippets
21
Records
28
Agent score
78%

What's inside fastwebsockets

  1. Understand the Plaintext, echo WebSocket server benchmark

    main

    The benchmark measures the performance of WebSocket servers by tracking the number of messages sent per second. The Y-axis in the benchmark charts represents the message throughput (messages/sec), while the payload size per message is used to differentiate the test cases.

    Benchmark environment details:

    • OS: Linux divy 5.19.0-1022-gcp #24~22.04.1-Ubuntu SMP x86_64 GNU/Linux
    • Memory: 32GiB System memory
    • CPU: Intel(R) Xeon(R) CPU @ 3.10GHz

    This benchmark compares fastwebsockets against other implementations including rust-websocket, uWebSockets, and tokio-tungstenite.

  2. Handle WebSocket message fragmentation with FragmentCollector

    main

    By default, fastwebsockets provides raw frames where the FIN bit may be unset, requiring the application to handle fragmentation manually.

    To receive concatenated, full messages instead of individual fragments, wrap your WebSocket instance in a FragmentCollector. When using FragmentCollector, ws.read_frame().await? will always return a frame where fin is true, representing a complete message.

    let mut ws = WebSocket::after_handshake(socket);
    let mut ws = FragmentCollector::new(ws);
    
    let incoming = ws.read_frame().await?;
    // Always returns full messages
    assert!(incoming.fin);
  3. Integrate fastwebsockets with Axum

    main

    To use fastwebsockets with the Axum web framework, enable the upgrade and with_axum features in your Cargo.toml:

    fastwebsockets = { version = "0.9.0", features = ["upgrade", "with_axum"] }

    In your Axum handler, use upgrade::IncomingUpgrade to receive the upgrade request. Call .upgrade() on the incoming upgrade to get a response and an UpgradeFut. You can then wrap the resulting stream in a FragmentCollector to handle full messages easily.

    use axum::{response::IntoResponse, routing::get, Router};
    use fastwebsockets::upgrade;
    use fastwebsockets::OpCode;
    use fastwebsockets::WebSocketError;
    
    #[tokio::main]
    async fn main() {
      let app = Router::new().route("/", get(ws_handler));
    
      let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
      axum::serve(listener, app).await.unwrap();
    }
    
    async fn handle_client(fut: upgrade::UpgradeFut) -> Result<(), WebSocketError> {
      let mut ws = fastwebsockets::FragmentCollector::new(fut.await?);
    
      loop {
        let frame = ws.read_frame().await?;
        match frame.opcode {
          OpCode::Close => break,
          OpCode::Text | OpCode::Binary => {
            ws.write_frame(frame).await?;
          }
          _ => {}
        }
      }
    
      Ok
    }
    
    async fn ws_handler(ws: upgrade::IncomingUpgrade) -> impl IntoResponse {
      let (response, fut) = ws.upgrade().unwrap();
    
      tokio::task::spawn(async move {
        if let Err(e) = handle_client(fut).await {
          eprintln!("Error in websocket connection: {}", e);
        }
      });
    
      response
    }
  4. Perform server-side HTTP upgrades using the upgrade feature

    main

    If you enable the upgrade feature, you can use fastwebsockets::upgrade to handle server-side WebSocket upgrades powered by hyper.

    Calling upgrade::upgrade(&mut req) returns a tuple containing a Response (to be sent back to the client) and an UpgradeFut (the future that resolves to the upgraded stream). You should spawn the future to handle the WebSocket connection independently.

    use fastwebsockets::upgrade;
    use hyper::{Request, body::{Incoming, Bytes}, Response};
    use http_body_util::Empty;
    use anyhow::Result;
    
    async fn server_upgrade(
      mut req: Request<Incoming>,
    ) -> Result<Response<Empty<Bytes>>> {
      let (response, fut) = upgrade::upgrade(&mut req)?;
    
      tokio::spawn(async move {
        if let Err(e) = handle_client(fut).await {
          eprintln!("Error in websocket connection: {}", e);
        }
      });
    
      Ok(response)
    }
  5. Perform client-side WebSocket handshakes

    main

    To connect to a WebSocket server as a client, use the fastwebsockets::handshake module.

    1. Construct a hyper::Request with the necessary WebSocket headers:
      • UPGRADE: "websocket"
      • CONNECTION: "upgrade"
      • Sec-WebSocket-Key: Generated via handshake::generate_key()
      • Sec-WebSocket-Version: "13"
    2. Use handshake::client to perform the handshake. This requires an executor (e.g., a SpawnExecutor that wraps tokio::task::spawn) to manage the asynchronous tasks.
    use fastwebsockets::handshake;
    use fastwebsockets::WebSocket;
    use hyper::{Request, body::Bytes, upgrade::Upgraded, header::{UPGRADE, CONNECTION}};
    use http_body_util::Empty;
    use tokio::net::TcpStream;
    use std::future::Future;
    use anyhow::Result;
    
    async fn connect() -> Result<WebSocket<Upgraded>> {
      let stream = TcpStream::connect("localhost:9001").await?;
    
      let req = Request::builder()
        .method("GET")
        .uri("http://localhost:9001/")
        .header("Host", "localhost:9001")
        .header(UPGRADE, "websocket")
        .header(CONNECTION, "upgrade")
        .header(
          "Sec-WebSocket-Key",
          handshake::generate_key(),
        )
        .header("Sec-WebSocket-Version", "13")
        .body(Empty::<Bytes>::new())?;
    
      let (ws, _) = handshake::client(&SpawnExecutor, req, stream).await?;
      Ok(ws)
    }
    
    // Tie hyper's executor to tokio runtime
    struct SpawnExecutor;
    
    impl<Fut> hyper::rt::Executor<Fut> for SpawnExecutor
    where
      Fut: Future + Send + 'static,
      Fut::Output: Send + 'static,
    {
      fn execute(&self, fut: Fut) {
        tokio::task::spawn(fut);
      }
    }
  6. Manage WebSocket frame payloads with `Payload`

    main

    The Payload enum is a memory-efficient container for WebSocket frame data. It supports various ownership models to avoid unnecessary allocations:

    • Borrowed(&[u8]): A reference to existing data.
    • BorrowedMut(&mut [u8]): A mutable reference to existing data.
    • Owned(Vec<u8>): An owned vector of bytes.
    • Bytes(BytesMut): Data backed by a BytesMut buffer.

    Payload implements Deref<Target = [u8]>, allowing you to treat it like a byte slice. If you need to modify a borrowed payload, use the .to_mut() method, which will convert the payload to Owned if necessary.

    // Example of converting a payload to mutable
    let mut payload = Payload::from("hello");
    let mut_slice = payload.to_mut();
    mut_slice[0] = b'H';
  7. Use FragmentCollector to handle fragmented WebSocket messages

    main

    The FragmentCollector is a wrapper for a WebSocket that automatically handles fragmented messages. Instead of receiving multiple individual frames for a single logical message, FragmentCollector buffers the fragments in memory and returns a single, complete Frame once the final fragment (with the fin bit set) is received.

    Key behaviors:

    • Memory Usage: The entire message payload is buffered in memory until completion. Use this only when streaming is not an option or messages are reasonably sized.
    • UTF-8 Validation: For OpCode::Text messages, the collector ensures the resulting payload is valid UTF-8.
    • Automatic Management: It handles control frames (like Pongs) and ensures the connection state remains consistent during collection.

    To use it, wrap your WebSocket instance with FragmentCollector::new(ws) and call read_frame() instead of reading directly from the raw WebSocket.

    use tokio::net::TcpStream;
    use fastwebsockets::{WebSocket, FragmentCollector, OpCode, Role};
    use anyhow::Result;
    
    async fn handle_client(
      socket: TcpStream,
    ) -> Result<()> {
      let ws = WebSocket::after_handshake(socket, Role::Server);
      let mut ws = FragmentCollector::new(ws);
    
      loop {
        let frame = ws.read_frame().await?;
        match frame.opcode {
          OpCode::Close => break,
          OpCode::Text | OpCode::Binary => {
            ws.write_frame(frame).await?;
          }
          _ => {}
        }
      }
      Ok(())
    }
  8. Handle fragmented WebSocket messages with FragmentCollector

    main

    By default, fastwebsockets returns raw frames. To receive full, concatenated messages instead of individual fragments, wrap your WebSocket instance in a FragmentCollector. When using a FragmentCollector, read_frame() will always return a frame where the fin bit is set, representing a complete message.

    use fastwebsockets::{FragmentCollector, WebSocket, Role};
    use tokio::net::TcpStream;
    use anyhow::Result;
    
    async fn handle(socket: TcpStream) -> Result<()> {
      let mut ws = WebSocket::after_handshake(socket, Role::Server);
      let mut ws = FragmentCollector::new(ws);
      let incoming = ws.read_frame().await?;
      // Always returns full messages
      assert!(incoming.fin);
      Ok(())
    }
  9. Perform HTTP upgrades for WebSocket connections

    main

    If you enable the upgrade feature, you can use the fastwebsockets::upgrade::upgrade function to handle server-side WebSocket upgrades using hyper. This function returns a response to be sent to the client and a future that resolves to the upgraded WebSocket instance.

    use fastwebsockets::upgrade::upgrade;
    use http_body_util::Empty;
    use hyper::{Request, body::{Incoming, Bytes}, Response};
    use anyhow::Result;
    
    async fn server_upgrade(
      mut req: Request<Incoming>,
    ) -> Result<Response<Empty<Bytes>>> {
      let (response, fut) = upgrade(&mut req)?;
    
      tokio::spawn(async move {
        let ws = fut.await;
        // Do something with the websocket
      });
    
      Ok(response)
    }
  10. Implement a basic WebSocket echo server

    main

    You can use fastwebsockets as a full-fledged WebSocket server. After performing a handshake on a TcpStream, you can wrap the socket in a WebSocket instance.

    Key configuration methods for the WebSocket instance include:

    • set_writev(bool): Enables vectored writes.
    • set_auto_close(bool): Automatically handles closing.
    • set_auto_pong(bool): Automatically responds to PING frames with PONG.

    To handle messages, loop over ws.read_frame().await? and match on the OpCode.

    use fastwebsockets::{Frame, OpCode, WebSocket};
    use tokio::net::TcpStream;
    
    async fn handle_client(
      mut socket: TcpStream,
    ) -> Result<(), WebSocketError> {
      handshake(&mut socket).await?;
    
      let mut ws = WebSocket::after_handshake(socket);
      ws.set_writev(true);
      ws.set_auto_close(true);
      ws.set_auto_pong(true);
    
      loop {
        let frame = ws.read_frame().await?;
    
        match frame {
          OpCode::Close => break,
          OpCode::Text | OpCode::Binary => {
            let frame = Frame::new(true, frame.opcode, None, frame.payload);
            ws.write_frame(frame).await?;
          }
        }
      }
    
      Ok(())
    }
  11. Convert between u16 and CloseCode

    main

    You can convert between raw u16 status codes and the CloseCode enum using the From trait.

    u16 to CloseCode

    Converting a u16 to a CloseCode maps standard WebSocket status codes to their respective enum variants:

    • 1000 -> Normal
    • 1001 -> Away
    • 1002 -> Protocol
    • 1003 -> Unsupported
    • 1005 -> Status
    • 1006 -> Abnormal
    • 1007 -> Invalid
    • 1008 -> Policy
    • 1009 -> Size
    • 1010 -> Extension
    • 1011 -> Error
    • 1012 -> Restart
    • 1013 -> Again
    • 1015 -> Tls
    • 1..=999 -> Bad(code)
    • 1016..=2999 -> Reserved(code)
    • 3000..=3999 -> Iana(code)
    • 4000..=4999 -> Library(code)
    • All other values -> Bad(code)

    CloseCode to u16

    Converting a CloseCode back to a u16 returns the corresponding numeric status code used in the WebSocket protocol.

    // Convert u16 to CloseCode
    let code = CloseCode::from(1000); // Returns CloseCode::Normal
    
    // Convert CloseCode to u16
    let numeric_code: u16 = CloseCode::Normal.into(); // Returns 1000
  12. Use IncomingUpgrade with Axum

    main

    If the with_axum feature is enabled, you can use IncomingUpgrade as an Axum extractor (FromRequestParts). This simplifies the handshake process within an Axum handler.

    IncomingUpgrade automatically:

    • Extracts the Sec-WebSocket-Key.
    • Validates the Sec-WebSocket-Version is 13.
    • Retrieves the hyper::upgrade::OnUpgrade extension.
    • Generates the correct Sec-WebSocket-Accept header for the response.
    // Example Axum handler using IncomingUpgrade
    async fn ws_handler(
        upgrade: IncomingUpgrade,
    ) -> impl axum::response::IntoResponse {
        // 1. Generate the response using the helper
        let (response, upgrade_fut) = upgrade.upgrade().unwrap();
        
        // 2. Spawn a task to handle the actual websocket connection
        tokio::spawn(async move {
            let ws = upgrade_fut.await.unwrap();
            // use ws...
        });
    
        // 3. Return the response to the client
        response
    }