Connected Mailbox Exercise
In this exercise, we will take our "SimpleDB" protocol parser and turn it into a network-connected data storage service. When a user sends a "PUBLISH" we will push the data into a queue, and when the user sends a "RETRIEVE" we will pop some data off the queue (if any is available). The user will connect via TCP to port 7878.
After completing this exercise you are able to
- 
write a Rust binary that uses a Rust library 
- 
combine two Rust packages into a Cargo Workspace 
- 
open a TCP port and perform an action when each user connects 
- 
use I/O traits to read/write from a TCP socket 
Prerequisites
- 
creating and running binary crates with cargo
- 
using matchto pattern-match on anenum, capturing any inner values
- 
using Rust's ReadandWriteI/O traits
- 
familiarity with TCP socket listening and accepting 
Tasks
- 
Create an empty folder called connected-mailbox. Copy in thesimple-dbproject from before and create a new binary crate calledtcp-server, and put them both into a Cargo Workspace.📂 connected-mailbox ┣ 📄 Cargo.toml ┃ ┣ 📂 simple-db ┃ ┣ 📄 Cargo.toml ┃ ┗ ... ┃ ┗ 📂 tcp-server ┣ 📄 Cargo.toml ┗ ...
- 
Write a basic TCP Server which can listen for TCP connections on 127.0.0.1:7878. For each incoming connection, read all of the input as a string, and send it back to the client.
- 
Change the TCP Server to depend upon the simple-dbcrate, using a relative path.
- 
Change your TCP Server to use your simple-dbcrate to parse the input, and provide an appropriate canned response.
- 
Set up a VecDequeand either push or pop from that queue, depending on the command you have received.
At every step, try out your program using a command-line TCP Client: you can either use nc, or netcat, or our supplied tools/tcp-client program.
Optional Tasks:
- Run cargo clippyon your codebase.
- Run cargo fmton your codebase.
- Wrap your VecDequeinto astruct Applicationwith a method that takes asimple-db::Commandand returns anOption<String>. Write some tests for it.
Help
Connecting over TCP/IP
Using nc, netcat or ncat
The nc, netcat, or ncat tools may be available on your macOS or Linux machine. They all work in a similar fashion.
$ echo "PUBLISH 1234" | nc 127.0.0.1 7878
The echo command adds a new-line character automatically. Use echo -n if you don't want it to add a new-line character.
Using our TCP Client
We have written a basic TCP Client which should work on any platform.
$ cd tools/tcp-client
$ cargo run -- "PUBLISH hello"
$ cargo run -- "RETRIEVE"
It automatically adds a newline character on to the end of every message you send. It is hard-coded to connect to a server at 127.0.0.1:7878.
Writing to a stream
If you want to write to an object that implements std::io::Write, you could use writeln!.
Solution
#![allow(unused)] fn main() { use std::io::prelude::*; use std::net::{TcpStream}; fn handle_client(mut stream: TcpStream) -> Result<(), std::io::Error> { let mut buffer = String::new(); stream.read_to_string(&mut buffer)?; println!("Received: {:?}", buffer); writeln!(stream, "Thank you for {buffer:?}!")?; Ok(()) } }
Writing a TCP Server
If you need a working example of a basic TCP Echo server, you can start with our template.
Solution
use std::io::prelude::*; use std::net::{TcpListener, TcpStream}; use std::time::Duration; const DEFAULT_TIMEOUT: Option<Duration> = Some(Duration::from_millis(1000)); fn main() -> std::io::Result<()> { let listener = TcpListener::bind("127.0.0.1:7878")?; // accept connections and process them one at a time for stream in listener.incoming() { match stream { Ok(stream) => { println!("Got client {:?}", stream.peer_addr()); if let Err(e) = handle_client(stream) { println!("Error handling client: {:?}", e); } } Err(e) => { println!("Error connecting: {:?}", e); } } } Ok(()) } /// Process a single connection from a single client. /// /// Drops the stream when it has finished. fn handle_client(mut stream: TcpStream) -> Result<(), std::io::Error> { stream.set_read_timeout(DEFAULT_TIMEOUT)?; stream.set_write_timeout(DEFAULT_TIMEOUT)?; let mut buffer = String::new(); stream.read_to_string(&mut buffer)?; println!("Received: {:?}", buffer); writeln!(stream, "Thank you for {buffer:?}!")?; Ok(()) }
Making a Workspace
Solution
A workspace file looks like:[workspace]
resolver= "2"
members = ["simple-db", "tcp-server"]
Each member is a folder containing a Cargo package (i.e. that contains a Cargo.toml file).
Handling Errors
Solution
In a binary program anyhow is a good way to handle top-level errors.
use std::io::Read;
fn handle_client(stream: &mut std::net::TcpStream) -> Result<(), anyhow::Error> {
    // This returns a `Result<(), std::io::Error>`, and the `std::io::Error` will auto-convert into an `anyhow::Error`.
    stream.read_to_string(&mut buffer)?;
    /// ... etc
    Ok(())    
}You could also write an enum Error which has a variant for std::io::Error and a variant for simple_db::Error, and suitable impl From<...> for Error blocks.
When handling a client, you could .unwrap() the function which handles the client, but do you want to quit the server if the client sends a malformed message? Perhaps you should catch the result with a match, and print an error to the console before moving on to the next client.
Solution
If you need it, we have provided a complete solution for this exercise.