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:

c
#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:

text
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 1

Konfigurasi signal handler di main server:

c
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:

bash
gcc -std=c11 -Wall -Wextra -pedantic -o socket_opts demo_socket_opts.c
./socket_opts

Output yang Dihasilkan:

text
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 1

Verifikasi ECONNREFUSED (errno 111) terjadi saat connect ke port yang tidak ada listener. Test juga SIGPIPE handling dengan server utama:

bash
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 %1

Output yang Dihasilkan (di sisi server, dua koneksi ditangani tanpa crash):

text
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 ditutup

Verifikasi graceful shutdown dengan SIGINT:

bash
./server 8102 &
SERVER_PID=$!
sleep 0.3
kill -INT $SERVER_PID
sleep 0.5

Output yang Dihasilkan:

text
Server berjalan di 0.0.0.0:8102 (Ctrl+C untuk berhenti)
Shutting down...
Total koneksi: 0, total requests: 0

Tanpa handler SIGINT, server akan mati tanpa mencetak Shutting down... atau statistik.

Mari kita telaah baris-baris kunci pada error handling:

  1. Baris struct timeval tv; tv.tv_sec = 2; tv.tv_usec = 0;: konfigurasi timeout. timeval punya dua field: detik dan mikrodetik. Untuk 2.5 detik, set tv_sec = 2; tv_usec = 500000;.
  2. Baris setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)): set timeout untuk read. Setelah operasi read memblokir selama 2 detik tanpa data, return -1 dengan errno = EAGAIN.
  3. Baris setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)): set timeout untuk write. Berguna saat peer lambat menerima data — write tidak memblokir selamanya.
  4. Baris int rc = connect(fd, ...): connect dengan timeout aktif. Bila server tidak merespons dalam 2 detik, connect return -1 dengan errno = ETIMEDOUT. Untuk demo ini, port 9999 tidak ada listener sehingga gagal cepat dengan ECONNREFUSED.
  5. 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 dengan Address already in use.
  6. 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 memakai write dan set variabel volatile sig_atomic_t.
  7. Baris signal(SIGPIPE, SIG_IGN);: abaikan SIGPIPE. write ke socket yang peer-nya tutup akan return -1 dengan errno = EPIPE — bisa ditangani dengan break loop.
  8. Baris if (errno == EINTR) continue; (di loop accept): bila accept diinterupsi signal (mis. SIGINT diterima saat accept memblokir), errno = EINTR. Kita continue loop, bukan error — loop akan cek server_running dan keluar bila handler sudah set ke 0.
  9. Baris write(STDOUT_FILENO, "\rShutting down...\n", 19); (di handler): cetak dari signal handler. write adalah async-signal-safe — boleh dipanggil dari handler. printf tidak aman karena memakai buffer internal yang bisa rusak bila diinterupsi.
  10. Baris while (server_running): loop utama memeriksa flag. Saat SIGINT diterima, handler set server_running = 0 — iterasi berikutnya keluar dari loop. Pola “flag + check” lebih aman daripada exit() 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
c
/* 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
c
/* 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
c
/* 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;
}