Sebelum menulis kode server atau client, kita perlu memahami konsep dasar komunikasi jaringan. Pada bagian ini kita membahas apa itu TCP socket, alamat IP, port, dan urutan panggilan system yang dipakai server (socket -> bind -> listen -> accept) serta client (socket -> connect). Tujuan bagian ini adalah membentuk mental model yang benar sebelum menulis kode — tanpa pemahaman konsep, debugging socket menjadi tebakan.

Konsep #

TCP (Transmission Control Protocol) adalah protokol transport yang andal: data dikirim sebagai stream byte yang dijamin sampai dalam urutan yang sama. Socket adalah endpoint komunikasi — file descriptor di sisi process yang merepresentasikan koneksi. Berbeda dari file biasa, socket dua arah: kita bisa read dan write secara bergantian.

Server dan client punya peran berbeda. Server membuka socket, mengikat (bind) ke alamat IP + port tertentu, lalu menunggu koneksi masuk (listen + accept). Client membuka socket dan menghubungi (connect) ke alamat server. Setelah koneksi terbentuk, kedua belah pihak bisa saling kirim data melalui socket masing-masing. Alamat IP mengidentifikasi mesin (host) di jaringan; port (16-bit, 0-65535) mengidentifikasi proses di mesin tersebut.

Empat panggilan system inti server: (1) socket(AF_INET, SOCK_STREAM, 0) membuat socket IPv4 TCP, mengembalikan file descriptor; (2) bind(fd, &addr, sizeof(addr)) mengaitkan socket ke alamat IP dan port; (3) listen(fd, backlog) menandai socket sebagai pasif — siap menerima koneksi; (4) accept(fd, &client_addr, &client_len) memblokir hingga client connect, lalu mengembalikan socket baru untuk komunikasi dengan client tersebut. Client punya dua: socket dan connect.

Mekanisme port: port di bawah 1024 disebut privileged (butuh root) — hindari untuk aplikasi pengembangan. Pilih port antara 1024-65535 yang belum dipakai aplikasi lain. Default kita pakai port 8080 — umum untuk development, jarang konflik.

Implementasi #

Demo minimum yang membuat socket TCP lalu langsung menutupnya — membuktikan system call socket bekerja:

c
#include <stdio.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;
    }
    printf("Socket dibuat, fd = %d\n", fd);

    if (close(fd) == 0) {
        printf("Socket %d ditutup.\n", fd);
    }
    return 0;
}

Output yang Dihasilkan:

text
Socket dibuat, fd = 3
Socket 3 ditutup.

Angka fd = 3 karena file descriptor 0 (stdin), 1 (stdout), 2 (stderr) sudah dipakai process — socket mengambil fd berikutnya yang bebas. Angka ini bisa berbeda di sistem lain bila ada fd yang sudah ditutup sebelumnya.

Struktur alamat IPv4 struct sockaddr_in:

c
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main(void) {
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;            /* IPv4 */
    addr.sin_port = htons(8080);          /* port 8080, network byte order */
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);

    char ip_str[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &addr.sin_addr, ip_str, sizeof(ip_str));
    printf("Alamat: %s:%d\n", ip_str, ntohs(addr.sin_port));
    return 0;
}

Output yang Dihasilkan:

text
Alamat: 127.0.0.1:8080

Pengujian #

Compile dan jalankan kedua demo:

bash
gcc -std=c11 -Wall -Wextra -pedantic -o socket_demo demo_socket.c
./socket_demo
gcc -std=c11 -Wall -Wextra -pedantic -o addr_demo demo_addr.c
./addr_demo

Verifikasi port tidak dipakai aplikasi lain sebelum menjalankan server:

bash
ss -tln 2>/dev/null | grep 8080 || echo "Port 8080 bebas"

Output yang Dihasilkan (bila port bebas):

text
Port 8080 bebas

Bila port sudah dipakai, ganti ke port lain (mis. 8090) atau hentikan aplikasi yang memakai port tersebut.

Mari kita telaah baris-baris kunci pada demo socket:

  1. Baris int fd = socket(AF_INET, SOCK_STREAM, 0);: buat socket IPv4 TCP. AF_INET = Address Family Internet (IPv4); SOCK_STREAM = TCP (reliable, ordered); argumen ketiga 0 = pilih protokol default (IPPROTO_TCP untuk SOCK_STREAM). Return value -1 menandakan error — selalu periksa.
  2. Baris if (fd < 0) { perror("socket"); return 1; }: penjaga error. perror mencetak pesan sistem (mis. “Permission denied” atau “Too many open files”) ke stderr. Return 1 menandakan exit code error ke shell.
  3. Baris close(fd): tutup socket dan bebaskan file descriptor. Tanpa close, fd bocor — setelah ribuan koneksi, process kehabisan fd dan tidak bisa buat socket baru.
  4. Baris addr.sin_family = AF_INET;: set address family IPv4. Tanpa baris ini, bind gagal karena struktur tidak lengkap.
  5. Baris addr.sin_port = htons(8080);: set port. htons (host to network short) mengkonversi dari byte order host (little-endian di x86) ke network byte order (big-endian) — wajib karena sin_port adalah field jaringan.
  6. Baris inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);: konversi string IP ke binary. pton = presentation to network. Lebih aman dari inet_addr yang mengembalikan -1 (sama dengan 255.255.255.255) bila gagal.
  7. Baris inet_ntop(AF_INET, &addr.sin_addr, ip_str, sizeof(ip_str));: konversi binary ke string — kebalikan dari pton. Berguna untuk logging alamat client yang baru connect.

Pada Bagian 2 kita akan menulis server lengkap dengan loop accept yang menangani koneksi client.

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