Actix Web Websocket: The Ultimate Guide for Developers

Introduction

Actix web is a powerful Rust web framework that allows developers to build fast, secure, and scalable web applications. One of the key features of actix web is its support for websockets, which enables real-time communication between the server and the client. In this comprehensive guide, we will explore everything you need to know about actix web websockets, including its architecture, implementation, and best practices.

What are Websockets?

Websockets are a protocol that enables bi-directional, real-time communication between the server and the client. Unlike traditional HTTP requests, which are stateless and require a new connection for each request, websockets maintain a persistent connection that allows both the server and the client to send and receive data at any time. This makes websockets ideal for applications that require real-time updates, such as chat applications, online gaming, and financial trading platforms.

Actix Web Websocket Architecture

Actix web websockets are based on the Websocket protocol specification, which defines how the server and client should communicate over a websocket connection. The actix web websocket architecture consists of two main components: the server-side handler and the client-side handler.

Server-side Handler

The server-side handler is responsible for handling incoming websocket connections from the client. When a client initiates a websocket connection, the server creates a new WebSocket instance that represents the connection. The WebSocket instance is then passed to a user-defined handler function that can be used to send and receive messages over the websocket connection.

The server-side handler can be implemented using the actix_web::web::WebSocket type, which provides a high-level API for handling websocket connections. Alternatively, developers can implement their own custom websocket handler by implementing the actix_web::WsHandler trait.

Client-side Handler

The client-side handler is responsible for initiating websocket connections to the server and handling incoming messages from the server. When a client wants to establish a websocket connection, it sends a handshake request to the server using the Websocket protocol. If the handshake is successful, the server responds with a handshake response, and the websocket connection is established.

The client-side handler can be implemented using a variety of programming languages and libraries, including JavaScript, Python, and Rust. In this guide, we will focus on implementing the client-side handler using JavaScript and the popular WebSocket API.

Implementing Actix Web Websockets

Now that we have a basic understanding of the actix web websocket architecture, let’s explore how to implement websockets in a real-world application. In this section, we will walk through a step-by-step tutorial for building a simple chat application using actix web websockets.

Step 1: Setting up the Project

The first step in building our chat application is to set up a new actix web project. We can do this by running the following command:

 $ cargo new chat-app --bin 

This will create a new Rust project called chat-app with a binary target. Next, we need to add the actix web and tokio libraries to our project dependencies. We can do this by adding the following lines to our Cargo.toml file:

 [dependencies]actix-web = "3.3.2"tokio = { version = "1.11.0", features = ["full"] } 

Once we have added the dependencies, we can run the following command to install them:

 $ cargo build 

Step 2: Defining the Websocket Handler

The next step is to define the server-side websocket handler. We can do this by creating a new Rust module called websocket.rs in our project’s src directory. In this module, we will define a struct called ChatSession that represents a websocket session between the server and the client.

Here’s what our websocket.rs module should look like:

 use actix_web::{web, Error, HttpRequest, HttpResponse};use actix_web_actors::ws;

struct ChatSession {// TODO: Implement ChatSession struct}

pub async fn websocket_handler(req: HttpRequest,stream: web::Payload,) -> Result {let resp = ws::start(ChatSession {}, &req, stream);resp}

In this code, we define a new function called websocket_handler that takes an HttpRequest and a web::Payload as input parameters. The HttpRequest represents the incoming websocket connection request, and the web::Payload represents the incoming websocket data stream.

Inside the websocket_handler function, we create a new instance of the ChatSession struct and pass it to the ws::start function. The ws::start function takes care of establishing the websocket connection and invoking the appropriate handler functions when messages are received or sent.

At this point, our ChatSession struct is empty, so let’s define its fields and methods.

Step 3: Defining the ChatSession Struct

The ChatSession struct represents a websocket session between the server and the client. It contains a few fields that are used to manage the state of the session, as well as several methods that are used to handle incoming and outgoing messages.

Here’s what our ChatSession struct should look like:

 use actix::prelude::*;use actix_web_actors::ws;

struct ChatSession {// Unique session IDid: usize,// Client sessionws: Option<:websocketcontext>>,}

impl Actor for ChatSession {type Context = ws::WebsocketContext;

fn started(&mut self, ctx: &mut Self::Context) {// TODO: Implement started method}

fn stopped(&mut self, ctx: &mut Self::Context) {// TODO: Implement stopped method}}

impl StreamHandler> for ChatSession {fn handle(&mut self, msg: Result<:message ws::protocolerror>, ctx: &mut Self::Context) {// TODO: Implement handle method}}

impl ChatSession {// TODO: Implement ChatSession methods}

In this code, we define a new struct called ChatSession that implements the Actor and StreamHandler traits. The Actor trait represents a long-lived object that can receive messages and execute code in response. The StreamHandler trait represents an object that can handle incoming messages over a stream, such as a websocket connection.

The ChatSession struct contains two fields: id, which is a unique identifier for the session, and ws, which is an optional reference to the WebsocketContext that represents the connection between the server and the client.

The Actor trait requires us to implement two methods: started and stopped. The started method is called when the ChatSession actor is first created, and allows us to perform any initialization tasks that we need. The stopped method is called when the actor is stopped, and allows us to perform any cleanup tasks that we need.

The StreamHandler trait requires us to implement a single method: handle. The handle method is called whenever a new message is received over the websocket connection.

In the next step, we will implement the started, stopped, and handle methods.

Step 4: Implementing the ChatSession Methods

Now that we have defined the ChatSession struct, let’s implement its methods.

Implementing the started Method

The started method is called when the ChatSession actor is first created. In this method, we will generate a unique ID for the session and store a reference to the WebsocketContext that represents the connection between the server and the client.

 impl Actor for ChatSession {type Context = ws::WebsocketContext;

fn started(&mut self, ctx: &mut Self::Context) {// Generate unique session IDself.id = ctx.random::();// Store reference to WebsocketContextself.ws = Some(ctx);}}

Implementing the stopped Method

The stopped method is called when the ChatSession actor is stopped. In this method, we will perform any cleanup tasks that we need, such as closing the websocket connection.

 impl Actor for ChatSession {type Context = ws::WebsocketContext;

fn stopped(&mut self, ctx: &mut Self::Context) {// Close websocket connectionself.ws.take();}}

Implementing the handle Method

The handle method is called whenever a new message is received over the websocket connection. In this method, we will parse the incoming message and perform the appropriate action based on its contents.

 impl StreamHandler> for ChatSession {fn handle(&mut self, msg: Result<:message ws::protocolerror>, ctx: &mut Self::Context) {match msg {Ok(ws::Message::Text(text)) => {// Handle text messageself.handle_text_message(text, ctx);}Ok(ws::Message::Binary(bin)) => {// Handle binary messageself.handle_binary_message(bin, ctx);}_ => {}}}}

impl ChatSession {fn handle_text_message(&mut self, msg: String, ctx: &mut ws::WebsocketContext) {// TODO: Implement handle_text_message method}

fn handle_binary_message(&mut self, msg: Vec, ctx: &mut ws::WebsocketContext) {// TODO: Implement handle_binary_message method}}

In this code, we define a new method called handle_text_message and handle_binary_message that take the incoming message as input parameter. These methods are responsible for parsing the incoming message and performing the appropriate action based on its contents.

At this point, our ChatSession struct is fully implemented. The next step is to define the client-side websocket handler in JavaScript.

Step 5: Defining the Client-side Websocket Handler

The client-side websocket handler is responsible for initiating the websocket connection to the server and handling incoming messages. In this section, we will define the client-side websocket handler using the popular WebSocket API in JavaScript.

Here’s what our client-side websocket handler should look like:

 const socket = new WebSocket("ws://localhost:8080/ws");

socket.onopen = function (event) {console.log("WebSocket connection established.");};

socket.onmessage = function (event) {console.log("Received message: " + event.data);};

socket.onclose = function (event) {console.log("WebSocket connection closed.");};

socket.onerror = function (event) {console.error("WebSocket error: " + event);};

In this code, we create a new instance of the WebSocket class and pass the URL of our websocket endpoint as the input parameter. We then define several event handlers that will be called when certain events occur.

The onopen event handler is called when the websocket connection is established. In this handler, we can perform any initialization tasks that we need, such as sending an authentication token to the server.

The onmessage event handler is called whenever a new message is received over the websocket connection. In this handler, we can parse the incoming message and perform the appropriate action based on its contents.

The onclose event handler is called when the websocket connection is closed. In this handler, we can perform any cleanup tasks that we need, such as removing event listeners.

The onerror event handler is called when an error occurs during the websocket connection. In this handler, we can log the error message for debugging purposes.

Step 6: Sending and Receiving Messages

Now that we have defined both the server-side and client-side websocket handlers, let’s explore how to send and receive messages between them.

Sending Messages from the Server

To send a message from the server to the client, we can use the send method of the WebsocketContext struct. For example, to send a text message to the client, we can use the following code:

 ctx.text("Hello, world!"); 

To send a binary message to the client, we can use the following code:

 ctx.binary(vec![0x00, 0x01, 0x02, 0x03]); 

Receiving Messages on the Client

To receive messages on the client, we can define an onmessage event handler for our WebSocket instance. For example, to log incoming text messages to the console, we can use the following code:

 socket.onmessage = function (event) {console.log("Received message: " + event.data);}; 

To handle incoming binary messages, we can use the following code:

 socket.onmessage = function (event) {if (event.data instanceof ArrayBuffer) {console.log("Received binary message: " + new Uint8Array(event.data));}}; 

Step 7: Running the Application

Now that we have implemented the server-side and client-side websocket handlers, let’s run our application and test it out.

To run the application, we can use the following command:

 $ cargo run 

This will start the actix web server and listen for incoming websocket connections on port 8080.

To test the application, we can open a new browser window and navigate to http://localhost:8080/. This will establish a websocket connection to the server and initiate the chat application.

Congratulations! You have successfully implemented a simple chat application using actix web websockets.

Best Practices for Actix Web Websockets

Now that we have explored how to implement actix web websockets, let’s discuss some best practices