Socket adalah abstraksi komunikasi dua arah yang disediakan kernel. Berbeda dengan pipe yang hanya bisa antar proses berkerabat, socket dapat menghubungkan proses di mesin yang sama (Unix domain socket, AF_UNIX) atau di mesin berbeda melalui jaringan (TCP/IP, AF_INET). API socket cukup besar—mencakup socket, bind, listen, accept, connect, send, recv—sehingga pembahasan penuh membutuhkan satu section tersendiri (lihat section Jaringan dan Protokol). Pada chapter ini kita memperkenalkan empat langkah inti: membuat socket, bind, listen, dan connect, dengan dua demo konkret: Unix domain socket untuk IPC lokal dan TCP socket IPv4 sebagai persiapan komunikasi jaringan.

Empat Langkah Membuat Socket Server #

Membuat server socket selalu mengikuti empat langkah berurutan:

  1. socket(domain, type, protocol): Membuat file descriptor socket baru. domain menentukan jenis komunikasi (AF_UNIX untuk lokal, AF_INET untuk IPv4). type menentukan semantik (SOCK_STREAM untuk TCP-like, SOCK_DGRAM untuk UDP-like).
  2. bind(fd, addr, len): Mengaitkan socket ke alamat—file path untuk Unix domain, atau IP:port untuk IPv4.
  3. listen(fd, backlog): Menandai socket sebagai pasif—siap menerima koneksi. backlog adalah batas antrian koneksi yang belum di-accept.
  4. accept(fd, ...): Memblokir hingga klien connect, lalu mengembalikan file descriptor baru untuk koneksi tersebut. Socket asli tetap mendengarkan koneksi lain.
c
/* File: socket_unix_dasar.c - Unix domain socket: buat, bind, listen */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>

int main(void) {
    /* Langkah 1: buat socket */
    int fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (fd < 0) {
        perror("socket");
        return 1;
    }
    printf("Socket dibuat, fd=%d\n", fd);

    /* Langkah 2: bind ke path /tmp/socket_demo */
    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, "/tmp/socket_demo", sizeof(addr.sun_path) - 1);

    unlink("/tmp/socket_demo");   /* hapus kalau sudah ada */
    if (bind(fd, (struct sockaddr *) &addr, sizeof(addr)) != 0) {
        perror("bind");
        close(fd);
        return 1;
    }
    printf("Bind ke %s berhasil\n", addr.sun_path);

    /* Langkah 3: listen */
    if (listen(fd, 5) != 0) {
        perror("listen");
        close(fd);
        return 1;
    }
    printf("Listen berhasil, menunggu koneksi...\n");

    close(fd);
    unlink("/tmp/socket_demo");
    printf("Socket ditutup dan file dihapus.\n");
    return 0;
}

Compile dan jalankan:

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

Output yang Dihasilkan:

text
Socket dibuat, fd=3
Bind ke /tmp/socket_demo berhasil
Listen berhasil, menunggu koneksi...
Socket ditutup dan file dihapus.

Mari kita telaah baris-baris kode di atas:

  1. Baris 9 (socket(AF_UNIX, SOCK_STREAM, 0)): Membuat socket Unix domain. AF_UNIX berarti komunikasi antar-proses di mesin yang sama lewat filesystem path. SOCK_STREAM berarti koneksi stream dua arah (mirip TCP, tetapi lokal). Argumen ketiga 0 memilih protokol default untuk kombinasi domain+type.
  2. Baris 14-18 (struct sockaddr_un addr): Struktur alamat untuk Unix domain socket. Field sun_family wajib AF_UNIX. Field sun_path adalah path file di filesystem yang akan menjadi alamat socket. Path harus cukup pendek (batas sizeof(addr.sun_path) biasanya 108 byte di Linux).
  3. Baris 20 (unlink("/tmp/socket_demo")): Menghapus file socket lama bila ada. Tanpa unlink, bind gagal dengan EADDRINUSE bila path sudah dipakai socket sebelumnya yang tidak ditutup dengan benar.
  4. Baris 21 (bind(fd, ...)): Mengaitkan socket ke path. Kernel membuat file khusus bertipe socket di filesystem. Proses lain dapat connect ke path ini.
  5. Baris 28 (listen(fd, 5)): Mengubah socket menjadi pasif. Angka 5 adalah backlog—jumlah koneksi yang dapat menunggu di antrian accept sebelum ditolak. Pada Linux modern, nilai ini adalah hint saja; kernel mungkin mengalokasikan lebih.
  6. Baris 33-34 (close(fd); unlink(...)): Menutup socket lalu menghapus file socket dari filesystem. Tanpa unlink, file socket tetap ada di filesystem tetapi tidak terpakai—sampah.

Klien-Server Unix Domain Socket Lengkap #

Demo berikut menjalankan server di parent dan klien di child, lewat fork. Server menerima koneksi, membaca pesan, membalas. Klien connect, mengirim pesan, membaca balasan.

c
/* File: socket_unix_complete.c - klien-server Unix domain socket */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <string.h>
#include <time.h>

#define SOCK_PATH "/tmp/socket_demo2"

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

static void jalankan_server(void) {
    int server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (server_fd < 0) { perror("socket"); _exit(1); }

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);

    unlink(SOCK_PATH);
    if (bind(server_fd, (struct sockaddr *) &addr, sizeof(addr)) != 0) {
        perror("bind"); _exit(1);
    }
    if (listen(server_fd, 5) != 0) { perror("listen"); _exit(1); }

    printf("[SERVER] menunggu koneksi...\n");
    fflush(stdout);

    /* Langkah 4: accept - memblokir hingga klien connect */
    int client_fd = accept(server_fd, NULL, NULL);
    if (client_fd < 0) { perror("accept"); _exit(1); }

    printf("[SERVER] koneksi diterima\n");
    fflush(stdout);

    char buf[128];
    ssize_t n = read(client_fd, buf, sizeof(buf) - 1);
    if (n > 0) {
        buf[n] = '\0';
        printf("[SERVER] menerima: %s\n", buf);
        fflush(stdout);
    }

    const char *balasan = "Halo Klien, ini Server!";
    write(client_fd, balasan, strlen(balasan));

    close(client_fd);
    close(server_fd);
    unlink(SOCK_PATH);
}

static void jalankan_klien(void) {
    /* Tunggu server siap */
    jeda_ms(100);

    int fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (fd < 0) { perror("socket"); _exit(1); }

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);

    if (connect(fd, (struct sockaddr *) &addr, sizeof(addr)) != 0) {
        perror("connect"); _exit(1);
    }

    const char *pesan = "Halo Server, ini Klien!";
    write(fd, pesan, strlen(pesan));

    char buf[128];
    ssize_t n = read(fd, buf, sizeof(buf) - 1);
    if (n > 0) {
        buf[n] = '\0';
        printf("[KLIEN] menerima: %s\n", buf);
        fflush(stdout);
    }
    close(fd);
}

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

Compile dan jalankan:

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

Output yang Dihasilkan:

text
[SERVER] menunggu koneksi...
[SERVER] koneksi diterima
[SERVER] menerima: Halo Server, ini Klien!
[KLIEN] menerima: Halo Klien, ini Server!

Mari kita telaah baris-baris kode di atas:

  1. Baris 16-46 (jalankan_server): Fungsi server—membuat socket, bind, listen, lalu accept. accept memblokir hingga klien connect, lalu mengembalikan client_fd baru. Setelah itu, server membaca pesan klien dan menulis balasan.
  2. Baris 30 (accept(server_fd, NULL, NULL)): Mengembalikan file descriptor baru untuk koneksi yang masuk. Socket asli server_fd tetap mendengarkan koneksi lain—kita bisa memanggil accept lagi untuk koneksi kedua. Argumen kedua dan ketiga NULL berarti kita tidak peduli alamat klien (bisa diisi struct sockaddr_un untuk membaca path klien).
  3. Baris 41 (write(client_fd, ...)): Setelah membaca pesan, server menulis balasan ke client_fd (bukan server_fd!). Inilah inti: koneksi aktif memiliki descriptor sendiri, terpisah dari listening socket.
  4. Baris 60 (jeda_ms(100) di klien): Klien menunggu 100 ms sebelum connect untuk memastikan server sudah memanggil listen. Tanpa jeda, connect bisa gagal dengan ECONNREFUSED bila server belum siap.
  5. Baris 73 (connect(fd, ...)): Menghubungkan socket klien ke alamat server. Setelah sukses, fd dapat dipakai untuk read/write seperti file biasa.

TCP Socket IPv4: Persiapan Jaringan #

Untuk komunikasi lintas mesin, gunakan AF_INET (IPv4) atau AF_INET6 (IPv6). Struktur alamat berubah dari sockaddr_un menjadi sockaddr_in, dengan field sin_port (port jaringan) dan sin_addr (IP address).

c
/* File: socket_tcp_dasar.c - TCP socket IPv4: buat, bind, listen */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(void) {
    /* Buat TCP socket (IPv4) */
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
        perror("socket");
        return 1;
    }
    printf("TCP socket dibuat, fd=%d\n", fd);

    /* Aktifkan SO_REUSEADDR untuk demo cepat restart */
    int opt = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    /* Bind ke 127.0.0.1:8080 */
    struct sockaddr_in addr;
    addr.sin_family      = AF_INET;
    addr.sin_port        = htons(8080);
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

    if (bind(fd, (struct sockaddr *) &addr, sizeof(addr)) != 0) {
        perror("bind");
        close(fd);
        return 1;
    }
    printf("Bind ke 127.0.0.1:8080 berhasil\n");

    if (listen(fd, 5) != 0) {
        perror("listen");
        close(fd);
        return 1;
    }
    printf("Listen berhasil. Socket siap menerima koneksi TCP.\n");

    close(fd);
    printf("Socket ditutup.\n");
    return 0;
}

Compile dan jalankan:

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

Output yang Dihasilkan:

text
TCP socket dibuat, fd=3
Bind ke 127.0.0.1:8080 berhasil
Listen berhasil. Socket siap menerima koneksi TCP.
Socket ditutup.

Mari kita telaah baris-baris kode di atas:

  1. Baris 10 (socket(AF_INET, SOCK_STREAM, 0)): AF_INET berarti IPv4. SOCK_STREAM di atas IPv4 otomatis memilih protokol TCP—tidak perlu argumen ketiga.
  2. Baris 17 (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, ...)): Opsi SO_REUSEADDR mengizinkan bind ke port yang baru saja dipakai proses keluar dalam keadaan TIME_WAIT. Tanpa opsi ini, restart server cepat bisa gagal dengan EADDRINUSE.
  3. Baris 20-23 (struct sockaddr_in addr): Struktur alamat IPv4. Field sin_family = AF_INET. Field sin_port harus dalam network byte order—gunakan htons (host-to-network-short) untuk mengkonversi. Field sin_addr.s_addr pakai htonl(INADDR_LOOPBACK) = 127.0.0.1 (loopback, hanya koneksi dari mesin yang sama).
  4. Baris 25 (bind(fd, ...)): Mengaitkan socket ke IP:port. Berbeda dari Unix domain yang membuat file path, bind TCP hanya memakai nomor port di kernel—tidak ada file di filesystem.
  5. htonl / htons: Konversi byte order. Arsitektur x86 menyimpan angka sebagai little-endian, tetapi TCP/IP mensyaratkan network byte order big-endian. htons mengkonversi 16-bit (untuk port), htonl 32-bit (untuk IP).