Network Programming with Rust
repository·master·Indexed 43 days ago
https://github.com/packtpublishing/network-programming-with-rustComplete source code and educational projects for the book Network Programming with Rust. Covers networking concepts from low-level socket programming and TCP/UDP to asynchronous networking with the Tokio stack, DNS, TLS, and high-level web servers using Rocket. Includes practical examples using crates such as mio, pnet, ipnetwork, trust-dns-resolver, and nom.
What's inside Network Programming with Rust
- This repository contains the complete source code and supporting project files for the book Network Programming with Rust. It is designed to guide developers through building networking applications using Rust, covering topics from basic socket programming to asynchronous networking with the Tokio stack, DNS, TCP/UDP, and security primitives like TLS.
Run the rocket-blog server
masterTo start the Rocket server, use
cargo run. You must provide theDATABASE_URLenvironment variable so the application can connect to the database.DATABASE_URL=db.sql cargo runSetup the database for the rocket-blog project
masterTo initialize the database schema using Diesel migrations, run the following command from the project root. Ensure the
DATABASE_URLenvironment variable points to your SQL file.DATABASE_URL=db.sql diesel migration runNavigate the project code structure
masterThe repository is organized into numbered directories corresponding to the chapters in the book. Each directory contains a standalone Rust project. For example, code related to Chapter 2 is located in theChapter02folder. Note thatChapter 1contains no code as it focuses on introductory concepts.Send requests using CollatzInput
masterWhen calling the
Clientservice, you must wrap your data in theCollatzInputenum. This allows you to choose between a single message or a continuous stream of data.CollatzInput::Once(String): Sends a single string message.CollatzInput::Stream(CollatzStream): Sends a stream of strings.
// Example of sending a single value client.call(CollatzInput::Once("42".to_string())); // Example of sending a stream client.call(CollatzInput::Stream(my_collatz_stream));Handle Nom parser results
masterWhen using
nomparsers, the result is wrapped in anIResultenum. You should handle the three possible outcomes:IResult::Done(rest, value): The parser successfully parsed the input.restcontains the remaining unparsed input, andvaluecontains the parsed data.IResult::Error(err): The parser encountered an error (e.g., a tag mismatch).IResult::Incomplete(needed): The parser requires more input to complete the operation.
match parse_request(input.as_bytes()) { IResult::Done(rest, value) => println!("Rest: {:?} Value: {:?}", rest, value), IResult::Error(err) => println!("{:?}", err), IResult::Incomplete(needed) => println!("{:?}", needed) }Handle gRPC response results
masterWhen calling gRPC methods, the returned response object is asynchronous. You must call
.wait()on the response to block and retrieve the result.Successful responses via
.wait()return a tuple in theOkvariant:Ok((metadata, response_body, status)).metadata: The call metadata.response_body: The actual deserialized message returned by the server.status: The gRPC status code.
If the call fails,
.wait()returns anErr(e)containing the error details.// Pattern for handling gRPC responses let resp = client.method_name(grpc::RequestOptions::new(), request); match resp.wait() { Err(e) => panic!("{:?}", e), Ok((_, response, _)) => println!("{:?}", response), }Understand the TCP Echo Server logic
masterThe server operates using the following logic:
- Binding: It binds to
127.0.0.1:8888usingstd::net::TcpListener. - Concurrency: It uses
listener.incoming()to accept connections. Each successful connection is moved into a new thread viathread::spawnto allow multiple clients to be handled simultaneously. - Client Handling: The
handle_clientfunction reads data from theTcpStreaminto a 512-byte buffer. - Random Delay: Before writing the data back, the server calculates a random
Durationusingrand::thread_rng().choose(&[0, 1, 2, 3, 4, 5])and sleeps for that amount of time. - Echo: The exact bytes read are written back to the stream using
stream.write().
// Core logic snippet from handle_client let sleep = Duration::from_secs(*thread_rng().choose(&[0, 1, 2, 3, 4, 5]).unwrap()); std::thread::sleep(sleep); stream.write(&buf[..bytes_read])?;- Binding: It binds to
Implement a basic Mio event loop with TcpListener
masterTo create a non-blocking TCP server using
mio, follow these steps:- Bind the Listener: Use
mio::tcp::TcpListener::bindto bind to aSocketAddr. - Initialize Poll: Create a new event loop instance using
Poll::new(). - Register the Listener: Register the
TcpListenerwith thePollinstance. Use a uniqueTokento identify the listener in the event loop and specify the interest (e.g.,Ready::readable()). UsingPollOpt::edge()enables edge-triggered notifications. - Event Loop: Create an
Eventscollection and enter a loop callingpoll.poll(&mut events, None). - Handle Events: Iterate through the returned events and match on the
Token. For a server listener token, callserver.accept()to handle new connections.
use mio::*; use mio::tcp::TcpListener; const SERVER: Token = Token(0); // ... inside run() method ... let server = TcpListener::bind(&address).unwrap(); let poll = Poll::new().unwrap(); poll.register(&server, SERVER, Ready::readable(), PollOpt::edge()).unwrap(); let mut events = Events::with_capacity(1024); loop { poll.poll(&mut events, None).unwrap(); for event in events.iter() { match event.token() { SERVER => { let (_stream, remote) = server.accept().unwrap(); println!("Connection from {}", remote); } _ => {} } } }- Bind the Listener: Use
Scale a Hyper server across multiple CPU cores
masterTo maximize throughput, you can scale a
hyperserver by spawning multiple worker threads, each running its own event loop (tokio_core::reactor::Core).- Use
net2::TcpBuilderwith.reuse_port(true)to allow multiple processes/threads to bind to the same address. - Use
start_serverto spawnnum_cpus::get() - 1threads, each running theservefunction. - The main thread then runs the final instance of
serve.
This pattern utilizes
Arc<Http>to share the protocol configuration across all worker threads.fn start_server(num: usize, addr: &str) { let addr = addr.parse().unwrap(); let protocol = Arc::new(Http::new()); { for _ in 0..num - 1 { let protocol = Arc::clone(&protocol); thread::spawn(move || serve(&addr, &protocol)); } } serve(&addr, &protocol); } fn main() { // Scales to the number of available CPUs start_server(num_cpus::get(), "0.0.0.0:3000"); }- Use
Configure and start a May HTTP server
masterYou can configure the
mayruntime and start anHttpServerby following these steps:- Configure IO Workers: Use
may::config().set_io_workers(n)to set the number of IO workers (typically matching the number of CPU cores). - Initialize Server: Instantiate
HttpServerwith yourHttpServiceimplementation and call.start("address:port"). - Run: Call
.join()on the server instance to block the main thread and keep the server running.
Note:
.start()and.join()returnResulttypes and should be handled with.unwrap()or proper error handling.fn main() { // Set IO workers to the number of available CPUs may::config().set_io_workers(num_cpus::get()); // Start the server on 0.0.0.0:3000 with the Echo service let server = HttpServer(Echo).start("0.0.0.0:3000").unwrap(); // Wait for the server to finish server.join().unwrap(); }- Configure IO Workers: Use
Configure the DATABASE_URL environment variable
masterThe
rocket-blogapplication uses a connection pool for SQLite viar2d2. To initialize the database pool, you must set theDATABASE_URLenvironment variable. The application usesdotenvto load this variable from a.envfile if present.# Example .env file content DATABASE_URL=example.db