Jaringan tidak selalu ideal: client bisa disconnect mendadak, server bisa hang, koneksi bisa terputus karena Wi-Fi turun. Pada bagian ini kita memperkuat server dan client dengan penanganan error yang lebih baik: setsockopt untuk timeout, signal(SIGPIPE, SIG_IGN) agar server tidak crash saat client disconnect mendadak, dan SO_REUSEADDR agar restart server cepat. Tujuan bagian ini adalah membangun program jaringan yang tangguh terhadap kondisi error.
Konsep #
Tiga kategori error jaringan yang perlu ditangani: (1) error koneksi — ECONNREFUSED (port tidak ada listener), ETIMEDOUT (server tidak merespons), ENETUNREACH (host tidak terjangkau); (2) error selama komunikasi — EPIPE (peer menutup koneksi), ECONNRESET (peer reset koneksi); (3) error sumber daya — EMFILE (terlalu banyak fd), ENOMEM (malloc gagal). Tanpa penanganan, error ini menyebabkan crash atau behavior aneh.
Mekanisme setsockopt: set option pada socket sebelum bind/connect. SO_RCVTIMEO dan SO_SNDTIMEO mengatur timeout untuk read/write — bila operasi tidak selesai dalam waktu yang ditentukan, return -1 dengan errno = EAGAIN atau EWOULDBLOCK. SO_REUSEADDR mengizinkan bind ke port yang masih dalam TIME_WAIT state — penting untuk restart server cepat.
Mekanisme SIGPIPE: saat write ke socket yang peer-nya sudah tutup, kernel mengirim SIGPIPE ke process. Default handler mengakhiri process — server mati tanpa pesan. Solusi: signal(SIGPIPE, SIG_IGN) agar signal diabaikan, sehingga write mengembalikan -1 dengan errno = EPIPE — bisa ditangani dengan break loop.
Mekanisme SIGINT untuk graceful shutdown: ketika pengguna menekan Ctrl+C, kernel mengirim SIGINT. Handler kita mengubah flag server_running = 0 — loop accept akan keluar di iterasi berikutnya. Tanpa handler, SIGINT langsung membunuh process tanpa mencetak statistik akhir.
Implementasi #
Demo setsockopt untuk timeout dan reuse address:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(void) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) { perror("socket"); return 1; }
struct timeval tv;
tv.tv_sec = 2;
tv.tv_usec = 0;
if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) {
perror("setsockopt SO_RCVTIMEO");
} else {
printf("SO_RCVTIMEO di-set ke 2 detik\n");
}
if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) < 0) {
perror("setsockopt SO_SNDTIMEO");
} else {
printf("SO_SNDTIMEO di-set ke 2 detik\n");
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
int rc = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
if (rc < 0) {
printf("connect() gagal: %s (errno %d)\n", strerror(errno), errno);
}
close(fd);
/* Demo SO_REUSEADDR */
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
int yes = 1;
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) {
perror("setsockopt SO_REUSEADDR");
} else {
printf("SO_REUSEADDR di-set ke 1\n");
}
close(listen_fd);
return 0;
}Output yang Dihasilkan:
SO_RCVTIMEO di-set ke 2 detik
SO_SNDTIMEO di-set ke 2 detik
connect() gagal: Connection refused (errno 111)
SO_REUSEADDR di-set ke 1Konfigurasi signal handler di main server:
static int server_running = 1;
static void handle_signal(int sig) {
(void)sig;
server_running = 0;
write(STDOUT_FILENO, "\rShutting down...\n", 19);
}
int main(int argc, char *argv[]) {
/* ... parsing port ... */
signal(SIGINT, handle_signal);
signal(SIGTERM, handle_signal);
signal(SIGPIPE, SIG_IGN);
/* ... socket, bind, listen, accept loop ... */
while (server_running) {
int client_fd = accept(listen_fd, ...);
if (client_fd < 0) {
if (errno == EINTR) continue;
perror("accept");
continue;
}
handle_client(client_fd);
}
/* ... cleanup ... */
}Pengujian #
Build dan jalankan demo socket options:
gcc -std=c11 -Wall -Wextra -pedantic -o socket_opts demo_socket_opts.c
./socket_optsOutput yang Dihasilkan:
SO_RCVTIMEO di-set ke 2 detik
SO_SNDTIMEO di-set ke 2 detik
connect() gagal: Connection refused (errno 111)
SO_REUSEADDR di-set ke 1Verifikasi ECONNREFUSED (errno 111) terjadi saat connect ke port yang tidak ada listener. Test juga SIGPIPE handling dengan server utama:
gcc -std=c11 -Wall -Wextra -pedantic -o server server.c protocol.c
./server 8101 &
sleep 0.3
# Client connect lalu langsung tutup tanpa QUIT
{ printf "PING\n"; sleep 0.1; } | ./client 127.0.0.1 8101
# Verifikasi server masih hidup — kirim koneksi lagi
{ printf "PING\nQUIT\n"; } | ./client 127.0.0.1 8101
kill %1Output yang Dihasilkan (di sisi server, dua koneksi ditangani tanpa crash):
Server berjalan di 0.0.0.0:8101 (Ctrl+C untuk berhenti)
[+] koneksi dari 127.0.0.1:51234
[-] koneksi ditutup
[+] koneksi dari 127.0.0.1:51244
[-] koneksi ditutupVerifikasi graceful shutdown dengan SIGINT:
./server 8102 &
SERVER_PID=$!
sleep 0.3
kill -INT $SERVER_PID
sleep 0.5Output yang Dihasilkan:
Server berjalan di 0.0.0.0:8102 (Ctrl+C untuk berhenti)
Shutting down...
Total koneksi: 0, total requests: 0Tanpa handler SIGINT, server akan mati tanpa mencetak Shutting down... atau statistik.
Mari kita telaah baris-baris kunci pada error handling:
- Baris
struct timeval tv; tv.tv_sec = 2; tv.tv_usec = 0;: konfigurasi timeout.timevalpunya dua field: detik dan mikrodetik. Untuk 2.5 detik, settv_sec = 2; tv_usec = 500000;. - Baris
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)): set timeout untukread. Setelah operasireadmemblokir selama 2 detik tanpa data, return-1denganerrno = EAGAIN. - Baris
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)): set timeout untukwrite. Berguna saat peer lambat menerima data —writetidak memblokir selamanya. - Baris
int rc = connect(fd, ...): connect dengan timeout aktif. Bila server tidak merespons dalam 2 detik,connectreturn -1 denganerrno = ETIMEDOUT. Untuk demo ini, port 9999 tidak ada listener sehingga gagal cepat denganECONNREFUSED. - Baris
int yes = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));: aktifkan reuse-address. Tanpa ini, restart server dalam TIME_WAIT period gagal denganAddress already in use. - Baris
signal(SIGINT, handle_signal);: daftarkan handler untuk SIGINT. Handler hanya mengubah flag dan mencetak pesan — tidak memanggil fungsi non-async-signal-safe (seperti printf). Handler yang aman hanya memakaiwritedan set variabel volatile sig_atomic_t. - Baris
signal(SIGPIPE, SIG_IGN);: abaikan SIGPIPE.writeke socket yang peer-nya tutup akan return -1 denganerrno = EPIPE— bisa ditangani dengan break loop. - Baris
if (errno == EINTR) continue;(di loop accept): bilaacceptdiinterupsi signal (mis. SIGINT diterima saatacceptmemblokir), errno = EINTR. Kita continue loop, bukan error — loop akan cekserver_runningdan keluar bila handler sudah set ke 0. - Baris
write(STDOUT_FILENO, "\rShutting down...\n", 19);(di handler): cetak dari signal handler.writeadalah async-signal-safe — boleh dipanggil dari handler.printftidak aman karena memakai buffer internal yang bisa rusak bila diinterupsi. - Baris
while (server_running): loop utama memeriksa flag. Saat SIGINT diterima, handler setserver_running = 0— iterasi berikutnya keluar dari loop. Pola “flag + check” lebih aman daripadaexit()dari handler.
Pada Bagian 6 kita akan menyusun Makefile lengkap dan menulis test otomatis untuk verifikasi end-to-end.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
client.c
Unduh berkas/* client.c - Client TCP untuk server protokol baris
*
* Build:
* gcc -std=c11 -Wall -Wextra -pedantic -o client client.c protocol.c
* Jalankan:
* ./client [host] [port] # default 127.0.0.1:8080
*
* Setelah terhubung, ketik perintah lalu Enter. Contoh:
* PING
* ECHO hello world
* UPPER convert me
* TIME
* STATS
* QUIT
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "protocol.h"
/* Membaca satu baris dari socket. Mengembalikan jumlah byte termasuk newline
* yang dibaca, 0 bila koneksi ditutup, -1 bila error. */
static ssize_t read_line(int fd, char *buf, size_t cap) {
size_t total = 0;
while (total < cap - 1) {
char c;
ssize_t n = read(fd, &c, 1);
if (n <= 0) {
return (n == 0) ? (ssize_t)total : -1;
}
buf[total++] = c;
if (c == '\n') {
break;
}
}
buf[total] = '\0';
return (ssize_t)total;
}
int main(int argc, char *argv[]) {
const char *host = "127.0.0.1";
int port = PROTO_PORT_DEFAULT;
if (argc > 1) {
host = argv[1];
}
if (argc > 2) {
port = (int)strtol(argv[2], NULL, 10);
}
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("socket");
return 1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons((uint16_t)port);
if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) {
fprintf(stderr, "Alamat IP tidak valid: %s\n", host);
close(fd);
return 1;
}
printf("Menghubungkan ke %s:%d...\n", host, port);
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("connect");
close(fd);
return 1;
}
printf("Terhubung. Ketik perintah (QUIT untuk keluar).\n");
char line[PROTO_BUFFER_MAX];
char reply[PROTO_BUFFER_MAX];
while (fgets(line, sizeof(line), stdin) != NULL) {
/* Pastikan ada newline. */
if (strchr(line, '\n') == NULL) {
/* Baris terlalu panjang, flush stdin. */
int c;
while ((c = getchar()) != '\n' && c != EOF) {
/* loop */
}
}
size_t len = strlen(line);
if (write(fd, line, len) != (ssize_t)len) {
perror("write");
break;
}
ssize_t r = read_line(fd, reply, sizeof(reply));
if (r <= 0) {
printf("Server menutup koneksi.\n");
break;
}
fputs(reply, stdout);
/* Berhenti bila server membalas kode BYE. */
if (strncmp(reply, "410", 3) == 0) {
break;
}
}
close(fd);
printf("Koneksi ditutup.\n");
return 0;
}
protocol.c
Unduh berkas/* protocol.c - Implementasi parser protokol */
#include "protocol.h"
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
static int eq_ci(const char *a, const char *b) {
while (*a != '\0' && *b != '\0') {
if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) {
return 0;
}
a++;
b++;
}
return *a == '\0' && *b == '\0';
}
const char *proto_command_name(proto_command cmd) {
switch (cmd) {
case PROTO_CMD_PING: return "PING";
case PROTO_CMD_ECHO: return "ECHO";
case PROTO_CMD_UPPER: return "UPPER";
case PROTO_CMD_LOWER: return "LOWER";
case PROTO_CMD_TIME: return "TIME";
case PROTO_CMD_STATS: return "STATS";
case PROTO_CMD_QUIT: return "QUIT";
case PROTO_CMD_UNKNOWN:
default: return "UNKNOWN";
}
}
int proto_parse_line(const char *line, proto_message *msg) {
if (line == NULL || msg == NULL) {
return -1;
}
/* Lewati whitespace di awal. */
while (*line == ' ' || *line == '\t') {
line++;
}
if (*line == '\0' || *line == '\n' || *line == '\r') {
return -1;
}
/* Ekstrak command word pertama. */
char cmd_buf[PROTO_COMMAND_MAX];
int consumed = 0;
if (sscanf(line, "%15s%n", cmd_buf, &consumed) != 1) {
return -1;
}
if (eq_ci(cmd_buf, "PING")) {
msg->command = PROTO_CMD_PING;
} else if (eq_ci(cmd_buf, "ECHO")) {
msg->command = PROTO_CMD_ECHO;
} else if (eq_ci(cmd_buf, "UPPER")) {
msg->command = PROTO_CMD_UPPER;
} else if (eq_ci(cmd_buf, "LOWER")) {
msg->command = PROTO_CMD_LOWER;
} else if (eq_ci(cmd_buf, "TIME")) {
msg->command = PROTO_CMD_TIME;
} else if (eq_ci(cmd_buf, "STATS")) {
msg->command = PROTO_CMD_STATS;
} else if (eq_ci(cmd_buf, "QUIT") || eq_ci(cmd_buf, "EXIT")) {
msg->command = PROTO_CMD_QUIT;
} else {
msg->command = PROTO_CMD_UNKNOWN;
}
/* Salin sisa argumen, lewati satu spasi setelah command word. */
const char *args_start = line + consumed;
while (*args_start == ' ' || *args_start == '\t') {
args_start++;
}
strncpy(msg->args, args_start, PROTO_LINE_MAX - 1);
msg->args[PROTO_LINE_MAX - 1] = '\0';
/* Hapus newline di akhir. */
size_t len = strlen(msg->args);
while (len > 0 && (msg->args[len - 1] == '\n' ||
msg->args[len - 1] == '\r')) {
msg->args[--len] = '\0';
}
msg->args_len = len;
return 0;
}
int proto_build_reply(char *out, size_t out_cap,
proto_reply code, const char *text) {
if (out == NULL || out_cap < 8) {
return -1;
}
const char *body = (text != NULL) ? text : "";
int n = snprintf(out, out_cap, "%d %s\n", (int)code, body);
if (n < 0 || (size_t)n >= out_cap) {
return -1;
}
return n;
}
server.c
Unduh berkas/* server.c - Server TCP sederhana berbasis protokol baris
*
* Build:
* gcc -std=c11 -Wall -Wextra -pedantic -o server server.c protocol.c
* Jalankan:
* ./server [port] # default port 8080
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <ctype.h>
#include <signal.h>
#include <time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "protocol.h"
static int server_running = 1;
static long total_requests = 0;
static long total_connections = 0;
static void handle_signal(int sig) {
(void)sig;
server_running = 0;
/* Memberi kesempatan loop utama keluar. */
write(STDOUT_FILENO, "\rShutting down...\n", 19);
}
/* Memproses satu pesan dari client, mengembalikan kode reply + body. */
static void process_message(const proto_message *msg,
proto_reply *reply_code, char *body, size_t body_cap) {
total_requests++;
switch (msg->command) {
case PROTO_CMD_PING:
*reply_code = PROTO_REPLY_PONG;
snprintf(body, body_cap, "pong");
break;
case PROTO_CMD_ECHO:
*reply_code = PROTO_REPLY_OK;
snprintf(body, body_cap, "%s", msg->args);
break;
case PROTO_CMD_UPPER: {
char buf[PROTO_LINE_MAX];
strncpy(buf, msg->args, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
for (char *p = buf; *p != '\0'; p++) {
*p = (char)toupper((unsigned char)*p);
}
*reply_code = PROTO_REPLY_OK;
snprintf(body, body_cap, "%s", buf);
break;
}
case PROTO_CMD_LOWER: {
char buf[PROTO_LINE_MAX];
strncpy(buf, msg->args, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
for (char *p = buf; *p != '\0'; p++) {
*p = (char)tolower((unsigned char)*p);
}
*reply_code = PROTO_REPLY_OK;
snprintf(body, body_cap, "%s", buf);
break;
}
case PROTO_CMD_TIME: {
time_t t = time(NULL);
struct tm *tm_info = localtime(&t);
char datebuf[32];
strftime(datebuf, sizeof(datebuf), "%Y-%m-%d %H:%M:%S", tm_info);
*reply_code = PROTO_REPLY_OK;
snprintf(body, body_cap, "%s", datebuf);
break;
}
case PROTO_CMD_STATS:
*reply_code = PROTO_REPLY_OK;
snprintf(body, body_cap,
"connections=%ld requests=%ld",
total_connections, total_requests);
break;
case PROTO_CMD_QUIT:
*reply_code = PROTO_REPLY_BYE;
snprintf(body, body_cap, "bye");
break;
case PROTO_CMD_UNKNOWN:
default:
*reply_code = PROTO_REPLY_UNKNOWN;
snprintf(body, body_cap, "unknown command");
break;
}
}
static void handle_client(int client_fd) {
char in_buf[PROTO_BUFFER_MAX];
char out_buf[PROTO_BUFFER_MAX];
char body[PROTO_LINE_MAX];
ssize_t n;
/* Akumulasi sisa data dari read sebelumnya. */
size_t leftover = 0;
while (server_running) {
ssize_t got = read(client_fd, in_buf + leftover,
sizeof(in_buf) - leftover - 1);
if (got <= 0) {
break;
}
size_t total = leftover + (size_t)got;
in_buf[total] = '\0';
/* Proses per baris. */
char *line_start = in_buf;
char *newline;
size_t processed = 0;
while ((newline = strchr(line_start, '\n')) != NULL) {
*newline = '\0';
proto_message msg;
if (proto_parse_line(line_start, &msg) == 0) {
proto_reply code = PROTO_REPLY_OK;
body[0] = '\0';
process_message(&msg, &code, body, sizeof(body));
proto_build_reply(out_buf, sizeof(out_buf), code, body);
size_t reply_len = strlen(out_buf);
write(client_fd, out_buf, reply_len);
if (msg.command == PROTO_CMD_QUIT) {
close(client_fd);
return;
}
}
processed = (size_t)(newline - in_buf) + 1;
line_start = newline + 1;
}
/* Pindahkan sisa yang belum diproses ke awal buffer. */
leftover = total - processed;
if (leftover > 0 && processed > 0) {
memmove(in_buf, in_buf + processed, leftover);
}
(void)n;
}
close(client_fd);
}
int main(int argc, char *argv[]) {
int port = PROTO_PORT_DEFAULT;
if (argc > 1) {
port = (int)strtol(argv[1], NULL, 10);
if (port <= 0 || port > 65535) {
fprintf(stderr, "Port tidak valid: %s\n", argv[1]);
return 1;
}
}
signal(SIGINT, handle_signal);
signal(SIGTERM, handle_signal);
signal(SIGPIPE, SIG_IGN);
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
perror("socket");
return 1;
}
/* Aktifkan reuse-address agar binding cepat setelah restart. */
int yes = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons((uint16_t)port);
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind");
close(listen_fd);
return 1;
}
if (listen(listen_fd, 8) < 0) {
perror("listen");
close(listen_fd);
return 1;
}
printf("Server berjalan di 0.0.0.0:%d (Ctrl+C untuk berhenti)\n", port);
while (server_running) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(listen_fd,
(struct sockaddr *)&client_addr, &client_len);
if (client_fd < 0) {
if (errno == EINTR) {
continue;
}
perror("accept");
continue;
}
total_connections++;
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip));
printf("[+] koneksi dari %s:%d\n",
ip, ntohs(client_addr.sin_port));
handle_client(client_fd);
printf("[-] koneksi ditutup\n");
}
close(listen_fd);
printf("Total koneksi: %ld, total requests: %ld\n",
total_connections, total_requests);
return 0;
}