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 #
| Skenario | Errno | Penyebab | Strategi |
|---|---|---|---|
| Server tidak listening | ECONNREFUSED (111) | Tidak ada server di port | Retry dengan backoff |
| Server menutup paksa | ECONNRESET (104), EPIPE (32) | close() server saat klien masih aktif | Tutup socket, laporkan error |
| Server diam | EAGAIN (11), EWOULDBLOCK | Server menerima koneksi tetapi tidak merespons | Timeout 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:
signal(SIGPIPE, SIG_IGN)di awalmain—abaikan sinyal,sendakan mengembalikan-1denganerrno = EPIPE.- Gunakan flag
MSG_NOSIGNALpada setiap panggilansend—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.
/* 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:
gcc -std=c11 -Wall -Wextra -o errhandler errhandler.c
timeout 10 ./errhandlerOutput yang Dihasilkan:
=== 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:
- 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, sehinggaconnectkembali denganerrno = ECONNREFUSED(111). Kondisi ini transient—bisa jadi server sedang restart—sehingga retry dengan exponential backoff adalah strategi yang tepat. - Baris 39-51 (
jalankan_server_close): Server menerima koneksi, tidur 300 ms, lalu menutup paksa tanpa membaca data. Setelahclosedi sisi server, kernel server mengirim FIN ke klien, dan bila klien mengirim data setelah FIN diterima, server akan merespons dengan RST. - Baris 60 (
signal(SIGPIPE, SIG_IGN)): Wajib sebelum memanggilsendke socket yang mungkin sudah ditutup peer. Tanpa ini,SIGPIPEakan menghentikan proses. DenganSIG_IGN,sendmengembalikan-1denganerrno = EPIPE(32) atauECONNRESET(104) tergantung timing. - Baris 67 (
send #1: return 24): Pengiriman pertama berhasil karena TCP send buffer masih ada ruang. Data masuk buffer kernel,sendkembali segera. Padahal server sudah menutup socket—FIN belum sampai ke klien saatsendpertama dieksekusi. - Baris 71 (
send #2: return -1, errno=104): Setelah menunggu 700 ms, RST dari server sudah sampai ke klien.sendkedua sekarang kembali denganerrno = ECONNRESET(104). Beberapa sistem mengembalikanEPIPE(32) — tergantung urutan FIN dan RST. Keduanya menandakan koneksi sudah mati. - Baris 91-93 (
struct timeval tv; setsockopt(...)): Mengatur timeout 1 detik untukrecv(lihat bab Timeout dan Retry untuk penjelasan rinci).recvakan kembali denganerrno = EAGAINbila tidak ada data dalam 1 detik.
Strategi Penanganan per Errno #
Berikut panduan penanganan setiap errno umum:
| Errno | Nama | Penanganan |
|---|---|---|
| 11 (EAGAIN) | Resource temporarily unavailable | Retry sama persis (bila pakai SO_RCVTIMEO) |
| 32 (EPIPE) | Broken pipe | Tutup socket, laporkan error, jangan retry |
| 104 (ECONNRESET) | Connection reset by peer | Tutup socket, kemungkinan retry dengan backoff |
| 111 (ECONNREFUSED) | Connection refused | Retry dengan exponential backoff |
| 110 (ETIMEDOUT) | Connection timed out | Retry dengan backoff |
| 113 (EHOSTUNREACH) | No route to host | Retry setelah jeda lama (mis. 1 menit) |