API socket adalah jembatan antara aplikasi C dan tumpukan jaringan kernel. Tujuh panggilan inti membentuk sebagian besar program jaringan: socket, bind, listen, accept, connect, send, dan recv. Bab ini menelusuri ketujuh panggilan tersebut dalam satu demo terintegrasi yang menjalankan server dan klien dalam satu proses (lewat fork). Kita juga akan menggunakan getaddrinfo sebagai pengganti gethostbyname yang sudah obsolete, agar kode tetap berfungsi untuk IPv4 maupun IPv6 tanpa perubahan.

Tujuh Panggilan Inti Socket #

Tabel berikut merangkum tujuh panggilan yang membentuk dasar pemrograman socket:

PanggilanSisiPeran
socket()keduanyaMembuat file descriptor socket baru
bind()serverMengaitkan socket ke alamat (IP:port atau path)
listen()serverMenandai socket sebagai pasif (siap menerima koneksi)
accept()serverMemblokir hingga klien connect, lalu mengembalikan fd baru
connect()klienMenghubungkan socket ke alamat server
send()keduanyaMengirim data ke socket
recv()keduanyaMembaca data dari socket

Server selalu memanggil empat panggilan pertama secara berurutan: socket -> bind -> listen -> accept. Klien cukup memanggil tiga: socket -> connect -> send/recv. Setelah koneksi terbentuk, baik server maupun klien menggunakan send dan recv secara simetris—dari sudut pandang kernel, koneksi TCP adalah stream dua arah yang setara.

Mengapa getaddrinfo, Bukan gethostbyname #

Fungsi lama gethostbyname hanya mengembalikan IPv4 dan tidak thread-safe. Penggantinya, getaddrinfo, mengembalikan linked-list struct addrinfo yang dapat memuat hasil untuk IPv4 dan IPv6 sekaligus. Fungsi ini juga menerima parameter hints (ai_family, ai_socktype, ai_flags) untuk memfilter hasil sesuai kebutuhan aplikasi.

Karena getaddrinfo mengembalikan linked-list, kode kita tidak perlu mengasumsikan jumlah alamat atau jenisnya. Cukup iterasi dari ai_next hingga NULL dan coba socket() + connect() untuk setiap entri hingga satu berhasil. Pola ini disebut “coba-alamat-satu-per-satu” dan rekomendasi resmi dari Beej’s Guide to Network Programming.

Demo Lengkap: Server dan Klien dalam Satu Proses #

Berikut program yang memanggil ketujuh API socket dalam satu alur. Proses parent berperan sebagai server, child berperan sebagai klien, sehingga kita bisa melihat kedua sisi dalam satu eksekusi.

c
/* File: sockprog.c - Demo lengkap 7 socket API calls */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <netdb.h>
#include <time.h>

#define PORT "9092"

static void jeda_ms(int ms) {
    struct timespec ts = { ms / 1000, (ms % 1000) * 1000000L };
    nanosleep(&ts, NULL);
}

static void jalankan_server(void) {
    struct addrinfo hints = {0}, *res;
    hints.ai_family   = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags    = AI_PASSIVE;  /* Bind ke semua interface */

    int rc = getaddrinfo(NULL, PORT, &hints, &res);
    if (rc != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rc));
        fflush(stderr);
        _exit(1);
    }

    /* 1) socket() */
    int srv = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (srv < 0) { perror("socket"); _exit(1); }

    int opt = 1;
    setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    /* 2) bind() */
    if (bind(srv, res->ai_addr, res->ai_addrlen) != 0) {
        perror("bind"); _exit(1);
    }
    freeaddrinfo(res);

    /* 3) listen() */
    if (listen(srv, 5) != 0) { perror("listen"); _exit(1); }

    printf("[SERVER] socket/bind/listen OK di port %s\n", PORT);
    fflush(stdout);

    /* 4) accept() */
    int cli = accept(srv, NULL, NULL);
    if (cli < 0) { perror("accept"); _exit(1); }
    printf("[SERVER] koneksi diterima, fd klien=%d\n", cli);
    fflush(stdout);

    /* 6) recv() */
    char buf[256];
    ssize_t n = recv(cli, buf, sizeof(buf) - 1, 0);
    if (n > 0) {
        buf[n] = '\0';
        printf("[SERVER] recv %zd bytes: %s", n, buf);
        fflush(stdout);
        /* 7) send() - echo balik */
        send(cli, buf, (size_t)n, 0);
    }
    close(cli);
    close(srv);
}

static void jalankan_klien(void) {
    jeda_ms(200);
    struct addrinfo hints = {0}, *res;
    hints.ai_family   = AF_INET;
    hints.ai_socktype = SOCK_STREAM;

    int rc = getaddrinfo("127.0.0.1", PORT, &hints, &res);
    if (rc != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rc));
        fflush(stderr);
        _exit(1);
    }

    /* 1) socket() di sisi klien */
    int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (fd < 0) { perror("socket"); _exit(1); }

    /* 5) connect() */
    if (connect(fd, res->ai_addr, res->ai_addrlen) != 0) {
        perror("connect"); _exit(1);
    }
    freeaddrinfo(res);
    printf("[KLIEN] connect sukses\n");
    fflush(stdout);

    /* 7) send() dari sisi klien */
    const char *msg = "Halo, ini pesan lewat socket!\n";
    ssize_t sent = send(fd, msg, strlen(msg), 0);
    printf("[KLIEN] send %zd bytes\n", sent);
    fflush(stdout);

    /* 6) recv() di sisi klien */
    char buf[256];
    ssize_t n = recv(fd, buf, sizeof(buf) - 1, 0);
    if (n > 0) {
        buf[n] = '\0';
        printf("[KLIEN] recv %zd bytes: %s", n, buf);
        fflush(stdout);
    }
    close(fd);
}

int main(void) {
    fflush(stdout);
    pid_t pid = fork();
    if (pid == 0) { jalankan_klien(); fflush(stdout); _exit(0); }
    jalankan_server();
    fflush(stdout);
    waitpid(pid, NULL, 0);
    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -o sockprog sockprog.c
timeout 5 ./sockprog

Output yang Dihasilkan:

text
[SERVER] socket/bind/listen OK di port 9092
[KLIEN] connect sukses
[SERVER] koneksi diterima, fd klien=4
[KLIEN] send 30 bytes
[SERVER] recv 30 bytes: Halo, ini pesan lewat socket!
[KLIEN] recv 30 bytes: Halo, ini pesan lewat socket!

Mari kita telaah baris-baris kode di atas:

  1. Baris 17-22 (getaddrinfo(NULL, PORT, &hints, &res) di server): Argumen pertama NULL dengan ai_flags = AI_PASSIVE memberitahu getaddrinfo untuk mengembalikan alamat wildcard 0.0.0.0 (semua interface) yang cocok untuk bind. Bila kita ingin bind ke alamat spesifik, ganti NULL dengan string alamat (mis. "127.0.0.1").
  2. Baris 24 (socket(res->ai_family, res->ai_socktype, res->ai_protocol)): Membuat socket dengan parameter yang sesuai hasil getaddrinfo. Pendekatan ini membuat kode otomatis berfungsi untuk IPv4 maupun IPv6—tidak ada hardcoding AF_INET.
  3. Baris 31 (bind(srv, res->ai_addr, res->ai_addrlen)): Mengaitkan socket ke alamat. res->ai_addr adalah struct sockaddr * generik yang sudah diisi dengan IPv4 atau IPv6 oleh getaddrinfo. Kita tidak perlu tahu jenis alamat yang dikembalikan—cukup pass-through.
  4. Baris 33 (freeaddrinfo(res)): Setelah bind berhasil, linked-list res tidak lagi diperlukan dan wajib dibebaskan. Panggilan ini wajib—tanpa freeaddrinfo, terjadi memory leak setiap kali getaddrinfo dipanggil.
  5. Baris 38 (accept(srv, NULL, NULL)): Memblokir hingga klien connect. Argumen kedua dan ketiga NULL berarti kita tidak peduli alamat klien—bila kita ingin mencatat IP klien (mis. untuk logging), isi dengan struct sockaddr_storage dan pointer panjang.
  6. Baris 80-81 (getaddrinfo("127.0.0.1", PORT, ...) di klien): Tanpa AI_PASSIVE, getaddrinfo mengembalikan alamat tujuan untuk connect. Klien menentukan hostname tujuan sebagai string ("127.0.0.1" atau "example.com").
  7. Baris 92 (send(fd, msg, strlen(msg), 0)): Mengirim data ke socket yang sudah connect. Argumen keempat adalah flags—0 berarti default. MSG_NOSIGNAL berguna untuk mencegah SIGPIPE bila server menutup koneksi mendadak (lihat bab Network Error Handling).
  8. Baris 97 (recv(fd, buf, sizeof(buf) - 1, 0)): Membaca data dari socket. Selalu sisakan satu byte untuk null-terminator (sizeof(buf) - 1) karena recv tidak menambah \0 secara otomatis.

Membaca dan Menulis dengan Benar #

Banyak pemula mengasumsikan satu send di sisi klien selalu dipetakan ke satu recv di sisi server. Ini salah untuk TCP. TCP adalah stream, bukan sequence of messages. Tiga panggilan send mungkin diterima sebagai satu recv (jika datanya kecil), atau satu send besar sebagai beberapa recv (jika data melebihi buffer kernel).

Untuk mengatasi hal ini, ada dua pendekatan umum:

  1. Delimiter-based: akhiri setiap pesan dengan karakter khusus (mis. \n). Server membaca byte demi byte hingga menemukan delimiter. Pendekatan ini dipakai HTTP, SMTP, IRC.
  2. Length-prefix: sertakan panjang pesan sebagai header biner di awal setiap pesan. Server membaca 4 byte pertama sebagai panjang, lalu membaca sisa pesan sesuai panjang tersebut. Pendekatan ini dipakai di banyak protokol binary modern seperti gRPC, MQTT, AMQP.

Bab Parsing Protokol akan membahas implementasi kedua pendekatan. Untuk sekarang, perhatikan bahwa loop recv hingga seluruh pesan terbaca adalah praktik wajib.