Build a Rust speed test using Actix and WebSockets
16 minIntroduction
Speed tests are an excellent way to check your network connection speed. Fast network connections are key for enjoying a seamless experience on the internet.
In this tutorial, we will build our own speed test application using Rust Actix, WebSockets, and a simple JavaScript client. Then we will Dockerize our application and add it to the GitHub container registry to deploy it to Koyeb.
Actix is a high-performance and popular framework for Rust that we will use to build our web application. Our application will perform speed tests thanks to a WebSocket connection. WebSockets open a two-way connection between clients and servers, allowing for fast and real-time communication.
By the end of this tutorial, you will be able to test the performance of your network connection using your very own speed test site hosted on Koyeb. Thanks to Koyeb, our application will benefit from native global load balancing, autoscaling, autohealing, and auto HTTPS (SSL) encryption with zero configuration on our part.
Prerequisites
To follow this guide, you will need:
- A local development environment with Rust installed
- A GitHub account to store our Docker image on the GitHub container registry
- A Koyeb account to deploy and run the Rust Actix server
- Docker installed on your machine
- To have configured Docker for use with GitHub Packages
Steps
To successfully build our speed test site and deploy it on Koyeb, you will need to follow these steps:
- Create a new Rust project
- Set up a basic web server with Actix
- Work with WebSockets using Actix
- Send the test file
- Create the speed test client
- Dockerize the Rust application
- Push the Docker image to GitHub Container Registry
- Deploy the sample network performance application on Koyeb
Create a new Rust project
To start, create a new rust project:
Cargo is a build system and package manager for Rust. In the koyeb-speed-test
directory that was just created, you should see a new Cargo.toml
file. This file is where we will tell Rust how to build our application and what dependencies we need.
In that file, edit the dependencies section to look like this:
Great! Now for our project files, make two new directories for our server source code and static files (HTML):
Populate the src
directory with the following files:
main.rs
will be where we initialize and run the server. server.rs
will be where we define our server logic. Specifically, the WebSocket functionality for performing the speed test.
In the static
directory, create an index.html
file and a 10 MB file that we'll send over the network to test the connection speed:
NOTE: MacOS users need to do bs=1m
instead of bs=1M
.
That second command is creating a file that is roughly 10 megabytes of null characters. This way, we know exactly how much data we're sending over the network for calculating the connection speed later.
Our directory structure looks like this:
Awesome! Our project is looking good. Let's start building our server.
Set up a basic web server with Actix
Actix is a high-performance web framework for Rust. It is a framework that provides a simple, yet powerful, way to build web applications. In our case, we'll be using it for two things:
- Serving HTML to users: This will be for the main page of our application. It is how users will start the speed test and see the results.
- Serving test files over a WebSocket connection: This will be for performing the speed test.
In src/main.rs
, create a basic web server that will serve the index.html
file from the static
folder:
In this case, anytime someone accesses the /
URL, we will serve the index.html
file from the static
folder using the index()
function. The index()
function gets the file using the NamedFile
struct and then returns it to the caller.
Work with WebSockets using Actix
Now we're going to start working with WebSockets. We'll be using the actix-web-actors
crate to handle them. The socket logic will have to do the following:
- Ensure the socket is open (e.g. check if the socket was either closed by the client or if the connection was interrupted). We'll do this by pinging the client every five seconds.
- Upon request from the client, send a 10 MB file over the socket.
- Upon request from the client, close the socket.
To set up the WebSocket logic, add all of the following to src/server.rs
:
There is a lot going on in this code snippet, here is an explanation of what this code is doing and why it is important.
Sockets are a way of allowing for continuous communication between a server and a client. With sockets, the connection is kept open until either the client or the server closes it.
Each client that connects to the server has their own socket. Each socket has a context, which is a type that implements the Actor
trait. This is where we will be working with the socket.
There are issues with this model. Specifically, how do we ensure the socket is still open if connection interruptions can disconnect it? This is what the hb()
function is for! It is first initialized with the current socket's context and then runs an interval. This interval will run every 5 seconds and will ping the client. If the client does not respond within 10 seconds, the socket will be closed.
Now, update src/main.rs
too so that it can use the WebSocket logic we just wrote:
Now that our server logic is finished, we can finally start the server with the following command:
Go to your browser and visit http://localhost:8080
. You should see a blank index page.
Send The Test File
Currently, if a client were to send text to the server, through the WebSocket, we would echo it back to them.
However, we are not concerned with the text that is sent over the socket. Whatever the client sends, we want to respond with the test file, since that is the sole purpose of our server. Let's update the Text
case in the handle()
function to do that:
Now, whenever a client sends text to the server through the WebSocket, we'll write the 10mb file to a buffer and send that to the client as binary data.
Create The Speed Test Client
Now, we will create a client that can send text to the server and receive the test file as a response.
Open up static/index.html
and add the following to create the client:
When we visit http://localhost:8080
in the browser, we should now see the following:
Great! Now we need to add some JavaScript, so this page can perform the test. Add the following script inside the <script>
tag inside index.html
:
Nice, our client is complete! Our client sends 10 requests to the server and then calculates the average time it took to complete each request.
Dockerize the Rust application
For deployment, we'll be using Docker. Docker is a lightweight containerization tool that allows us to run our server in a container.
To Dockerize our server, create a simple Dockerfile
in the root of the project directory. Add the following to it:
For consistency, name the working directory after the package name in the Cargo.toml
file. In our case, it's koyeb-speed-test
.
Let's break down what this file is doing. When we build the Docker image, it will download an official existing image for Rust, create the working directory and copy all of our project files into said directory. Then it will run the cargo install
command to install all of our dependencies and expose port 8080.
A small thing that might help build times is to create a .dockerignore
file in the root of the project directory. Add the following to it:
This way, when we build the Docker image, it will ignore the target
directory, which is where the cargo build
command creates the final executable.
The last and most important part is that it will run the koyeb-speed-test-server
command to start the server. We'll need to define this command in the Cargo.toml
file:
The last thing we must do to ensure our project works in the Docker container is to change the bind address in src/main.rs
to 0.0.0.0
instead of 127.0.0.1
:
Next, build an image for our project:
We can see if it runs by running the following command:
Push the Docker image to GitHub Container Registry
Now that we know our project runs in the container, push it to the registry:
Of course, you can use the container registry you prefer - Docker Hub, Azure ACR, AWS ECR - Koyeb supports those as well. In this guide, we are pushing to GitHub Container Registry.
Deploy the sample network performance application on Koyeb
It's now time to deploy our container image on Koyeb. On the Overview tab of the Koyeb control panel, click Create Web Service to begin:
- Choose Docker as the deployment method.
- Fill in the Docker image field with the name of the image we previously created.
- Click the Private image toggle and, in the select field, click Create secret. In the form that appears, choose a name for this new Secret (e.g.
gh-registry-secret
), choose the type of registry provider (GitHub in our case), and your GitHub username and a valid GitHub token with registry read/write permissions (for packages) as the password. You can create one here. - Choose a name for your App and Service and click Deploy.
You will automatically be redirected to the Koyeb App page, where you can follow the progress of your application's deployment. In a matter of seconds, once your app is deployed, click on the Public URL ending with koyeb.app
. You should see your speed test site in action!
If you would like to look at the code for this sample application, you can find it here.
Conclusion
You now have your very own speed test site written in Rust, Dockerized, and hosted on Koyeb. Our application natively benefits from global load balancing, autoscaling, autohealing, and auto HTTPS (SSL) encryption with zero configuration from us.
If you'd like to learn more about Rust and Actix, check out actix/examples and actix-web. This article was actually based on the echo example from Actix, found here.
If you would like to read more Koyeb tutorials, checkout out our tutorials collection. Have an idea for a tutorial you'd like us to cover? Let us know by joining the conversation over on the Koyeb community platform!