Aplikasi yang berjalan dalam satu process sudah kita kuasai pada proyek-proyek sebelumnya. Sekarang kita naik ke arsitektur client-server: dua program terpisah yang berkomunikasi lewat socket TCP. Pada proyek ini kita membangun server sederhana yang melayani koneksi client dengan protokol baris (line-based protocol) kita sendiri. Server mendukung perintah PING, ECHO, UPPER, LOWER, TIME, STATS, dan QUIT — fondasi yang sama dipakai oleh HTTP, Redis, dan SMTP.

Struktur proyek terdiri dari tiga modul: protocol.h/.c berisi parser protokol, server.c adalah TCP server yang melayani client, dan client.c adalah client interaktif yang membaca perintah dari stdin. Server menangani satu client pada satu waktu (single-threaded) — cukup untuk pembelajaran; produksi memakai thread pool atau epoll untuk concurrent.

Tampilan Program Akhir #

Sesi pemakaian berikut menunjukkan server dijalankan di port 8080 dan client mengirim beberapa perintah:

text
$ ./server 8080 &
Server berjalan di 0.0.0.0:8080 (Ctrl+C untuk berhenti)
[+] koneksi dari 127.0.0.1:52431

$ ./client 127.0.0.1 8080
Menghubungkan ke 127.0.0.1:8080...
Terhubung. Ketik perintah (QUIT untuk keluar).
PING
210 pong
ECHO hello world
200 hello world
UPPER convert me
200 CONVERT ME
TIME
200 2026-10-13 10:36:21
STATS
200 connections=1 requests=5
QUIT
410 bye
Koneksi ditutup.

Format balasan "<code> <text>\n" mirip dengan HTTP status line — client dapat mem-parse kode (200, 210, 410) untuk menentukan keberhasilan tanpa membaca teks. Pola ini umum di protokol teks seperti Redis dan Memcached.

Tujuan Pembelajaran #

Setelah menyelesaikan proyek ini, kita mampu:

  • memahami arsitektur client-server dan peran masing-masing,
  • membuat TCP server dengan socket, bind, listen, accept,
  • membuat TCP client dengan socket, connect,
  • merancang protokol baris sederhana dengan kode balasan,
  • menangani error jaringan: koneksi ditolak, host tidak ditemukan, koneksi terputus,
  • mengelola siklus hidup server: graceful shutdown lewat signal SIGINT.

Prasyarat #

Proyek ini memperkenalkan konsep baru seputar jaringan. Bab yang dirujuk:

  • File I/O Dasar — read/write untuk socket (mirip dengan file descriptor),
  • Pointer dan Array — buffer char[] untuk data jaringan,
  • Fungsi dan Header Files — modularisasi protocol.h/.c dan server.c/client.c,
  • Struktur Program C dan Fungsi main — argc/argv untuk port dan host,
  • Standar C — C11 untuk fitur <stdint.h>, _Static_assert.

Tahapan Pengerjaan #

Proyek dikerjakan dalam enam bagian berurutan. Setiap bagian membangun fondasi yang dipakai bagian berikutnya:

  1. Bagian 1: Konsep Client-Server — pemahaman TCP socket, alamat IP, port, dan urutan socket -> bind -> listen -> accept.
  2. Bagian 2: Implementasi Server — server.c dengan loop accept + handler per client.
  3. Bagian 3: Implementasi Client — client.c yang membaca stdin dan mengirim ke server.
  4. Bagian 4: Protokol Komunikasi — protocol.h/.c dengan parser baris dan builder balasan.
  5. Bagian 5: Error Handling dan Timeout — setsockopt untuk SO_RCVTIMEO, SO_REUSEADDR, dan penanganan SIGINT.
  6. Bagian 6: Testing dan Build — Makefile dengan target all, run-server, run-client, dan test otomatis.

Mulailah dari Bagian 1 untuk memahami konsep TCP sebelum menulis kode. Berkas sumber lengkap tersedia pada bagian bawah halaman masing-masing part.

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;
}