Sockets #

Socket is a communication mechanism between processes over a network. std::net in Rust provides TcpListener, TcpStream, and UdpSocket — enough networking primitives to build many applications without external dependencies. The traits of Read and Write discussed in the I/O article apply directly here: TcpStream implements both, so all the same buffering techniques can be used. This article covers TCP from simple servers to true concurrent servers, UDP for connectionless communications, important socket options like timeout and SO_REUSEADDR, and async TCP with tokio for high scale.

TCP — Reliable Transport Layer #

TCP guarantees delivery sequence, packet loss detection, and automatic retransmission. This is the default choice for almost all network applications that require reliability.

sequenceDiagram
    participant S as Server
    participant C as Client

    S->>S: TcpListener::bind(addr)
    S->>S: listener.incoming() — menunggu

    C->>S: TcpStream::connect(addr)
    S->>S: accept() → TcpStream
    Note over S,C: Koneksi TCP terbentuk

    C->>S: stream.write(data)
    S->>S: stream.read(buf)
    S->>C: stream.write(respons)
    C->>C: stream.read(buf)

    C->>C: drop(stream) — FIN
    S->>S: read() → 0 byte — koneksi ditutup

Simple TCP Server #

use std::io::{self, BufRead, BufReader, Write};
use std::net::TcpListener;

fn main() -> io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:7878")?;
    println!("Server berjalan di 127.0.0.1:7878");

    for koneksi in listener.incoming() {
        let mut stream = koneksi?;
        let alamat = stream.peer_addr()?;
        println!("Koneksi dari: {}", alamat);

        // Use BufReader for line-by-line reads — more efficient than byte-by-byte reads
        let mut reader = BufReader::new(stream.try_clone()?);
        let mut baris = String::new();
        reader.read_line(&mut baris)?;
        let pesan = baris.trim();
        println!("Diterima dari {}: '{}'", alamat, pesan);

        // Send response
        let respons = format!("Echo: {}\n", pesan);
        stream.write_all(respons.as_bytes())?;
    }

    Ok(())
}

Client TCP #

use std::io::{self, BufRead, BufReader, Write};
use std::net::TcpStream;

fn main() -> io::Result<()> {
    let mut stream = TcpStream::connect("127.0.0.1:7878")?;
    println!("Terhubung ke server");

    // Send message
    stream.write_all(b"Halo dari client!\n")?;

    // Read the response with BufReader
    let mut reader = BufReader::new(stream.try_clone()?);
    let mut respons = String::new();
    reader.read_line(&mut respons)?;
    println!("Respons: {}", respons.trim());

    Ok(())
}

Multi-Client Server with Threads #

The server above only serves one client at a time — the next one has to wait. To serve multiple clients simultaneously, each connection needs to be handled in a separate thread:

use std::io::{self, BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;

fn tangani_client(mut stream: TcpStream) -> io::Result<()> {
    let alamat = stream.peer_addr()?;
    println!("[{}] Terhubung", alamat);

    let reader_stream = stream.try_clone()?;
    let mut reader = BufReader::new(reader_stream);

    loop {
        let mut baris = String::new();
        let dibaca = reader.read_line(&mut baris)?;

        // 0 bytes read = client closed connection
        if dibaca == 0 {
            println!("[{}] Koneksi ditutup", alamat);
            break;
        }

        let pesan = baris.trim();
        println!("[{}] → '{}'", alamat, pesan);

        // Echo back + uppercase
        let respons = format!("{}\n", pesan.to_uppercase());
        stream.write_all(respons.as_bytes())?;

        // Exit the loop if the client sends "quit"
        if pesan.eq_ignore_ascii_case("quit") {
            break;
        }
    }

    Ok(())
}

fn main() -> io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:7878")?;
    println!("Server multi-client di 127.0.0.1:7878");

    for koneksi in listener.incoming() {
        match koneksi {
            Ok(stream) => {
                // Spawn a new thread for each client
                thread::spawn(move || {
                    if let Err(e) = tangani_client(stream) {
                        eprintln!("Error client: {}", e);
                    }
                });
            }
            Err(e) => eprintln!("Gagal menerima koneksi: {}", e),
        }
    }

    Ok(())
}

Thread Pool — Limit Number of Threads #

Unlimited spawn threads for each connection can consume memory when there are thousands of clients. Thread pool limits the number of threads and queues work:

use std::io::{self, BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::thread;

type Pekerjaan = Box<dyn FnOnce() + Send + 'static>;

struct ThreadPool {
    workers: Vec<thread::JoinHandle<()>>,
    sender: mpsc::Sender<Pekerjaan>,
}

impl ThreadPool {
    fn baru(ukuran: usize) -> Self {
        let (tx, rx) = mpsc::channel::<Pekerjaan>();
        let rx = Arc::new(Mutex::new(rx));
        let mut workers = Vec::with_capacity(ukuran);

        for id in 0..ukuran {
            let rx = Arc::clone(&rx);
            let handle = thread::spawn(move || loop {
                let pekerjaan = rx.lock().unwrap().recv();
                match pekerjaan {
                    Ok(f) => {
                        println!("Worker {}: mengerjakan tugas", id);
                        f();
                    }
                    Err(_) => {
                        println!("Worker {}: channel ditutup, berhenti", id);
                        break;
                    }
                }
            });
            workers.push(handle);
        }

        ThreadPool { workers, sender: tx }
    }

    fn jalankan<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        self.sender.send(Box::new(f)).unwrap();
    }
}

fn main() -> io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:7878")?;
    // Limit to 4 threads — suitable for moderately loaded servers
    let pool = ThreadPool::baru(4);
    println!("Server thread pool (4 worker) di 127.0.0.1:7878");

    for koneksi in listener.incoming() {
        if let Ok(stream) = koneksi {
            pool.jalankan(move || {
                tangani_client_sederhana(stream);
            });
        }
    }

    Ok(())
}

fn tangani_client_sederhana(mut stream: TcpStream) {
    let mut buf = [0u8; 512];
    if let Ok(n) = stream.read(&mut buf) {
        let _ = stream.write_all(&buf[..n]);
    }
}

use std::io::Read;

Socket Options — Timeout and Configuration #

Read and Write Timeout #

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

fn buat_koneksi_dengan_timeout(addr: &str) -> io::Result<TcpStream> {
    // connect_timeout — timeout when connecting
    let addr: std::net::SocketAddr = addr.parse().unwrap();
    let stream = TcpStream::connect_timeout(&addr, Duration::from_secs(5))?;

    // Timeout for read operations — WouldBlock/TimedOut error if limit exceeded
    stream.set_read_timeout(Some(Duration::from_secs(10)))?;

    // Timeout for write operations
    stream.set_write_timeout(Some(Duration::from_secs(5)))?;

    Ok(stream)
}

fn main() -> io::Result<()> {
    match buat_koneksi_dengan_timeout("127.0.0.1:7878") {
        Ok(mut stream) => {
            stream.write_all(b"ping\n")?;

            let mut buf = [0u8; 64];
            match stream.read(&mut buf) {
                Ok(n) => println!("Respons: {}", String::from_utf8_lossy(&buf[..n])),
                Err(e) if e.kind() == io::ErrorKind::TimedOut => {
                    println!("Timeout — server tidak merespons");
                }
                Err(e) => return Err(e),
            }
        }
        Err(e) if e.kind() == io::ErrorKind::TimedOut => {
            println!("Gagal terhubung: timeout");
        }
        Err(e) => return Err(e),
    }

    Ok(())
}

SO_REUSEADDR — Restart Server Without Waiting #

use std::net::TcpListener;

fn main() -> std::io::Result<()> {
    use std::net::SocketAddr;

    // Without SO_REUSEADDR: "address already in use" error on fast restart
    // Rust enables SO_REUSEADDR by default in TcpListener::bind()
    // so you can restart the server without waiting for TIME_WAIT to complete

    let listener = TcpListener::bind("0.0.0.0:8080")?;
    println!("Mendengarkan di port 8080 (semua interface)");

    // local_addr() to find out the actual port (useful if bind to port 0)
    let port = listener.local_addr()?.port();
    println!("Port aktual: {}", port);

    Ok(())
}

UDP — Connectionless Communication #

UDP has no handshake, no delivery guarantee, no sequencing — but it’s much faster and lighter. Suitable for gaming, media streaming, DNS, and packet loss tolerant protocols.

flowchart LR
    subgraph UDP
        CS["Client\nUdpSocket::bind(:0)\nsend_to(data, server)"]
        SS["Server\nUdpSocket::bind(:port)\nrecv_from(&buf)"]
        CS -- "paket (tidak ada koneksi)" --> SS
        SS -- "send_to(respons, client)" --> CS
    end

UDP Server #

use std::net::UdpSocket;

fn main() -> std::io::Result<()> {
    let socket = UdpSocket::bind("127.0.0.1:9000")?;
    println!("Server UDP di 127.0.0.1:9000");

    let mut buf = [0u8; 1024];

    loop {
        let (jumlah, sumber) = socket.recv_from(&mut buf)?;
        let pesan = String::from_utf8_lossy(&buf[..jumlah]);
        println!("Dari {}: '{}'", sumber, pesan.trim());

        // Send response to sender
        let respons = format!("ACK: {}", pesan.trim());
        socket.send_to(respons.as_bytes(), sumber)?;

        if pesan.trim() == "stop" {
            println!("Server berhenti");
            break;
        }
    }

    Ok(())
}

UDP Client #

use std::net::UdpSocket;
use std::time::Duration;

fn main() -> std::io::Result<()> {
    // Bind to port 0 — OS selects available port
    let socket = UdpSocket::bind("127.0.0.1:0")?;
    socket.set_read_timeout(Some(Duration::from_secs(3)))?;

    let server = "127.0.0.1:9000";
    let pesan_list = ["Halo", "Dunia", "stop"];

    for pesan in &pesan_list {
        socket.send_to(pesan.as_bytes(), server)?;
        println!("Dikirim: '{}'", pesan);

        let mut buf = [0u8; 1024];
        match socket.recv_from(&mut buf) {
            Ok((n, dari)) => {
                println!("Dari {}: '{}'", dari, String::from_utf8_lossy(&buf[..n]));
            }
            Err(e) if e.kind() == std::io::ErrorKind::TimedOut => {
                println!("Timeout — tidak ada respons");
            }
            Err(e) => return Err(e),
        }
    }

    Ok(())
}

UDP Broadcast #

use std::net::UdpSocket;

fn main() -> std::io::Result<()> {
    let socket = UdpSocket::bind("0.0.0.0:0")?;

    // Enable broadcast — needs to be explicit in Rust
    socket.set_broadcast(true)?;

    // Send to entire local network (broadcast subnet)
    let pesan = b"Halo semua di jaringan!";
    socket.send_to(pesan, "255.255.255.255:9001")?;
    println!("Broadcast dikirim");

    Ok(())
}

Non-Blocking Socket #

Non-blocking mode makes read/write/accept operations immediately return WouldBlock instead of waiting — useful for multiplexing multiple sockets in one thread without OS-level async:

use std::io::{self, Read};
use std::net::TcpListener;

fn main() -> io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:7878")?;

    // Set non-blocking on listener
    listener.set_nonblocking(true)?;
    println!("Server non-blocking di 127.0.0.1:7878");

    let mut koneksi_aktif = Vec::new();

    loop {
        // Accept new connections — return immediately if there are none
        match listener.accept() {
            Ok((stream, addr)) => {
                println!("Koneksi baru dari {}", addr);
                stream.set_nonblocking(true)?;
                koneksi_aktif.push(stream);
            }
            Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
                // No new connections — continue existing process
            }
            Err(e) => return Err(e),
        }

        // Process all active connections
        koneksi_aktif.retain_mut(|stream| {
            let mut buf = [0u8; 512];
            match stream.read(&mut buf) {
                Ok(0) => false, // Connection closed — remove from list
                Ok(n) => {
                    println!("Data: {}", String::from_utf8_lossy(&buf[..n]));
                    true
                }
                Err(e) if e.kind() == io::ErrorKind::WouldBlock => true, // No data yet
                Err(_) => false, // Error — remove from list
            }
        });

        // Take a short nap so you don't get busy-looping
        std::thread::sleep(std::time::Duration::from_millis(10));
    }
}

Async TCP with Tokio #

For servers that need to handle thousands of concurrent connections, async is much more efficient than thread-per-connection — a single OS thread can manage thousands of async tasks:

use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpListener;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("127.0.0.1:7878").await?;
    println!("Async server di 127.0.0.1:7878");

    loop {
        let (socket, addr) = listener.accept().await?;
        println!("Koneksi async dari {}", addr);

        // Spawn task async — lightweight compared to OS threads
        tokio::spawn(async move {
            let (reader, mut writer) = socket.into_split();
            let mut lines = BufReader::new(reader).lines();

            while let Ok(Some(baris)) = lines.next_line().await {
                println!("[{}] → '{}'", addr, baris);
                let respons = format!("ECHO: {}\n", baris);
                if writer.write_all(respons.as_bytes()).await.is_err() {
                    break;
                }
                if baris.eq_ignore_ascii_case("quit") {
                    break;
                }
            }
            println!("[{}] Disconnected", addr);
        });
    }
}
ApproachWhen to use
Threads-per-connectionNot too many connections, simple logic
Thread poolMedium connection, want to limit resources
Non-blocking + manual pollFull control, no async dependencies
Async (tokio)Thousands of connections, I/O-bound, production scale

Summary #

  • TcpListener::bind + incoming() for server, TcpStream::connect for client. Both can be used directly with BufReader because TcpStream implements Read and Write.
  • Read with BufReader + read_line — much more efficient than byte-by-byte read, and natural for row-based protocols.
  • try_clone() to get two handles to the same socket — one for reading, one for writing from different threads.
  • Spawn thread per client for simple concurrency, thread pool to limit resources. For high scale, use async tokio.
  • Always set read_timeout and write_timeout in production — without them, operations can wait forever if the client crashes or the network goes down.
  • 0 byte dibaca = koneksi ditutup — important pattern when reading in loop; make sure to exit the loop when read() returns 0.
  • UDP is faster but not reliable — use for packet loss tolerant cases (gaming, streaming, DNS, discovery). Always set read_timeout because there is no connection lost notification.
  • set_broadcast(true) required before UDP broadcast — off by default.
  • Async tokio for production scale — a single thread can handle thousands of concurrent connections without the overhead of OS threading.


← Previous: I/O   Next: Web Socket →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact