Network Programming with Rust

repository·master·Indexed 43 days ago

https://github.com/packtpublishing/network-programming-with-rust

Complete 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.

Tokens
34.7K
Snippets
103
Records
107
Agent score
54%

What's inside Network Programming with Rust

  1. Overview of Network Programming with Rust repository

    master
    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.
  2. Send requests using CollatzInput

    master

    When calling the Client service, you must wrap your data in the CollatzInput enum. 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));
  3. Handle Nom parser results

    master

    When using nom parsers, the result is wrapped in an IResult enum. You should handle the three possible outcomes:

    1. IResult::Done(rest, value): The parser successfully parsed the input. rest contains the remaining unparsed input, and value contains the parsed data.
    2. IResult::Error(err): The parser encountered an error (e.g., a tag mismatch).
    3. 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)
    }
  4. Handle gRPC response results

    master

    When 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 the Ok variant: 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 an Err(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),
    }
  5. Understand the TCP Echo Server logic

    master

    The server operates using the following logic:

    1. Binding: It binds to 127.0.0.1:8888 using std::net::TcpListener.
    2. Concurrency: It uses listener.incoming() to accept connections. Each successful connection is moved into a new thread via thread::spawn to allow multiple clients to be handled simultaneously.
    3. Client Handling: The handle_client function reads data from the TcpStream into a 512-byte buffer.
    4. Random Delay: Before writing the data back, the server calculates a random Duration using rand::thread_rng().choose(&[0, 1, 2, 3, 4, 5]) and sleeps for that amount of time.
    5. 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])?;
  6. Implement a basic Mio event loop with TcpListener

    master

    To create a non-blocking TCP server using mio, follow these steps:

    1. Bind the Listener: Use mio::tcp::TcpListener::bind to bind to a SocketAddr.
    2. Initialize Poll: Create a new event loop instance using Poll::new().
    3. Register the Listener: Register the TcpListener with the Poll instance. Use a unique Token to identify the listener in the event loop and specify the interest (e.g., Ready::readable()). Using PollOpt::edge() enables edge-triggered notifications.
    4. Event Loop: Create an Events collection and enter a loop calling poll.poll(&mut events, None).
    5. Handle Events: Iterate through the returned events and match on the Token. For a server listener token, call server.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);
                }
                _ => {}
            }
        }
    }
  7. Scale a Hyper server across multiple CPU cores

    master

    To maximize throughput, you can scale a hyper server by spawning multiple worker threads, each running its own event loop (tokio_core::reactor::Core).

    1. Use net2::TcpBuilder with .reuse_port(true) to allow multiple processes/threads to bind to the same address.
    2. Use start_server to spawn num_cpus::get() - 1 threads, each running the serve function.
    3. 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");
    }
  8. Configure and start a May HTTP server

    master

    You can configure the may runtime and start an HttpServer by following these steps:

    1. Configure IO Workers: Use may::config().set_io_workers(n) to set the number of IO workers (typically matching the number of CPU cores).
    2. Initialize Server: Instantiate HttpServer with your HttpService implementation and call .start("address:port").
    3. Run: Call .join() on the server instance to block the main thread and keep the server running.

    Note: .start() and .join() return Result types 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();
    }