Net #

Networking is one of Rust’s primary use cases — a language that promises C-level performance with compiler-guaranteed memory safety. std::net provides complete synchronous networking primitives: TCP for reliable and ordered communications, UDP for fast communications without delivery guarantees, and types for representing network addresses. Understanding std::net is an important foundation before stepping into async networking with Tokio or Hyper — many of the concepts are the same, just the execution model is different. This article discusses TCP servers and clients, UDP sockets, handling concurrent connections with threads, timeout management, and when synchronous networking is no longer enough.

Network Address Type #

Before creating a connection, Rust provides a type that represents the network address in a type-safe manner.

use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4};

fn main() {
    // IpAddr — can be IPv4 or IPv6
    let ipv4: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
    let ipv6: IpAddr = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));

    // Parse from string
    let ip: IpAddr = "192.168.1.1".parse().expect("IP tidak valid");
    let ip6: IpAddr = "::1".parse().expect("IPv6 tidak valid");

    // Type checking
    println!("{}", ipv4.is_ipv4());       // true
    println!("{}", ipv4.is_loopback());   // true (127.0.0.1)
    println!("{}", ipv4.is_private());    // false (127.x.x.x is not a private range)

    let private: IpAddr = "192.168.1.100".parse().unwrap();
    println!("{}", private.is_private()); // true

    // Ipv4Addr useful constant
    println!("{}", Ipv4Addr::LOCALHOST);  // 127.0.0.1
    println!("{}", Ipv4Addr::UNSPECIFIED); // 0.0.0.0
    println!("{}", Ipv4Addr::BROADCAST);  // 255.255.255.255

    // SocketAddr — combination of IP and port
    let addr: SocketAddr = "127.0.0.1:8080".parse().expect("Alamat tidak valid");
    println!("IP: {}", addr.ip());        // 127.0.0.1
    println!("Port: {}", addr.port());    // 8080

    // Create SocketAddr programmatically
    let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
    let addr_v4 = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 3000);

    // DNS resolution — changes hostname to SocketAddr
    use std::net::ToSocketAddrs;
    let addrs: Vec<SocketAddr> = "localhost:8080"
        .to_socket_addrs()
        .expect("Resolusi DNS gagal")
        .collect();
    println!("Resolved: {:?}", addrs);
}

TCP Server — Accepting Connections #

TcpListener listens for incoming connections on a specific address and port. Each connection received produces TcpStream which can be read and written.

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

fn tangani_klien(mut stream: TcpStream) {
    let peer_addr = stream.peer_addr().unwrap();
    println!("Koneksi baru dari: {}", peer_addr);

    // Read data from client
    let mut buffer = [0u8; 1024];
    match stream.read(&mut buffer) {
        Ok(0) => println!("Klien {} menutup koneksi", peer_addr),
        Ok(n) => {
            let pesan = String::from_utf8_lossy(&buffer[..n]);
            println!("Diterima dari {}: {}", peer_addr, pesan.trim());

            // Send response
            let respons = format!("Echo: {}", pesan.trim());
            stream.write_all(respons.as_bytes()).unwrap();
        }
        Err(e) => eprintln!("Error baca dari {}: {}", peer_addr, e),
    }
}

fn main() {
    // Bind to address — "0.0.0.0" means all interfaces
    let listener = TcpListener::bind("127.0.0.1:7878")
        .expect("Gagal bind ke port 7878");

    println!("Server mendengarkan di {}", listener.local_addr().unwrap());

    // accept() blocks until there is an incoming connection
    for stream in listener.incoming() {
        match stream {
            Ok(stream) => tangani_klien(stream),
            Err(e) => eprintln!("Error koneksi: {}", e),
        }
    }
}

Server that Handles Multiple Connections with Threads #

The server above can only handle one client at a time — the second client must wait for the first client to finish. For a real server, each connection needs to be handled in a separate thread.

use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
use std::thread;

fn tangani_klien(mut stream: TcpStream) {
    let peer = stream.peer_addr().unwrap();
    let mut buffer = [0u8; 4096];

    loop {
        match stream.read(&mut buffer) {
            Ok(0) => {
                println!("Klien {} terputus", peer);
                break;
            }
            Ok(n) => {
                let data = &buffer[..n];
                // Echo returns to the client
                if stream.write_all(data).is_err() {
                    break;
                }
            }
            Err(e) => {
                eprintln!("Error pada {}: {}", peer, e);
                break;
            }
        }
    }
}

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    println!("Echo server berjalan di port 7878");

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                // Spawn a new thread for each connection
                thread::spawn(move || tangani_klien(stream));
            }
            Err(e) => eprintln!("Error accept: {}", e),
        }
    }
}

Simple HTTP Server from Scratch #

To understand how HTTP works at a low level, we can build a minimal HTTP server:

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

fn tangani_http(mut stream: TcpStream) {
    let peer = stream.peer_addr().unwrap();
    let reader = BufReader::new(stream.try_clone().unwrap());

    // Read request line and headers
    let mut baris: Vec<String> = Vec::new();
    for line in reader.lines() {
        match line {
            Ok(l) if l.is_empty() => break,  // blank line = end of headers
            Ok(l) => baris.push(l),
            Err(_) => break,
        }
    }

    if baris.is_empty() {
        return;
    }

    // Parse request line: "GET /path HTTP/1.1"
    let request_line = &baris[0];
    let bagian: Vec<&str> = request_line.split_whitespace().collect();
    if bagian.len() < 2 {
        return;
    }

    let method = bagian[0];
    let path = bagian[1];
    println!("{} {} dari {}", method, path, peer);

    // Create response based on path
    let (status, body) = match (method, path) {
        ("GET", "/") => (
            "200 OK",
            "<h1>Selamat datang!</h1><p>Server Rust berjalan.</p>"
        ),
        ("GET", "/health") => (
            "200 OK",
            r#"{"status": "ok"}"#
        ),
        _ => (
            "404 Not Found",
            "<h1>404 - Halaman Tidak Ditemukan</h1>"
        ),
    };

    let content_type = if path == "/health" {
        "application/json"
    } else {
        "text/html; charset=utf-8"
    };

    let respons = format!(
        "HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
        status,
        content_type,
        body.len(),
        body
    );

    stream.write_all(respons.as_bytes()).ok();
}

fn main() {
    let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
    println!("HTTP server berjalan di http://localhost:8080");

    for stream in listener.incoming().flatten() {
        thread::spawn(move || tangani_http(stream));
    }
}

TCP Client — Establishing a Connection #

TcpStream::connect() establishes a TCP connection to the server.

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

fn main() {
    // Simple connection
    match TcpStream::connect("127.0.0.1:7878") {
        Ok(mut stream) => {
            println!("Terhubung ke server!");

            // Send data
            stream.write_all(b"halo server\n").unwrap();

            // Read response
            let mut buffer = [0u8; 1024];
            let n = stream.read(&mut buffer).unwrap();
            println!("Respons: {}", String::from_utf8_lossy(&buffer[..n]));
        }
        Err(e) => eprintln!("Gagal terhubung: {}", e),
    }

    // Connection with timeout
    let addr = "127.0.0.1:7878".parse().unwrap();
    match TcpStream::connect_timeout(&addr, Duration::from_secs(5)) {
        Ok(stream) => println!("Terhubung dalam batas waktu"),
        Err(e) => eprintln!("Timeout atau gagal: {}", e),
    }
}

Simple HTTP Client from Scratch #

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

fn http_get(host: &str, path: &str) -> Result<String, Box<dyn std::error::Error>> {
    // Make a connection to port 80
    let mut stream = TcpStream::connect(format!("{}:80", host))?;

    // Send HTTP request
    let request = format!(
        "GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
        path, host
    );
    stream.write_all(request.as_bytes())?;

    // Read entire response
    let mut respons = String::new();
    stream.read_to_string(&mut respons)?;

    // Separate headers from body
    if let Some(pos) = respons.find("\r\n\r\n") {
        Ok(respons[pos + 4..].to_string())
    } else {
        Ok(respons)
    }
}

fn main() {
    // For HTTPS, use crate reqwest or rustls
    // HTTP plain text is increasingly rare in production
    match http_get("example.com", "/") {
        Ok(body) => println!("Body: {}...", &body[..body.len().min(200)]),
        Err(e) => eprintln!("Error: {}", e),
    }
}
For HTTP clients in production code, use the reqwest crate — it handles HTTPS, redirects, cookies, connection pooling, and timeouts automatically. A manual HTTP client from scratch is only useful for understanding very specific protocols or scenarios.

Buffered I/O on TcpStream #

Reading and writing one byte or one character at a time is very inefficient because each operation results in a system call. BufReader and BufWriter add a buffer on top of the stream to reduce the number of system calls.

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

fn tangani_line_protocol(stream: TcpStream) {
    let peer = stream.peer_addr().unwrap();

    // Clone stream for separate reader and writer
    let stream_writer = stream.try_clone().expect("Gagal clone stream");

    let reader = BufReader::new(stream);
    let mut writer = BufWriter::new(stream_writer);

    // Read line by line — efficient because BufReader buffers
    for line in reader.lines() {
        match line {
            Ok(baris) => {
                println!("Dari {}: {}", peer, baris);

                // Simple command processing
                let respons = match baris.trim() {
                    "PING" => "PONG\n".to_string(),
                    "TIME" => format!("{}\n", chrono_sekarang()),
                    "QUIT" => {
                        writer.write_all(b"BYE\n").ok();
                        writer.flush().ok();
                        break;
                    }
                    perintah => format!("UNKNOWN: {}\n", perintah),
                };

                // BufWriter does not send immediately — flush required
                writer.write_all(respons.as_bytes()).ok();
                writer.flush().ok();  // send now
            }
            Err(_) => break,
        }
    }
}

fn chrono_sekarang() -> String {
    use std::time::{SystemTime, UNIX_EPOCH};
    let ts = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs();
    ts.to_string()
}

fn main() {
    let listener = TcpListener::bind("127.0.0.1:6379").unwrap();
    println!("Server line-protocol berjalan di port 6379");
    println!("Coba: nc localhost 6379 lalu ketik PING, TIME, atau QUIT");

    for stream in listener.incoming().flatten() {
        thread::spawn(move || tangani_line_protocol(stream));
    }
}

Timeout on TcpStream #

Without timeouts, read and write operations on sockets can block forever — dangerous for servers that expect responses within a certain time.

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

fn main() {
    let mut stream = TcpStream::connect("127.0.0.1:7878").unwrap();

    // Set timeout for read operations
    stream.set_read_timeout(Some(Duration::from_secs(5)))
        .expect("Gagal setel read timeout");

    // Set timeout for write operations
    stream.set_write_timeout(Some(Duration::from_secs(5)))
        .expect("Gagal setel write timeout");

    stream.write_all(b"request").unwrap();

    let mut buffer = [0u8; 1024];
    match stream.read(&mut buffer) {
        Ok(n) => println!("Diterima: {}", String::from_utf8_lossy(&buffer[..n])),
        Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
            eprintln!("Timeout — server tidak merespons dalam 5 detik");
        }
        Err(e) if e.kind() == std::io::ErrorKind::TimedOut => {
            eprintln!("Timeout — server tidak merespons dalam 5 detik");
        }
        Err(e) => eprintln!("Error: {}", e),
    }

    // Remove timeout — return to infinite blocking
    stream.set_read_timeout(None).unwrap();

    // Check the timeout that has been set
    println!("{:?}", stream.read_timeout().unwrap());   // Some(5s)
    println!("{:?}", stream.write_timeout().unwrap());  // Some(5s)
}

UDP Socket #

UDP is connectionless — each packet is sent independently without a handshake. Faster than TCP but does not guarantee delivery or sequencing.

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

fn main() {
    // UDP Server
    let socket = UdpSocket::bind("127.0.0.1:8888")
        .expect("Gagal bind UDP socket");

    println!("UDP server mendengarkan di port 8888");

    // recv_from return(number_of_bytes, sender_address)
    let mut buffer = [0u8; 1024];
    loop {
        match socket.recv_from(&mut buffer) {
            Ok((n, addr)) => {
                let pesan = String::from_utf8_lossy(&buffer[..n]);
                println!("Dari {}: {}", addr, pesan.trim());

                // Send response to sender
                let respons = format!("Echo: {}", pesan.trim());
                socket.send_to(respons.as_bytes(), addr).ok();
            }
            Err(e) => eprintln!("Error: {}", e),
        }
    }
}
use std::net::UdpSocket;
use std::time::Duration;

fn main() {
    // UDP Client
    let socket = UdpSocket::bind("0.0.0.0:0")  // port 0 = OS selects a free port
        .expect("Gagal bind");

    socket.set_read_timeout(Some(Duration::from_secs(3))).unwrap();

    // Send message
    let server = "127.0.0.1:8888";
    socket.send_to(b"halo UDP server", server).expect("Gagal kirim");

    // Receive response
    let mut buffer = [0u8; 1024];
    match socket.recv_from(&mut buffer) {
        Ok((n, addr)) => {
            println!("Respons dari {}: {}", addr, String::from_utf8_lossy(&buffer[..n]));
        }
        Err(e) => eprintln!("Timeout atau error: {}", e),
    }

    // connect() on UDP — not a real connection, just an address filter
    // After this, send/recv only with the specified address
    socket.connect(server).unwrap();
    socket.send(b"pesan via connected UDP").unwrap();
}
sequenceDiagram
    participant Client
    participant TCPServer as TCP Server
    participant UDPServer as UDP Server

    Note over Client,TCPServer: TCP — connection-oriented
    Client->>TCPServer: SYN
    TCPServer->>Client: SYN-ACK
    Client->>TCPServer: ACK (handshake selesai)
    Client->>TCPServer: Data
    TCPServer->>Client: ACK + Data
    Client->>TCPServer: FIN (tutup koneksi)

    Note over Client,UDPServer: UDP — connectionless
    Client->>UDPServer: Datagram (langsung)
    UDPServer->>Client: Datagram (langsung, tidak dijamin)
    Client->>UDPServer: Datagram (mungkin hilang, tidak ada notifikasi)

Thread Pool for TCP Server #

Servers that create a new thread for each connection are not scalable — thousands of concurrent connections means thousands of threads, which consumes memory and overloads the OS scheduler. Thread pools limit the number of threads while still serving many connections.

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

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

struct ThreadPool {
    _workers: Vec<thread::JoinHandle<()>>,
    sender: mpsc::Sender<Option<Job>>,
}

impl ThreadPool {
    fn new(ukuran: usize) -> Self {
        let (tx, rx) = mpsc::channel::<Option<Job>>();
        let rx = Arc::new(Mutex::new(rx));

        let workers = (0..ukuran).map(|id| {
            let rx = Arc::clone(&rx);
            thread::spawn(move || loop {
                let pesan = rx.lock().unwrap().recv().unwrap();
                match pesan {
                    Some(job) => job(),
                    None => {
                        println!("Worker {} berhenti", id);
                        break;
                    }
                }
            })
        }).collect();

        ThreadPool { _workers: workers, sender: tx }
    }

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

impl Drop for ThreadPool {
    fn drop(&mut self) {
        // Send a stop signal to all workers
        for _ in &self._workers {
            self.sender.send(None).unwrap();
        }
    }
}

fn tangani_koneksi(mut stream: TcpStream) {
    let mut buffer = [0u8; 1024];
    if let Ok(n) = stream.read(&mut buffer) {
        let respons = format!(
            "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, World!"
        );
        stream.write_all(respons.as_bytes()).ok();
    }
}

fn main() {
    let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
    let pool = ThreadPool::new(8);  // maximum 8 concurrent threads

    println!("Server dengan thread pool berjalan di port 8080");

    for stream in listener.incoming().flatten() {
        pool.execute(move || tangani_koneksi(stream));
    }
}

Non-Blocking I/O #

Non-blocking mode allows sockets to be operated without blocking threads — read() and write() return immediately even if there is no data.

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

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    // Set listener to non-blocking
    listener.set_nonblocking(true).unwrap();

    let mut koneksi: Vec<TcpStream> = Vec::new();

    loop {
        // Try accepting new connections — not blocking
        match listener.accept() {
            Ok((stream, addr)) => {
                println!("Koneksi baru dari: {}", addr);
                stream.set_nonblocking(true).unwrap();
                koneksi.push(stream);
            }
            Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
                // No new connections — continue
            }
            Err(e) => eprintln!("Error accept: {}", e),
        }

        // Process all existing connections
        let mut masih_aktif = Vec::new();
        for mut stream in koneksi.drain(..) {
            let mut buffer = [0u8; 1024];
            match stream.read(&mut buffer) {
                Ok(0) => {
                    // Connection closed by client
                    println!("Klien {} terputus", stream.peer_addr().unwrap());
                }
                Ok(n) => {
                    // There is data — process and send back
                    stream.write_all(&buffer[..n]).ok();
                    masih_aktif.push(stream);
                }
                Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
                    // No data currently — connection is still alive
                    masih_aktif.push(stream);
                }
                Err(e) => eprintln!("Error baca: {}", e),
            }
        }
        koneksi = masih_aktif;

        // Inefficient busy-wait — in production use epoll/kqueue via Tokio
        std::thread::sleep(std::time::Duration::from_millis(1));
    }
}
Non-blocking I/O with busy-wait loop as above consumes CPU inefficiently. In production, use event-driven I/O via epoll (Linux), kqueue (macOS), or IOCP (Windows). Tokio abstracts all this with a more ergonomic API — this is the main reason to use async networking in Rust.

When to Switch to Async Networking #

std::net is synchronous and blocking — each operation blocks the thread that called it. For servers handling many concurrent connections, this becomes a bottleneck.

Gunakan std::net jika:
  ✓ Jumlah koneksi bersamaan rendah (puluhan hingga ratusan)
  ✓ Setiap koneksi membutuhkan CPU-intensive work — thread per koneksi justru efisien
  ✓ Prototyping atau memahami networking dari level dasar
  ✓ Tool CLI yang membuat satu atau beberapa koneksi, bukan server
  ✓ Tidak ada dependency async di seluruh codebase

Beralih ke Tokio + async/await jika:
  ✗ Server perlu menangani ribuan koneksi bersamaan (C10K problem)
  ✗ Banyak operasi I/O-bound yang menunggu network atau disk
  ✗ Perlu integrasi dengan ecosystem async (reqwest, sqlx, axum)
  ✗ Latency per-request perlu diminimalkan
  ✗ Sudah menggunakan async di bagian lain codebase

Comparison of threading models:

Aspectstd::net + threads per connectionTokyo async
Concurrent connectionsHundreds (limited by RAM & OS)Tens of thousands
Overhead per connection~8MB stack per thread~KB per task
Code complexitySimplerMore complex (lifetime, Pin)
CPU-intensive workNatural — threads running parallelNeed spawn_blocking
EcosystemLimitedVery rich (reqwest, sqlx, axum)
DebuggingEasierMore difficult
flowchart TD
    A{Jenis aplikasi networking?} --> B{Jumlah koneksi bersamaan?}
    B -- Sedikit, puluhan --> C["std::net + thread per koneksi\nSederhana, cukup untuk use case ini"]
    B -- Banyak, ribuan --> D["Tokio + async/await\nEfisien untuk I/O-bound massif"]

    A --> E{CLI tool atau client?}
    E -- Yes --> F["std::net langsung\natau reqwest untuk HTTP"]
    E -- No --> B

    C --> G{Perlu HTTP/HTTPS?}
    G -- Yes --> H["Gunakan framework:\nAxum, Actix-web, Rocket"]
    G -- No --> I[std::net TCP/UDP is sufficient]

    D --> J{Protokol apa?}
    J -- HTTP/REST --> K[Axum or Actix-web]
    J -- TCP custom --> L[Tokio TcpListener]
    J -- UDP --> M[Tokio UdpSocket]

    style C fill:#e8f5e9
    style D fill:#e3f2fd
    style F fill:#e8f5e9
    style H fill:#fff3e0
    style K fill:#fff3e0

Equivalent Example: std::net vs Tokio #

// ===== std::net (synchronous) =====
use std::net::TcpListener;
use std::io::{Read, Write};
use std::thread;

fn server_sync() {
    let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
    for stream in listener.incoming().flatten() {
        thread::spawn(move || {
            let mut stream = stream;
            let mut buf = [0u8; 1024];
            let n = stream.read(&mut buf).unwrap();
            stream.write_all(&buf[..n]).unwrap();
        });
    }
}
// ===== Tokio (asynchronous) =====
// Cargo.toml: tokio = { version = "1", features = ["full"] }
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn server_async() {
    let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
    loop {
        let (mut stream, _) = listener.accept().await.unwrap();
        tokio::spawn(async move {
            let mut buf = [0u8; 1024];
            let n = stream.read(&mut buf).await.unwrap();
            stream.write_all(&buf[..n]).await.unwrap();
        });
    }
}

The structure is almost identical — the main differences are await and async. Understanding std::net first makes the transition to Tokio easier because the concept is the same.


Summary #

  • TcpListener::bind for server, TcpStream::connect for client — both return Result; Handle bind errors (port already in use) and connect errors (server does not exist) explicitly.
  • Spawn threads per connection for simple serversthread::spawn(move || tangani_klien(stream)) is the easiest pattern for concurrency. Add a thread pool if concurrent connections can reach hundreds.
  • BufReader and BufWriter for efficiency — read line by line with BufReader::lines() instead of direct read(). BufWriter accumulates data before sending — don’t forget flush().
  • Always set timeoutset_read_timeout and set_write_timeout prevent threads from blocking forever when the client is not responding. Handle ErrorKind::TimedOut and ErrorKind::WouldBlock.
  • UDP for speed, TCP for reliability — UDP doesn’t guarantee delivery or sequencing, but the overhead is much lower. Suitable for gaming, streaming, DNS, and metrics.
  • SocketAddr::from(([127,0,0,1], 8080)) is safer than string parsing — no need for unwrap as it can’t fail. Use string parsing only for user input.
  • std::net for limited connections, Tokio for thousands of connections — threads per connection are not scalable as each thread takes up ~8MB of stack. Tokio task is only a few KB overhead.
  • Non-blocking I/O without an event loop is inefficient — busy-wait loop consumes CPU. If you need non-blocking, use Tokio which has integrated epoll/kqueue/IOCP.


← Previous: Process
About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact