Advanced Level

Network Programming

Chapter 17: Network Programming 🌐

Build robust network applications in Rust with powerful libraries and safe concurrency.

Introduction to Network Programming in Rust

Rust provides excellent tools for network programming with a focus on safety and performance. The ownership system prevents data races in concurrent network applications, and the type system catches many errors at compile time.

TCP Programming

Creating a TCP Server

use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};

fn handle_client(mut stream: TcpStream) -> std::io::Result<()> {
    let mut buffer = [0; 512];
    stream.read(&mut buffer)?;
    
    let response = b"HTTP/1.1 200 OK\r\n\r\nHello, World!";
    stream.write(response)?;
    stream.flush()?;
    
    Ok(())
}

fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:7878")?;
    
    for stream in listener.incoming() {
        let stream = stream?;
        handle_client(stream)?;
    }
    
    Ok(())
}

Creating a TCP Client

use std::net::TcpStream;
use std::io::{Read, Write};

fn main() -> std::io::Result<()> {
    let mut stream = TcpStream::connect("127.0.0.1:7878")?;
    
    let request = b"GET / HTTP/1.1\r\nHost: 127.0.0.1:7878\r\n\r\n";
    stream.write(request)?;
    
    let mut buffer = [0; 1024];
    let bytes_read = stream.read(&mut buffer)?;
    
    println!("Response: {}", String::from_utf8_lossy(&buffer[..bytes_read]));
    
    Ok(())
}

HTTP Programming with Hyper

Setting up Hyper

Add to Cargo.toml:

[dependencies]
hyper = { version = "1.0", features = ["full"] }
tokio = { version = "1", features = ["full"] }

Simple HTTP Server

use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{body::Bytes, Request, Response};
use hyper_util::rt::TokioIo;
use std::convert::Infallible;
use std::net::SocketAddr;
use tokio::net::TcpListener;

async fn hello(_: Request<hyper::body::Incoming>) -> Result<Response<Bytes>, Infallible> {
    Ok(Response::new(Bytes::from("Hello World!")))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let addr: SocketAddr = ([127, 0, 0, 1], 3000).into();
    
    let listener = TcpListener::bind(addr).await?;
    println!("Listening on http://{}", addr);
    
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        
        tokio::task::spawn(async move {
            if let Err(err) = http1::Builder::new()
                .serve_connection(io, service_fn(hello))
                .await
            {
                eprintln!("Error serving connection: {:?}", err);
            }
        });
    }
}

Async Programming with Tokio

Async Basics

use tokio::time::{sleep, Duration};

async fn say_hello() {
    println!("Hello");
    sleep(Duration::from_millis(100)).await;
    println!("World!");
}

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(say_hello());
    handle.await.unwrap();
}

Concurrent Network Requests

use tokio::time::{sleep, Duration};
use std::time::Instant;

async fn fetch_data(id: u32) -> String {
    // Simulate network delay
    sleep(Duration::from_millis(100)).await;
    format!("Data from source {}", id)
}

#[tokio::main]
async fn main() {
    let start = Instant::now();
    
    // Sequential execution
    let data1 = fetch_data(1).await;
    let data2 = fetch_data(2).await;
    let data3 = fetch_data(3).await;
    
    println!("Sequential: {}, {}, {}", data1, data2, data3);
    println!("Sequential time: {:?}", start.elapsed());
    
    let start = Instant::now();
    
    // Concurrent execution
    let handle1 = tokio::spawn(fetch_data(1));
    let handle2 = tokio::spawn(fetch_data(2));
    let handle3 = tokio::spawn(fetch_data(3));
    
    let data1 = handle1.await.unwrap();
    let data2 = handle2.await.unwrap();
    let data3 = handle3.await.unwrap();
    
    println!("Concurrent: {}, {}, {}", data1, data2, data3);
    println!("Concurrent time: {:?}", start.elapsed());
}

Building a REST API with Axum

Setting up Axum

Add to Cargo.toml:

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Simple REST API

use axum::{
    routing::{get, post},
    http::StatusCode,
    response::IntoResponse,
    Json, Router,
};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;

#[derive(Serialize, Deserialize, Clone)]
struct User {
    id: u64,
    name: String,
    email: String,
}

#[derive(Serialize, Deserialize)]
struct CreateUser {
    name: String,
    email: String,
}

// In-memory storage (in real app, you'd use a database)
static mut USERS: Vec<User> = Vec::new();

async fn get_users() -> impl IntoResponse {
    unsafe {
        Json(&USERS)
    }
}

async fn create_user(Json(payload): Json<CreateUser>) -> impl IntoResponse {
    let user = User {
        id: unsafe { USERS.len() as u64 + 1 },
        name: payload.name,
        email: payload.email,
    };
    
    unsafe {
        USERS.push(user.clone());
    }
    
    (StatusCode::CREATED, Json(user))
}

async fn get_user(id: u64) -> impl IntoResponse {
    unsafe {
        USERS.iter().find(|user| user.id == id)
            .map(|user| Json(user.clone()))
            .unwrap_or_else(|| Json(User { id: 0, name: String::new(), email: String::new() }))
    }
}

#[tokio::main]
async fn main() {
    // Initialize with some users
    unsafe {
        USERS.push(User { id: 1, name: "Alice".to_string(), email: "alice@example.com".to_string() });
        USERS.push(User { id: 2, name: "Bob".to_string(), email: "bob@example.com".to_string() });
    }
    
    let app = Router::new()
        .route("/users", get(get_users).post(create_user))
        .route("/users/:id", get(get_user));
    
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("Server running on http://{}", addr);
    
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

UDP Programming

UDP Server

use std::net::UdpSocket;

fn main() -> std::io::Result<()> {
    let socket = UdpSocket::bind("127.0.0.1:34254")?;
    
    loop {
        let mut buf = [0; 1024];
        let (amt, src) = socket.recv_from(&mut buf)?;
        
        let buf = &mut buf[..amt];
        buf.reverse();
        
        socket.send_to(buf, &src)?;
    }
}

UDP Client

use std::net::UdpSocket;

fn main() -> std::io::Result<()> {
    let socket = UdpSocket::bind("127.0.0.1:0")?;
    socket.connect("127.0.0.1:34254")?;
    
    let message = b"Hello, UDP server!";
    socket.send(message)?;
    
    let mut buf = [0; 1024];
    let amt = socket.recv(&mut buf)?;
    
    println!("Response: {}", String::from_utf8_lossy(&buf[..amt]));
    
    Ok(())
}

Practical Examples

Example 1: Concurrent Web Scraper

// Add to Cargo.toml:
// reqwest = { version = "0.11", features = ["json"] }
// tokio = { version = "1", features = ["full"] }

use reqwest;
use tokio;

async fn fetch_url(url: &str) -> Result<String, reqwest::Error> {
    let response = reqwest::get(url).await?;
    let body = response.text().await?;
    Ok(body)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let urls = vec![
        "https://httpbin.org/delay/1",
        "https://httpbin.org/delay/2",
        "https://httpbin.org/delay/3",
    ];
    
    let start = std::time::Instant::now();
    
    // Concurrent requests
    let handles: Vec<_> = urls.into_iter()
        .map(|url| tokio::spawn(fetch_url(url)))
        .collect();
    
    for handle in handles {
        let result = handle.await?;
        match result {
            Ok(body) => println!("Fetched {} characters", body.len()),
            Err(e) => eprintln!("Error: {}", e),
        }
    }
    
    println!("Completed in {:?}", start.elapsed());
    Ok(())
}

Example 2: Chat Server

use std::collections::HashMap;
use std::net::{TcpListener, TcpStream};
use std::sync::{Arc, Mutex};
use std::thread;
use std::io::{Read, Write};

type Clients = Arc<Mutex<HashMap<String, TcpStream>>>;

fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:7878")?;
    let clients: Clients = Arc::new(Mutex::new(HashMap::new()));
    
    println!("Chat server running on 127.0.0.1:7878");
    
    for stream in listener.incoming() {
        let stream = stream?;
        let clients = Arc::clone(&clients);
        
        thread::spawn(move || {
            handle_client(stream, clients);
        });
    }
    
    Ok(())
}

fn handle_client(mut stream: TcpStream, clients: Clients) {
    let client_addr = stream.peer_addr().unwrap().to_string();
    
    // Add client to the list
    clients.lock().unwrap().insert(client_addr.clone(), stream.try_clone().unwrap());
    
    loop {
        let mut buffer = [0; 512];
        match stream.read(&mut buffer) {
            Ok(0) => {
                // Client disconnected
                clients.lock().unwrap().remove(&client_addr);
                break;
            }
            Ok(_) => {
                let message = String::from_utf8_lossy(&buffer);
                broadcast_message(&message, &clients, &client_addr);
            }
            Err(_) => {
                clients.lock().unwrap().remove(&client_addr);
                break;
            }
        }
    }
}

fn broadcast_message(message: &str, clients: &Clients, sender: &str) {
    let clients_map = clients.lock().unwrap();
    for (addr, mut client_stream) in clients_map.iter() {
        if addr != sender {
            // Send message to all other clients
            let _ = client_stream.write(message.as_bytes());
        }
    }
}

Performance Optimization in Network Programming

Connection Pooling

// Add to Cargo.toml:
// reqwest = { version = "0.11", features = ["json"] }
// tokio = { version = "1", features = ["full"] }

use reqwest;
use std::time::Instant;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create a client with connection pooling
    let client = reqwest::Client::builder()
        .pool_max_idle_per_host(20)
        .build()?;
    
    let start = Instant::now();
    
    // Make multiple requests using the same client
    for i in 0..100 {
        let url = format!("https://httpbin.org/get?param={}", i);
        let _ = client.get(&url).send().await?;
    }
    
    println!("Completed 100 requests in {:?}", start.elapsed());
    Ok(())
}

Zero-Copy Networking

use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};

fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:7878")?;
    
    for stream in listener.incoming() {
        let mut stream = stream?;
        let mut buffer = [0; 1024];
        
        loop {
            let bytes_read = stream.read(&mut buffer)?;
            if bytes_read == 0 {
                break;
            }
            
            // Zero-copy: directly write the same buffer back
            stream.write_all(&buffer[..bytes_read])?;
        }
    }
    
    Ok(())
}

Low-Level Network Programming

Raw Socket Example

// This example requires unsafe code and may need root privileges
use std::net::UdpSocket;
use std::mem;

#[repr(C)]
struct PacketHeader {
    src_port: u16,
    dst_port: u16,
    length: u16,
    checksum: u16,
}

fn main() -> std::io::Result<()> {
    let socket = UdpSocket::bind("0.0.0.0:0")?;
    
    // Enable raw socket access (platform-specific)
    // This is unsafe and requires appropriate permissions
    unsafe {
        // Configure socket for raw access
        // Implementation depends on the OS
    }
    
    Ok(())
}

Memory-Mapped Network Buffers

use memmap2::MmapMut;
use std::fs::OpenOptions;
use std::net::TcpStream;
use std::io::Write;

fn main() -> std::io::Result<()> {
    // Create a memory-mapped file for network buffer
    let file = OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .open("network_buffer.dat")?;
    
    file.set_len(4096)?;
    
    let mut mmap = unsafe { MmapMut::map_mut(&file)? };
    
    // Write data to memory-mapped buffer
    mmap[0..13].copy_from_slice(b"Hello, World!");
    
    // Flush changes to disk
    mmap.flush()?;
    
    // Send over network
    let mut stream = TcpStream::connect("127.0.0.1:7878")?;
    stream.write_all(&mmap[0..13])?;
    
    Ok(())
}

Common Mistakes

āŒ Blocking in Async Context

// Don't do this in async functions
async fn bad_example() {
    // This blocks the entire async runtime!
    std::thread::sleep(std::time::Duration::from_secs(1));
}

āœ… Proper Async Delay

// Do this instead
async fn good_example() {
    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}

āŒ Forgetting Error Handling

// Don't ignore errors
async fn another_bad_example() {
    let _response = reqwest::get("https://example.com").await.unwrap();
}

āœ… Proper Error Handling

// Handle errors properly
async fn good_error_handling() -> Result<(), reqwest::Error> {
    let response = reqwest::get("https://example.com").await?;
    // Process response...
    Ok(())
}

Key Takeaways

  • āœ… Rust's ownership system prevents data races in concurrent network applications
  • āœ… Use tokio for async network programming
  • āœ… Libraries like hyper and axum provide excellent HTTP capabilities
  • āœ… Connection pooling can significantly improve performance
  • āœ… Zero-copy techniques reduce memory overhead
  • āœ… Raw sockets require unsafe code but provide low-level control
  • āœ… Memory mapping can optimize large data transfers
  • āœ… Always handle network errors properly
  • āœ… Follow Rust naming conventions and idioms for network code

Ready for Chapter 18? → Advanced Traits

šŸ¦€ Rust Programming Tutorial

Learn from Zero to Advanced

Built with Next.js and Tailwind CSS • Open Source