Concurrent Servers in Rust

ingve1 pts0 comments

Concurrent Servers: Part 7 - Rust - Eli Bendersky's website

Toggle navigation

Eli Bendersky's website

About

Projects

Archives

This is part 7 in a series of posts on writing concurrent network servers.<br>In this part, we discuss how the challenges described in earlier parts are<br>tackled in the Rust programming language.

All posts in the series:

Part 1 - Introduction

Part 2 - Threads

Part 3 - Event-driven

Part 4 - libuv

Part 5 - Redis case study

Part 6 - Callbacks, Promises and async/await

Part 7 - Rust (this part)

Several years have passed since the previous parts were published. I've recently<br>went over them to make sure the information presented is still relevant and<br>all the code samples build and run using modern toolchains. I strongly<br>recommend reviewing the previous parts before reading this one.

This post assumes a basic familiarity with the Rust programming language. It<br>will only explain Rust constructs when we encounter code that wouldn't appear in<br>an introductory book or tutorial.

Setting the baseline - a sequential state machine server

The first few parts in the series focused on a socket server that implements<br>a simple state machine protocol. See part 1<br>for a complete description of the protocol. Let's start by showing how this<br>protocol is implemented in a basic sequential Rust server:

use async_socket_server::serve_connection;<br>use std::net::TcpListener;

fn main() -> std::io::Result()> {<br>let port = match std::env::args().nth(1) {<br>Some(s) => s,<br>None => "9090".to_string(),<br>};<br>let addr = format!("127.0.0.1:{port}");

let listener = TcpListener::bind(addr)?;<br>println!("Serving on port {port}");

loop {<br>let (stream, addr) = listener.accept()?;<br>println!("connection received from {}", addr);<br>if let Err(e) = serve_connection(stream) {<br>eprintln!("error serving connection: {}", e);<br>} else {<br>println!("peer done {addr}");

With the function serve_connection defined as:

pub enum ProcessingState {<br>WaitForMsg,<br>InMsg,

pub fn serve_connection(mut stream: TcpStream) -> std::io::Result()> {<br>stream.write_all(b"*")?;

let mut state = ProcessingState::WaitForMsg;<br>let mut buf = [0u8; 1024];<br>loop {<br>let n = stream.read(&mut buf)?;<br>if n == 0 {<br>// Connection closed by the client.<br>break;

for byte in &buf[..n] {<br>match state {<br>ProcessingState::WaitForMsg => {<br>if *byte == b'^' {<br>state = ProcessingState::InMsg;<br>ProcessingState::InMsg => {<br>if *byte == b'$' {<br>state = ProcessingState::WaitForMsg;<br>} else {<br>let newbyte = byte.wrapping_add(1);<br>stream.write_all(&[newbyte])?;

Ok(())

As a reminder, this server version is sequential because it accepts clients<br>one by one; the main loop blocks on serve_connection until it's done (the<br>client closes the connection), and only then goes back to accept the next<br>client.

One thread per client

Clearly, handling clients one by one won't do. In<br>part 2,<br>we've discussed approaches that use OS threads to handle clients concurrently.<br>Let's start with the unbounded one-thread-per-client solution in Rust:

use async_socket_server::serve_connection;<br>use std::{net::TcpListener, thread};

fn main() -> std::io::Result()> {<br>let port = match std::env::args().nth(1) {<br>Some(s) => s,<br>None => "9090".to_string(),<br>};<br>let addr = format!("127.0.0.1:{port}");

let listener = TcpListener::bind(addr)?;<br>println!("Serving on port {port}");

loop {<br>let (stream, addr) = listener.accept()?;<br>println!("connection received from {}", addr);

let res = thread::Builder::new().spawn(move || {<br>if let Err(e) = serve_connection(stream) {<br>eprintln!("error serving connection: {}", e);<br>} else {<br>println!("peer done {addr}");<br>});

if let Err(e) = res {<br>eprintln!("error spawning thread: {}", e);

The spawn method returns a Result>; on success, we allow<br>the handle to be dropped at the end of the loop iteration. In Rust, this<br>detaches the thread; we don't actually wait for it to complete. This<br>is reasonable for our code sample, because the loop is infinite; it never<br>terminates anyway. The potential for runaway threads is just one of the issues<br>with the unbounded threads approach discussed in part 2. The solution is to use<br>a fixed thread pool.

Thread pool

Before diving into the code, a quick note on the design: the thread pool is<br>a fixed set of threads that await "jobs" and handle them to completion. In our<br>case a "job" is serve_connection for a specific client. There are many ways<br>to implement a thread pool; for our use case, I went with a set of threads that<br>all get a shared channel to which the main thread sends jobs. A worker thread<br>picks up the next job from the channel, serves it to completion, and goes back<br>to waiting for the next job. Here's how this looks in code:

struct Job {<br>stream: TcpStream,<br>addr: SocketAddr,

fn worker(receiver: ReceiverJob>) {<br>while let Ok(job) = receiver.recv() {<br>if let Err(e) = serve_connection(job.stream) {<br>eprintln!("error serving connection from {}: {}", job.addr, e);<br>} else {<br>println!("peer done {}", job.addr);

What is Receiver? It's a type from the crossbeam_channel crate:

use...

part addr thread stream rust serve_connection

Related Articles