Server pada Bagian 2 tidak berguna tanpa client yang dapat menghubunginya. Pada bagian ini kita menulis client.c — program interaktif yang membaca perintah dari stdin, mengirim ke server lewat socket, lalu mencetak balasan. Tujuan bagian ini adalah memahami urutan panggilan client (socket -> connect -> write -> read) dan bagaimana pola request-reply bekerja pada TCP.

Konsep #

Client TCP lebih sederhana dari server: hanya butuh dua panggilan system untuk memulai koneksi. socket(AF_INET, SOCK_STREAM, 0) membuat socket (sama seperti server). connect(fd, &server_addr, sizeof(server_addr)) menghubungi server — system call ini memblokir hingga koneksi terbentuk atau gagal (timeout atau refused). Setelah connect berhasil, socket siap dipakai untuk write (kirim data ke server) dan read (terima balasan dari server).

Mekanisme connect: kernel mengirim paket TCP SYN ke alamat server, menunggu SYN-ACK balasan, lalu mengirim ACK untuk menyelesaikan three-way handshake. Bila server tidak merespons (mis. host mati), connect memblokir sampai timeout sistem (biasanya 75 detik di Linux). Bila server menolak (port tidak ada listener), connect gagal cepat dengan ECONNREFUSED.

Pola request-reply client: baca satu baris dari stdin dengan fgets, kirim ke server dengan write(fd, line, len), tunggu balasan dengan read(fd, reply, sizeof(reply)), cetak balasan ke stdout. Loop hingga client input QUIT atau server menutup koneksi. Penting: read dari socket tidak selalu mengembalikan satu baris penuh — TCP adalah stream, bukan message boundary. Implementasi kita memakai read_line yang membaca byte-per-byte hingga newline untuk memastikan satu baris balasan utuh.

Implementasi #

Inti dari client.c — fungsi main dengan urutan panggilan client:

c
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) {
        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);
        if (strncmp(reply, "410", 3) == 0) break;  /* BYE */
    }

    close(fd);
    printf("Koneksi ditutup.\n");
    return 0;
}

Fungsi helper read_line yang membaca byte-per-byte hingga newline:

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

Demo client minimum yang connect, kirim “PING”, baca balasan, tutup:

c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.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 sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8096);
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);

    if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("connect");
        return 1;
    }

    const char *msg = "PING\n";
    write(fd, msg, strlen(msg));

    char buf[256] = {0};
    ssize_t n = read(fd, buf, sizeof(buf) - 1);
    if (n > 0) {
        printf("Balasan server: %s", buf);
    }
    close(fd);
    return 0;
}

Pengujian #

Build client utama proyek:

bash
gcc -std=c11 -Wall -Wextra -pedantic -o client client.c protocol.c

Jalankan server di satu terminal (lihat Bagian 2), lalu jalankan client di terminal lain:

bash
./client 127.0.0.1 8097

Output yang Dihasilkan (input pengguna ditandai tanpa prefix):

text
Menghubungkan ke 127.0.0.1:8097...
Terhubung. Ketik perintah (QUIT untuk keluar).
PING
210 pong
ECHO hello
200 hello
QUIT
410 bye
Koneksi ditutup.

Verifikasi error handling: jalankan client ke port yang tidak ada server:

bash
./client 127.0.0.1 9999

Output yang Dihasilkan:

text
Menghubungkan ke 127.0.0.1:9999...
connect: Connection refused

Verifikasi validasi alamat IP:

bash
./client 999.999.999.999 8080

Output yang Dihasilkan:

text
Alamat IP tidak valid: 999.999.999.999

Mari kita telaah baris-baris kunci pada main client:

  1. Baris int fd = socket(AF_INET, SOCK_STREAM, 0);: buat socket client. Sama dengan server — file descriptor untuk komunikasi.
  2. Baris if (inet_pton(AF_INET, host, &addr.sin_addr) != 1): konversi string IP ke binary. Return value 1 menandakan sukses, 0 berarti format IP tidak valid, -1 berarti error sistem. Validasi ini menangkap input seperti "999.999.999.999" sebelum connect.
  3. Baris printf("Menghubungkan ke %s:%d...\n", host, port);: cetak status sebelum connect. Memberi feedback ke pengguna bahwa program sedang bekerja — tanpa ini, terminal terlihat seperti hang selama connect timeout.
  4. Baris if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0): hubungi server. connect memblokir hingga koneksi terbentuk (return 0) atau gagal (return -1 dengan errno). ECONNREFUSED = port tidak ada listener; ETIMEDOUT = server tidak merespons dalam batas waktu.
  5. Baris while (fgets(line, sizeof(line), stdin) != NULL): loop baca stdin. fgets membaca satu baris termasuk newline. Mengembalikan NULL saat EOF (Ctrl+D) — loop keluar.
  6. Baris if (strchr(line, '\n') == NULL): deteksi baris terlalu panjang. Bila input lebih panjang dari buffer, fgets tidak menyertakan newline. Kita flush sisa karakter dari stdin agar tidak bocor ke iterasi berikutnya.
  7. Baris if (write(fd, line, len) != (ssize_t)len): kirim data ke server. write dapat return kurang dari len (short write) — produksi memerlukan loop untuk mengirim seluruh buffer. Untuk demo, kita anggap short write tidak terjadi pada pesan pendek.
  8. Baris ssize_t r = read_line(fd, reply, sizeof(reply));: baca balasan baris utuh. Menggunakan helper yang membaca byte-per-byte hingga newline. Tanpa helper, read dapat return hanya sebagian baris — TCP stream tidak menjamin message boundary.
  9. Baris if (strncmp(reply, "410", 3) == 0) break;: deteksi kode BYE. Server membalas 410 bye\n untuk perintah QUIT — kita keluar loop setelah menerima balasan ini. Tanpa break, loop akan mencoba baca balasan berikutnya yang tidak ada (server sudah tutup koneksi).
  10. Baris close(fd); printf("Koneksi ditutup.\n");: tutup socket dan cetak status. Penting untuk selalu close agar file descriptor tidak bocor — walaupun process exit akan otomatis membersihkan, kebiasaan close eksplisit penting untuk program long-running.

Pada Bagian 4 kita akan merancang protokol formal dengan parser dan builder balasan.

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