Hello! I was experimenting with std::net
for writing a TCP server as part of a personal project. While working on this, I was going to try out
- a single-threaded server using
std::net::TcpListener
- a multi-threaded server using a single thread for the listener, but spawning threads to handle each stream
- a multi-threaded server with multiple threads listening (using reuse_port and reuse_address, probably using the net2 crate)
- a server using Tokio's TcpListener and tasks to handle each stream
- a server using Tokio's TcpListener and multiple threads listening, as in this gist by Alex Crichton
- [edit] After reading about Redis' design, also considering directly using mio's non-blocking
TcpListener
While trying to familiarise myself with all these available options, I decided to try and see how fast the simplest architecture of a single threaded server would be for my use case - a request counter. My project was going to be a metric collector where clients could increment and retrieve the current value of a metric, but I decided to build the simplest case of a single counter first.
After some trial and error, I wrote the code below. Writing another small Rust binary to create 4 threads and make 100K requests on each thread, I found its performance to be around 30k requests/second. I was wondering whether:
- I'm making some silly mistakes in the server code, the measurement or this idea of testing speed in the first place.
- Whether the code below is the fastest (while still being somewhat idiomatic) single-threaded TCP server that counts the number of requests made.
Sending "inc" increments the counter and returns "ok" and sending "read" returns the current value of the counter.
use std::io;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
fn main() {
run_server().unwrap();
}
fn run_server() -> io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:9000")?;
let mut c = 0i64;
for stream in listener.incoming() {
handle_client(stream?, &mut c);
}
Ok(())
}
fn handle_client(mut stream: TcpStream, counter: &mut i64) {
// In my local tests, all input was less than 8 bytes, so I thought using
// an 8 byte buffer would be fastest?
let mut buf = [0u8; 8];
// Also tried BufReader, but in this case we're dealing with <16 bytes
// per request, so it probably doesn't make sense?
let num_bytes_read = stream.read(&mut buf).unwrap();
// Noticed that operating on the buffer was faster than String, probably
// because of the overhead of from_utf8?
if num_bytes_read >= 6 && &buf[0..4] == b"read" {
// A little worried about the overhead of UTF-8 strings here.
stream.write(counter.to_string().as_bytes()).unwrap();
} else if num_bytes_read >= 5 && &buf[0..3] == b"inc" {
*counter = *counter + 1;
stream.write("ok".as_bytes()).unwrap();
} else {
// Not actually hitting this branch in the tests I was running.
stream.write("unknown".as_bytes()).unwrap();
}
}
Thanks!