Operasi jaringan penuh dengan mode kegagalan yang berbeda-beda: server tidak listening (ECONNREFUSED), koneksi terputus di tengah jalan (ECONNRESET, EPIPE), atau server diam terlalu lama (EAGAIN dari timeout). Setiap errno memerlukan penanganan yang berbeda—retry untuk error transient, fail-fast untuk error permanen. Bab ini membedah tiga skenario paling umum lewat satu program C yang menampilkan setiap errno secara konkret, dan menjelaskan strategi penanganan untuk masing-masing.

Tiga Skenario Error Utama #

SkenarioErrnoPenyebabStrategi
Server tidak listeningECONNREFUSED (111)Tidak ada server di portRetry dengan backoff
Server menutup paksaECONNRESET (104), EPIPE (32)close() server saat klien masih aktifTutup socket, laporkan error
Server diamEAGAIN (11), EWOULDBLOCKServer menerima koneksi tetapi tidak meresponsTimeout via SO_RCVTIMEO

Tabel di atas adalah peta jalan penanganan error jaringan. Bila aplikasi hanya mengandalkan “sukses atau crash”, aplikasi tersebut akan rentan terhadap semua kondisi nyata di jaringan: server restart, koneksi wi-fi putus sementara, firewall drop, atau server yang overload.

SIGPIPE: Jebakan Klasik #

Sebelum masuk ke tiga skenario, kita perlu membahas satu jebakan klasik. Bila klien memanggil send ke socket yang sudah ditutup oleh peer (server), kernel secara default mengirim sinyal SIGPIPE ke proses pengirim. Bila aplikasi tidak menangani SIGPIPE, sinyal ini akan membunuh proses—tidak ada kesempatan untuk cleanup, log error, atau retry.

Ada dua cara menangani SIGPIPE:

  1. signal(SIGPIPE, SIG_IGN) di awal main—abaikan sinyal, send akan mengembalikan -1 dengan errno = EPIPE.
  2. Gunakan flag MSG_NOSIGNAL pada setiap panggilan send—lebih lokal, tidak mengubah state global.

Pendekatan pertama lebih sederhana dan cocok untuk aplikasi yang semua send-nya harus diam saat socket ditutup. Pendekatan kedua lebih eksplisit dan cocok bila aplikasi punya kode legacy yang mengharapkan SIGPIPE.

Demo Tiga Skenario dalam Satu Program #

Program berikut menjalankan tiga skenario error secara berurutan: (1) koneksi ke port tanpa server, (2) server yang menutup paksa setelah klien connect, dan (3) server yang diam menyebabkan timeout.

c
/* File: errhandler.c - Beberapa skenario error jaringan */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT_NO_SERVER 9991
#define PORT_RESET_SERVER 9095
#define PORT_TIMEOUT_SERVER 9096

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

/* Skenario 1: Connection refused - tidak ada server listening */
static void skenario_econnrefused(void) {
    printf("\n=== Skenario 1: ECONNREFUSED ===\n");
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port   = htons(PORT_NO_SERVER);
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

    int rc = connect(fd, (struct sockaddr *) &addr, sizeof(addr));
    printf("connect ke 127.0.0.1:%d: rc=%d, errno=%d (%s)\n",
           PORT_NO_SERVER, rc, errno, strerror(errno));
    close(fd);
}

/* Skenario 2: Server close paksa, klien kirim lagi -> ECONNRESET */
static void jalankan_server_close(void) {
    int srv = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port   = htons(PORT_RESET_SERVER);
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

    if (bind(srv, (struct sockaddr *) &addr, sizeof(addr)) != 0) _exit(1);
    if (listen(srv, 1) != 0) _exit(1);

    int cli = accept(srv, NULL, NULL);
    if (cli < 0) _exit(1);
    jeda_ms(300);
    /* Tutup paksa tanpa membaca */
    close(cli);
    close(srv);
}

static void skenario_epipe(void) {
    printf("\n=== Skenario 2: ECONNRESET (server tutup paksa) ===\n");
    fflush(stdout);

    pid_t pid = fork();
    if (pid == 0) { jalankan_server_close(); _exit(0); }

    jeda_ms(200);
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port   = htons(PORT_RESET_SERVER);
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

    /* Abaikan SIGPIPE agar send() hanya return -1 dengan errno */
    signal(SIGPIPE, SIG_IGN);

    if (connect(fd, (struct sockaddr *) &addr, sizeof(addr)) != 0) {
        perror("connect"); return;
    }
    printf("[KLIEN] tersambung\n");

    const char *msg = "Pesan pertama ke server\n";
    ssize_t n1 = send(fd, msg, strlen(msg), 0);
    printf("[KLIEN] send #1: return %zd\n", n1);

    /* Tunggu server menutup */
    jeda_ms(700);

    const char *msg2 = "Pesan kedua\n";
    ssize_t n2 = send(fd, msg2, strlen(msg2), 0);
    printf("[KLIEN] send #2: return %zd, errno=%d (%s)\n",
           n2, errno, strerror(errno));

    close(fd);
    waitpid(pid, NULL, 0);
}

/* Skenario 3: Timeout via SO_RCVTIMEO */
static void skenario_timeout(void) {
    printf("\n=== Skenario 3: EAGAIN (timeout dari SO_RCVTIMEO) ===\n");
    int srv = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port   = htons(PORT_TIMEOUT_SERVER);
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    if (bind(srv, (struct sockaddr *) &addr, sizeof(addr)) != 0) { perror("bind"); return; }
    if (listen(srv, 1) != 0) { perror("listen"); return; }

    pid_t pid = fork();
    if (pid == 0) {
        int cli = accept(srv, NULL, NULL);
        if (cli >= 0) {
            jeda_ms(3000);
            close(cli);
        }
        _exit(0);
    }

    jeda_ms(200);
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (connect(fd, (struct sockaddr *) &addr, sizeof(addr)) != 0) {
        perror("connect"); close(srv); return;
    }

    struct timeval tv;
    tv.tv_sec  = 1;
    tv.tv_usec = 0;
    setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

    char buf[64];
    struct timespec t0, t1;
    clock_gettime(CLOCK_MONOTONIC, &t0);
    ssize_t n = recv(fd, buf, sizeof(buf), 0);
    clock_gettime(CLOCK_MONOTONIC, &t1);
    double elapsed = (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9;
    printf("[KLIEN] recv: return %zd setelah %.3f detik, errno=%d (%s)\n",
           n, elapsed, errno,
           errno == EAGAIN ? "EAGAIN (timeout)" : strerror(errno));

    close(fd);
    close(srv);
    kill(pid, SIGTERM);
    waitpid(pid, NULL, 0);
}

int main(void) {
    skenario_econnrefused();
    skenario_epipe();
    skenario_timeout();
    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -o errhandler errhandler.c
timeout 10 ./errhandler

Output yang Dihasilkan:

text

=== Skenario 1: ECONNREFUSED ===
connect ke 127.0.0.1:9991: rc=-1, errno=111 (Connection refused)

=== Skenario 2: ECONNRESET (server tutup paksa) ===
[KLIEN] tersambung
[KLIEN] send #1: return 24
[KLIEN] send #2: return -1, errno=104 (Connection reset by peer)

=== Skenario 3: EAGAIN (timeout dari SO_RCVTIMEO) ===
[KLIEN] recv: return -1 setelah 1.023 detik, errno=11 (EAGAIN (timeout))

Mari kita telaah baris-baris kode di atas:

  1. Baris 30-37 (skenario_econnrefused): Membuat socket TCP dan connect ke port 9991 yang tidak ada servernya. Kernel tujuan mengirim RST (reset) segera karena tidak ada socket listening di port itu, sehingga connect kembali dengan errno = ECONNREFUSED (111). Kondisi ini transient—bisa jadi server sedang restart—sehingga retry dengan exponential backoff adalah strategi yang tepat.
  2. Baris 39-51 (jalankan_server_close): Server menerima koneksi, tidur 300 ms, lalu menutup paksa tanpa membaca data. Setelah close di sisi server, kernel server mengirim FIN ke klien, dan bila klien mengirim data setelah FIN diterima, server akan merespons dengan RST.
  3. Baris 60 (signal(SIGPIPE, SIG_IGN)): Wajib sebelum memanggil send ke socket yang mungkin sudah ditutup peer. Tanpa ini, SIGPIPE akan menghentikan proses. Dengan SIG_IGN, send mengembalikan -1 dengan errno = EPIPE (32) atau ECONNRESET (104) tergantung timing.
  4. Baris 67 (send #1: return 24): Pengiriman pertama berhasil karena TCP send buffer masih ada ruang. Data masuk buffer kernel, send kembali segera. Padahal server sudah menutup socket—FIN belum sampai ke klien saat send pertama dieksekusi.
  5. Baris 71 (send #2: return -1, errno=104): Setelah menunggu 700 ms, RST dari server sudah sampai ke klien. send kedua sekarang kembali dengan errno = ECONNRESET (104). Beberapa sistem mengembalikan EPIPE (32) — tergantung urutan FIN dan RST. Keduanya menandakan koneksi sudah mati.
  6. Baris 91-93 (struct timeval tv; setsockopt(...)): Mengatur timeout 1 detik untuk recv (lihat bab Timeout dan Retry untuk penjelasan rinci). recv akan kembali dengan errno = EAGAIN bila tidak ada data dalam 1 detik.

Strategi Penanganan per Errno #

Berikut panduan penanganan setiap errno umum:

ErrnoNamaPenanganan
11 (EAGAIN)Resource temporarily unavailableRetry sama persis (bila pakai SO_RCVTIMEO)
32 (EPIPE)Broken pipeTutup socket, laporkan error, jangan retry
104 (ECONNRESET)Connection reset by peerTutup socket, kemungkinan retry dengan backoff
111 (ECONNREFUSED)Connection refusedRetry dengan exponential backoff
110 (ETIMEDOUT)Connection timed outRetry dengan backoff
113 (EHOSTUNREACH)No route to hostRetry setelah jeda lama (mis. 1 menit)