Operasi jaringan secara default bisa memblokir tanpa batas waktu. recv pada socket yang tersambung tetapi diam bisa menggantung selamanya—kernel tidak tahu apakah server sedang sibuk, sudah crash, atau jaringan terputus. Bab ini membahas dua teknik fundamental: SO_RCVTIMEO untuk membatasi berapa lama recv akan menunggu, dan exponential backoff untuk strategi retry yang tidak membebani server atau jaringan. Tanpa keduanya, aplikasi jaringan rentan terhadap denial-of-service akibat koneksi yang tidak pernah selesai.
Mengapa Timeout Wajib #
TCP sendiri punya timeout default (biasanya sekitar 20 menit pada Linux untuk keepalive), tetapi ini terlalu lama untuk pengalaman pengguna yang baik. Bayangkan klien HTTP yang menunggu 20 menit untuk satu request yang gagal—pengguna pasti sudah menutup aplikasi sebelum itu. Timeout yang wajar untuk operasi jaringan biasanya 1-30 detik, tergantung protokol.
Tanpa timeout, tiga skenario bisa menggantung program:
- Server diam: server menerima koneksi tetapi tidak mengirim apa-apa (bug, atau sengaja untuk testing).
- Jaringan lambat/terputus: paket hilang dan retransmission membutuhkan waktu lama.
- Half-open connection: server sudah crash, tetapi klien tidak tahu karena TCP tidak mengirim notifikasi.
SO_RCVTIMEO: Timeout untuk recv #
setsockopt dengan opsi SO_RCVTIMEO mengatur timeout untuk semua panggilan recv berikutnya pada socket. Bila recv tidak mendapat data dalam waktu yang ditentukan, kernel mengembalikan -1 dengan errno = EAGAIN (atau EWOULDBLOCK).
Demo berikut menunjukkan penggunaan SO_RCVTIMEO. Server menerima koneksi tetapi diam (tidak mengirim apa-apa), klien mengatur timeout 1 detik dan recv harus kembali dalam ~1 detik dengan errno EAGAIN.
Server (yang diam):
/* File: timeout_server.c - Server yang accept tapi tidak kirim apa-apa */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 9094
int main(void) {
int srv = socket(AF_INET, SOCK_STREAM, 0);
if (srv < 0) { perror("socket"); return 1; }
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);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (bind(srv, (struct sockaddr *) &addr, sizeof(addr)) != 0) {
perror("bind"); close(srv); return 1;
}
if (listen(srv, 1) != 0) {
perror("listen"); close(srv); return 1;
}
printf("[SERVER] siap di 127.0.0.1:%d (akan diam)\n", PORT);
fflush(stdout);
int cli = accept(srv, NULL, NULL);
if (cli < 0) { perror("accept"); close(srv); return 1; }
printf("[SERVER] klien terhubung, server diam...\n");
fflush(stdout);
/* tidur 5 detik tanpa menulis apa-apa */
sleep(5);
close(cli);
close(srv);
return 0;
}Klien dengan timeout:
/* File: timeout_client.c - Demo setsockopt SO_RCVTIMEO pada klien TCP */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 9094
static void jeda_ms(int ms) {
struct timespec ts = { ms / 1000, (ms % 1000) * 1000000L };
nanosleep(&ts, NULL);
}
int main(void) {
jeda_ms(200);
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) { perror("socket"); return 1; }
/* Set recv timeout 1 detik */
struct timeval tv;
tv.tv_sec = 1;
tv.tv_usec = 0;
if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) != 0) {
perror("setsockopt"); close(fd); return 1;
}
printf("[KLIEN] SO_RCVTIMEO diatur ke 1 detik\n");
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (connect(fd, (struct sockaddr *) &addr, sizeof(addr)) != 0) {
printf("[KLIEN] connect gagal: %s (errno=%d)\n", strerror(errno), errno);
close(fd);
return 1;
}
/* Jika terhubung, recv harus timeout dalam 1 detik */
printf("[KLIEN] menunggu data dari server (akan timeout)...\n");
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);
return 0;
}Jalankan server dan klien:
gcc -std=c11 -Wall -Wextra -o timeout_server timeout_server.c
gcc -std=c11 -Wall -Wextra -o timeout_client timeout_client.c
./timeout_server &
sleep 0.3
./timeout_client
waitOutput yang Dihasilkan (Klien):
[KLIEN] SO_RCVTIMEO diatur ke 1 detik
[KLIEN] menunggu data dari server (akan timeout)...
[KLIEN] recv return -1 setelah 1.029 detik (errno=11: EAGAIN/timeout)Output yang Dihasilkan (Server):
[SERVER] siap di 127.0.0.1:9094 (akan diam)
[SERVER] klien terhubung, server diam...Mari kita telaah baris-baris kode di atas:
- Baris 22-25 (
struct timeval tv): Mengatur timeout 1 detik (1.000.000 mikrodetik).timevalpunya dua field:tv_sec(detik) dantv_usec(mikrodetik). Untuk timeout 500 ms, gunakantv.tv_sec = 0; tv.tv_usec = 500000;. - Baris 26 (
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv))): OpsiSO_RCVTIMEOdiletakkan pada levelSOL_SOCKET(berlaku untuk semua jenis socket). Konfigurasi ini berlaku untuk semua panggilanrecvberikutnya. Untuksend, gunakanSO_SNDTIMEO. - Baris 43-45 (
clock_gettime(CLOCK_MONOTONIC, ...)): Mengukur waktu sebelum dan sesudahrecv.CLOCK_MONOTONICadalah clock yang tidak terpengaruh perubahan waktu sistem (mis. NTP sync)—cocok untuk mengukur durasi. - Baris 47 (
recv return -1 setelah 1.029 detik):recvkembali setelah 1.029 detik (sedikit lebih lama dari 1.000 karena scheduling overhead), denganerrno = EAGAIN(11). NilaiEAGAINdanEWOULDBLOCKseringkali sama nilainya—kita bisa cek keduanya denganif (errno == EAGAIN || errno == EWOULDBLOCK).
Retry dengan Exponential Backoff #
Bila operasi jaringan gagal karena kondisi sementara (server sedang restart, jaringan fluktuasi), kita ingin mencoba lagi—tetapi tidak boleh membebani server. Strategi exponential backoff menunggu waktu yang semakin lama di antara setiap retry: 200 ms, 400 ms, 800 ms, 1600 ms, dst. Strategi ini memberi waktu server untuk pulih tanpa membombardir dengan request.
/* File: backoff.c - Retry connect dengan exponential backoff */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 9999 /* port tanpa server - akan refuse */
static double now_sec(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec + ts.tv_nsec / 1e9;
}
int main(void) {
int max_attempts = 4;
int base_ms = 200; /* 200ms, 400ms, 800ms, 1600ms */
for (int attempt = 1; attempt <= max_attempts; attempt++) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) { perror("socket"); return 1; }
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
double t0 = now_sec();
int rc = connect(fd, (struct sockaddr *) &addr, sizeof(addr));
double elapsed = now_sec() - t0;
if (rc == 0) {
printf("Attempt %d: connect BERHASIL dalam %.3f detik\n",
attempt, elapsed);
close(fd);
return 0;
}
printf("Attempt %d: connect gagal (%s) dalam %.3f detik\n",
attempt, strerror(errno), elapsed);
close(fd);
if (attempt < max_attempts) {
int backoff_ms = base_ms << (attempt - 1); /* 200, 400, 800 */
printf(" -> retry dalam %d ms (exponential backoff)\n", backoff_ms);
struct timespec ts = { backoff_ms / 1000,
(backoff_ms % 1000) * 1000000L };
nanosleep(&ts, NULL);
}
}
printf("\nGagal setelah %d attempt. Berhenti mencoba.\n", max_attempts);
return 1;
}Compile dan jalankan (tidak ada server di port 9999, jadi semua attempt akan gagal):
gcc -std=c11 -Wall -Wextra -o backoff backoff.c
timeout 10 ./backoffOutput yang Dihasilkan:
Attempt 1: connect gagal (Connection refused) dalam 0.000 detik
-> retry dalam 200 ms (exponential backoff)
Attempt 2: connect gagal (Connection refused) dalam 0.000 detik
-> retry dalam 400 ms (exponential backoff)
Attempt 3: connect gagal (Connection refused) dalam 0.000 detik
-> retry dalam 800 ms (exponential backoff)
Attempt 4: connect gagal (Connection refused) dalam 0.000 detik
Gagal setelah 4 attempt. Berhenti mencoba.Total waktu yang dihabiskan adalah 200 + 400 + 800 = 1400 ms untuk retry, plus sekitar 1.5 detik untuk eksekusi keseluruhan. Ini jauh lebih cepat daripada retry setiap detik selama 60 detik, tetap memberi server waktu untuk pulih di antara percobaan.
Strategi Backoff Lanjutan #
Exponential backoff sederhana bisa diperbaiki dengan beberapa tambahan:
- Jitter: tambahkan komponen acak (mis. 0-50% dari base delay) untuk menghindari thundering herd—saat banyak klien retry pada waktu yang sama persis. Jitter membuat distribusi retry lebih merata.
- Cap maksimum: batasi backoff maksimum (mis. 30 detik) agar tidak tumbuh tak terhingga.
- Circuit breaker: setelah N kegagalan berturut-turut, berhenti mencoba untuk periode tertentu (mis. 1 menit) sebelum mencoba lagi.
/* Contoh backoff dengan jitter */
int backoff_ms = base_ms << (attempt - 1);
if (backoff_ms > 30000) backoff_ms = 30000; /* cap 30 detik */
int jitter = rand() % (backoff_ms / 2); /* 0..backoff/2 */
backoff_ms += jitter;