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