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 ditutupSimple 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
endUDP 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);
});
}
}
| Approach | When to use |
|---|---|
| Threads-per-connection | Not too many connections, simple logic |
| Thread pool | Medium connection, want to limit resources |
| Non-blocking + manual poll | Full control, no async dependencies |
| Async (tokio) | Thousands of connections, I/O-bound, production scale |
Summary #
TcpListener::bind+incoming()for server,TcpStream::connectfor client. Both can be used directly withBufReaderbecauseTcpStreamimplementsReadandWrite.- 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_timeoutandwrite_timeoutin 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 whenread()returns 0.- UDP is faster but not reliable — use for packet loss tolerant cases (gaming, streaming, DNS, discovery). Always set
read_timeoutbecause 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.