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
tokiofor async network programming - ā
Libraries like
hyperandaxumprovide 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