Socket

Socket #

Socket adalah mekanisme komunikasi antar proses melalui jaringan. std::net di Rust menyediakan TcpListener, TcpStream, dan UdpSocket — primitif jaringan yang cukup untuk membangun banyak aplikasi tanpa dependensi eksternal. Trait Read dan Write yang sudah dibahas di artikel I/O berlaku langsung di sini: TcpStream mengimplementasikan keduanya, sehingga semua teknik buffering yang sama bisa digunakan. Artikel ini membahas TCP dari server sederhana hingga server concurrent yang benar, UDP untuk komunikasi connectionless, opsi socket penting seperti timeout dan SO_REUSEADDR, dan async TCP dengan tokio untuk skala tinggi.

TCP — Transport Layer yang Andal #

TCP menjamin urutan pengiriman, deteksi kehilangan paket, dan retransmisi otomatis. Ini adalah pilihan default untuk hampir semua aplikasi jaringan yang membutuhkan keandalan.

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

Server TCP Sederhana #

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);

        // Gunakan BufReader untuk baca per baris — lebih efisien dari baca byte per byte
        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);

        // Kirim respons
        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");

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

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

    Ok(())
}

Server Multi-Client dengan Thread #

Server di atas hanya melayani satu client sekaligus — yang berikutnya harus menunggu. Untuk melayani banyak client secara bersamaan, setiap koneksi perlu ditangani di thread terpisah:

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 byte dibaca = client menutup koneksi
        if dibaca == 0 {
            println!("[{}] Koneksi ditutup", alamat);
            break;
        }

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

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

        // Keluar dari loop jika client kirim "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 thread baru untuk setiap 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 — Batasi Jumlah Thread #

Spawn thread tanpa batas untuk setiap koneksi bisa menghabiskan memori saat ada ribuan client. Thread pool membatasi jumlah thread dan mengantri pekerjaan:

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")?;
    // Batasi ke 4 thread — cocok untuk server dengan beban sedang
    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 dan Konfigurasi #

Timeout Baca dan Tulis #

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 — batas waktu saat menghubungkan
    let addr: std::net::SocketAddr = addr.parse().unwrap();
    let stream = TcpStream::connect_timeout(&addr, Duration::from_secs(5))?;

    // Timeout untuk operasi baca — error WouldBlock/TimedOut jika melewati batas
    stream.set_read_timeout(Some(Duration::from_secs(10)))?;

    // Timeout untuk operasi tulis
    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 Tanpa Tunggu #

use std::net::TcpListener;

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

    // Tanpa SO_REUSEADDR: error "address already in use" saat restart cepat
    // Rust mengaktifkan SO_REUSEADDR secara default di TcpListener::bind()
    // sehingga kamu bisa restart server tanpa menunggu TIME_WAIT selesai

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

    // local_addr() untuk tahu port aktual (berguna jika bind ke port 0)
    let port = listener.local_addr()?.port();
    println!("Port aktual: {}", port);

    Ok(())
}

UDP — Komunikasi Connectionless #

UDP tidak ada handshake, tidak ada jaminan pengiriman, tidak ada urutan — tapi jauh lebih cepat dan lebih ringan. Cocok untuk game, streaming media, DNS, dan protokol yang toleran terhadap kehilangan paket.

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

Server UDP #

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());

        // Kirim respons ke pengirim
        let respons = format!("ACK: {}", pesan.trim());
        socket.send_to(respons.as_bytes(), sumber)?;

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

    Ok(())
}

Client UDP #

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

fn main() -> std::io::Result<()> {
    // Bind ke port 0 — OS memilih port yang tersedia
    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")?;

    // Aktifkan broadcast — perlu eksplisit di Rust
    socket.set_broadcast(true)?;

    // Kirim ke seluruh jaringan lokal (subnet broadcast)
    let pesan = b"Halo semua di jaringan!";
    socket.send_to(pesan, "255.255.255.255:9001")?;
    println!("Broadcast dikirim");

    Ok(())
}

Non-Blocking Socket #

Mode non-blocking membuat operasi read/write/accept langsung return WouldBlock alih-alih menunggu — berguna untuk multiplexing banyak socket dalam satu thread tanpa 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 pada listener
    listener.set_nonblocking(true)?;
    println!("Server non-blocking di 127.0.0.1:7878");

    let mut koneksi_aktif = Vec::new();

    loop {
        // Terima koneksi baru — langsung return jika tidak ada
        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 => {
                // Tidak ada koneksi baru — lanjut proses yang ada
            }
            Err(e) => return Err(e),
        }

        // Proses semua koneksi aktif
        koneksi_aktif.retain_mut(|stream| {
            let mut buf = [0u8; 512];
            match stream.read(&mut buf) {
                Ok(0) => false, // Koneksi ditutup — hapus dari list
                Ok(n) => {
                    println!("Data: {}", String::from_utf8_lossy(&buf[..n]));
                    true
                }
                Err(e) if e.kind() == io::ErrorKind::WouldBlock => true, // Belum ada data
                Err(_) => false, // Error — hapus dari list
            }
        });

        // Tidur sebentar agar tidak busy-loop
        std::thread::sleep(std::time::Duration::from_millis(10));
    }
}

Async TCP dengan Tokio #

Untuk server yang perlu menangani ribuan koneksi bersamaan, async jauh lebih efisien dari thread-per-connection — satu thread OS bisa mengelola ribuan task async:

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 — ringan dibanding thread OS
        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);
        });
    }
}
PendekatanKapan digunakan
Thread-per-connectionKoneksi tidak terlalu banyak, logika sederhana
Thread poolKoneksi sedang, ingin batasi resource
Non-blocking + manual pollKontrol penuh, tidak mau dependensi async
Async (tokio)Ribuan koneksi, I/O-bound, skala produksi

Ringkasan #

  • TcpListener::bind + incoming() untuk server, TcpStream::connect untuk client. Keduanya bisa langsung digunakan dengan BufReader karena TcpStream mengimplementasikan Read dan Write.
  • Baca dengan BufReader + read_line — jauh lebih efisien dari baca byte per byte, dan natural untuk protokol berbasis baris.
  • try_clone() untuk mendapat dua handle ke socket yang sama — satu untuk baca, satu untuk tulis dari thread berbeda.
  • Spawn thread per client untuk concurrency sederhana, thread pool untuk batasi resource. Untuk skala tinggi, gunakan async tokio.
  • Selalu set read_timeout dan write_timeout di produksi — tanpanya, operasi bisa menunggu selamanya jika client crash atau jaringan terputus.
  • 0 byte dibaca = koneksi ditutup — pola penting saat baca dalam loop; pastikan keluar dari loop saat read() return 0.
  • UDP lebih cepat tapi tidak andal — gunakan untuk kasus yang toleran kehilangan paket (game, streaming, DNS, discovery). Selalu set read_timeout karena tidak ada notifikasi koneksi terputus.
  • set_broadcast(true) wajib sebelum broadcast UDP — tidak aktif secara default.
  • Async tokio untuk skala produksi — satu thread bisa menangani ribuan koneksi bersamaan tanpa overhead pembuatan thread OS.

← Sebelumnya: I/O   Berikutnya: Web Socket →

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