Server dan client di Bagian 2-3 saling bertukar teks tetapi belum ada kontrak formal tentang format pesan. Pada bagian ini kita menulis protocol.h dan protocol.c yang mendefinisikan command yang dikenal, kode balasan, dan fungsi parser/builder pesan. Tujuan bagian ini adalah memisahkan logika protokol dari logika socket — server dan client dapat berbagi modul yang sama untuk encode/decode pesan.

Konsep #

Protokol kita adalah line-based: setiap pesan adalah satu baris teks diakhiri karakter newline (\n). Format pesan dari client: <COMMAND> <ARG1> <ARG2>...\n (mis. ECHO hello world\n). Format balasan server: <CODE> <TEXT>\n (mis. 200 hello world\n). Baris kosong atau hanya whitespace dianggap invalid dan ditolak.

Command yang dikenal didefinisikan di enum proto_command: PING, ECHO, UPPER, LOWER, TIME, STATS, QUIT. Kode balasan di enum proto_reply: 200 OK, 210 PONG, 400 ERROR, 404 UNKNOWN, 410 BYE. Kode dipilih mirip HTTP status code agar familiar — 2xx sukses, 4xx error client.

Mekanisme parser proto_parse_line: skip whitespace di awal, ekstrak kata pertama sebagai command dengan sscanf("%15s%n", ...), bandingkan dengan command yang dikenal (case-insensitive), lalu salin sisa baris sebagai args. Mekanisme builder proto_build_reply: format kode dan teks dengan snprintf("%d %s\n", ...) ke buffer. Pemisahan parser/builder ke modul terpisah memungkinkan client juga memakai proto_parse_line bila ingin mem-parse balasan secara terstruktur.

Implementasi #

Header protocol.h (ringkas):

c
#ifndef PROTOCOL_H
#define PROTOCOL_H

#include <stddef.h>

#define PROTO_BUFFER_MAX   1024
#define PROTO_COMMAND_MAX  16
#define PROTO_LINE_MAX     256

typedef enum {
    PROTO_CMD_UNKNOWN = 0,
    PROTO_CMD_PING,
    PROTO_CMD_ECHO,
    PROTO_CMD_UPPER,
    PROTO_CMD_LOWER,
    PROTO_CMD_TIME,
    PROTO_CMD_STATS,
    PROTO_CMD_QUIT
} proto_command;

typedef enum {
    PROTO_REPLY_OK      = 200,
    PROTO_REPLY_PONG    = 210,
    PROTO_REPLY_ERROR   = 400,
    PROTO_REPLY_UNKNOWN = 404,
    PROTO_REPLY_BYE     = 410
} proto_reply;

typedef struct {
    proto_command command;
    char          args[PROTO_LINE_MAX];
    size_t        args_len;
} proto_message;

int proto_parse_line(const char *line, proto_message *msg);
int proto_build_reply(char *out, size_t out_cap,
                      proto_reply code, const char *text);
const char *proto_command_name(proto_command cmd);

#endif /* PROTOCOL_H */

Implementasi parser proto_parse_line di protocol.c:

c
int proto_parse_line(const char *line, proto_message *msg) {
    if (line == NULL || msg == NULL) return -1;
    while (*line == ' ' || *line == '\t') line++;
    if (*line == '\0' || *line == '\n' || *line == '\r') return -1;

    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;

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

Demo mandiri yang menguji parser dan builder:

c
#include <stdio.h>
#include "protocol.h"

int main(void) {
    proto_message msg;

    if (proto_parse_line("PING", &msg) == 0) {
        printf("Command: %s, args='%s', len=%zu\n",
               proto_command_name(msg.command), msg.args, msg.args_len);
    }
    if (proto_parse_line("ECHO hello world", &msg) == 0) {
        printf("Command: %s, args='%s', len=%zu\n",
               proto_command_name(msg.command), msg.args, msg.args_len);
    }
    if (proto_parse_line("FOO bar", &msg) == 0) {
        printf("Command: %s, args='%s', len=%zu\n",
               proto_command_name(msg.command), msg.args, msg.args_len);
    }
    if (proto_parse_line("", &msg) != 0) {
        printf("Empty line -> parse error (expected)\n");
    }

    char buf[256];
    int n = proto_build_reply(buf, sizeof(buf), PROTO_REPLY_PONG, "pong");
    printf("Reply: '%s' (length %d)\n", buf, n);
    return 0;
}

Output yang Dihasilkan:

text
Command: PING, args='', len=0
Command: ECHO, args='hello world', len=11
Command: UNKNOWN, args='bar', len=3
Empty line -> parse error (expected)
Reply: '210 pong
' (length 9)

Pengujian #

Compile dan jalankan demo parser:

bash
gcc -std=c11 -Wall -Wextra -pedantic -I/home/z/my-project/content/c/proyek-c/proyek-10-client-server-sederhana \
    -o /tmp/proto_demo /tmp/proto_demo.c \
    /home/z/my-project/content/c/proyek-c/proyek-10-client-server-sederhana/protocol.c
/tmp/proto_demo

Output yang Dihasilkan (sama dengan di atas):

text
Command: PING, args='', len=0
Command: ECHO, args='hello world', len=11
Command: UNKNOWN, args='bar', len=3
Empty line -> parse error (expected)
Reply: '210 pong
' (length 9)

Catatan: balasan '210 pong\n' (length 9 — 8 karakter + newline). Single quote di output adalah cara printf menampilkan string yang mengandung newline dalam satu baris.

Test integrasi: jalankan server dan client dengan beberapa perintah:

bash
./server 8098 &
sleep 0.3
{ echo "PING"; echo "ECHO hello"; echo "UPPER abc"; echo "FOO bar"; echo "QUIT"; } | ./client 127.0.0.1 8098
kill %1

Output yang Dihasilkan (di sisi client):

text
Menghubungkan ke 127.0.0.1:8098...
Terhubung. Ketik perintah (QUIT untuk keluar).
210 pong
200 hello
200 ABC
404 unknown command
410 bye
Koneksi ditutup.

Perintah FOO bar yang tidak dikenal membalas 404 unknown command — kode PROTO_REPLY_UNKNOWN menandakan command tidak dikenal, mirip HTTP 404.

Mari kita telaah baris-baris kunci pada parser:

  1. Baris if (line == NULL || msg == NULL) return -1;: penjaga NULL. Bila pemanggil lupa mengalokasikan msg atau memberi NULL line, kita kembalikan -1 alih-alih crash.
  2. Baris while (*line == ' ' || *line == '\t') line++;: skip whitespace di awal. Memungkinkan client mengirim " PING\n" (dengan spasi leading) dan tetap diparse dengan benar.
  3. Baris if (*line == '\0' || *line == '\n' || *line == '\r') return -1;: tolak baris kosong. Tanpa penolakan, baris kosong akan diparse sebagai command kosong yang tidak dikenal — pesan error membingungkan.
  4. Baris if (sscanf(line, "%15s%n", cmd_buf, &consumed) != 1): ekstrak kata pertama. Format %15s membaca maksimal 15 karakter (sisanya PROTO_COMMAND_MAX - 1 untuk null terminator). %n menyimpan jumlah karakter yang dikonsumsi ke consumed — kita pakai untuk melompati command word saat mengambil args.
  5. Baris if (eq_ci(cmd_buf, "PING")): perbandingan case-insensitive. eq_ci (custom helper) membandingkan karakter per karakter dengan toupper. Memungkinkan client mengirim "ping", "Ping", atau "PING" — semua dikenali.
  6. Baris msg->command = PROTO_CMD_UNKNOWN;: default bila command tidak cocok dengan yang dikenal. Server akan membalas 404 unknown command untuk command unknown.
  7. Baris strncpy(msg->args, args_start, PROTO_LINE_MAX - 1);: salin sisa baris sebagai args. Batas PROTO_LINE_MAX - 1 mencegah overflow buffer — sisanya dipotong.
  8. Baris msg->args[PROTO_LINE_MAX - 1] = '\0';: pastikan null-terminated. strncpy tidak selalu menambah null bila source lebih panjang dari batas.
  9. Baris while (len > 0 && (msg->args[len - 1] == '\n' || ...)): hapus newline di akhir args. Client mengirim "ECHO hello\n" — kita simpan "hello" tanpa newline agar mudah diproses server.
  10. Baris msg->args_len = len;: simpan panjang args. Berguna bila args mengandung spasi — strlen tetap bekerja tetapi args_len lebih eksplisit.

Pada Bagian 5 kita akan menambah error handling dan timeout agar server dan client lebih tangguh menghadapi kondisi jaringan yang tidak ideal.

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