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:
$ ./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/writeuntuk socket (mirip dengan file descriptor), - Pointer dan Array — buffer
char[]untuk data jaringan, - Fungsi dan Header Files — modularisasi
protocol.h/.cdanserver.c/client.c, - Struktur Program C dan Fungsi main —
argc/argvuntuk 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:
- Bagian 1: Konsep Client-Server — pemahaman TCP socket, alamat IP, port, dan urutan
socket->bind->listen->accept. - Bagian 2: Implementasi Server —
server.cdengan loopaccept+ handler per client. - Bagian 3: Implementasi Client —
client.cyang membaca stdin dan mengirim ke server. - Bagian 4: Protokol Komunikasi —
protocol.h/.cdengan parser baris dan builder balasan. - Bagian 5: Error Handling dan Timeout —
setsockoptuntukSO_RCVTIMEO,SO_REUSEADDR, dan penangananSIGINT. - 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.
Pahami arsitektur client-server dan protokol yang akan dipakai.
Buka → # Bagian 2: Implementasi ServerBuat server socket TCP dengan bind, listen, accept.
Buka → # Bagian 3: Implementasi ClientBuat client yang connect ke server dan kirim request.
Buka → # Bagian 4: Protokol KomunikasiRancang protokol request-response teks line-based.
Buka → # Bagian 5: Error Handling dan TimeoutTangani error jaringan dan set socket timeout.
Buka → # Bagian 6: Testing dan BuildTest dengan loopback dan build dengan Makefile.
Buka → # Buka →Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
client.c
Unduh berkas/* 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/* 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/* 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;
}