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 ditutupServer 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
endServer 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);
});
}
}
| Pendekatan | Kapan digunakan |
|---|---|
| Thread-per-connection | Koneksi tidak terlalu banyak, logika sederhana |
| Thread pool | Koneksi sedang, ingin batasi resource |
| Non-blocking + manual poll | Kontrol penuh, tidak mau dependensi async |
| Async (tokio) | Ribuan koneksi, I/O-bound, skala produksi |
Ringkasan #
TcpListener::bind+incoming()untuk server,TcpStream::connectuntuk client. Keduanya bisa langsung digunakan denganBufReaderkarenaTcpStreammengimplementasikanReaddanWrite.- 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_timeoutdanwrite_timeoutdi 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 saatread()return 0.- UDP lebih cepat tapi tidak andal — gunakan untuk kasus yang toleran kehilangan paket (game, streaming, DNS, discovery). Selalu set
read_timeoutkarena 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.