Server dan client yang telah kita bangun pada bagian 1-5 perlu diverifikasi end-to-end. Pada bagian ini kita menyusun Makefile dengan target all, run-server, run-client, dan clean, lalu menjalankan test integrasi yang memverifikasi protokol lengkap. Tujuan bagian ini adalah memastikan seluruh pipeline bekerja sebagai satu kesatuan dan build dapat direproduksi dengan satu perintah make.
Konsep #
Testing aplikasi jaringan berbeda dari testing library: kita tidak bisa langsung memanggil fungsi karena server dan client adalah process terpisah. Strategi yang kita pakai: (1) jalankan server di background, (2) jalankan client dengan input piped dari echo atau printf, (3) capture output client dan bandingkan dengan ekspektasi, (4) bunuh server dan periksa exit code.
Makefile untuk proyek jaringan punya target khusus: run-server (jalankan server di port default), run-client (jalankan client ke localhost), all (build server + client), clean (hapus binary). Variabel SRC_COMMON memisahkan kode yang dipakai bersama (protocol.c) dari kode spesifik (server.c, client.c).
Test end-to-end yang kita jalankan mencakup: (1) test PING/ECHO/UPPER/QUIT — verifikasi seluruh command dikenal dan diproses benar; (2) test unknown command — verifikasi kode 404; (3) test connect ke port tidak ada — verifikasi error message; (4) test graceful shutdown — verifikasi SIGINT menampilkan statistik.
Implementasi #
Makefile lengkap proyek:
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -pedantic
SRC_COMMON = protocol.c
SRC_SERVER = server.c
SRC_CLIENT = client.c
BIN_SERVER = server
BIN_CLIENT = client
.PHONY: all run-server run-client clean
all: $(BIN_SERVER) $(BIN_CLIENT)
run-server: $(BIN_SERVER)
./$(BIN_SERVER) 8080
run-client: $(BIN_CLIENT)
./$(BIN_CLIENT) 127.0.0.1 8080
$(BIN_SERVER): $(SRC_SERVER) $(SRC_COMMON)
$(CC) $(CFLAGS) -o $@ $^
$(BIN_CLIENT): $(SRC_CLIENT) $(SRC_COMMON)
$(CC) $(CFLAGS) -o $@ $^
clean:
rm -f $(BIN_SERVER) $(BIN_CLIENT) *.o
Script test sederhana test_e2e.sh yang otomatis menjalankan server dan client:
#!/bin/bash
# test_e2e.sh - test end-to-end client-server
set -e
./server 8103 &
SERVER_PID=$!
sleep 0.3
cleanup() {
kill -INT $SERVER_PID 2>/dev/null
sleep 0.3
kill -9 $SERVER_PID 2>/dev/null
}
trap cleanup EXIT
EXPECTED=$(cat <<'EOF'
210 pong
200 hello
200 CONVERT ME
404 unknown command
410 bye
EOF
)
ACTUAL=$(printf "PING\nECHO hello\nUPPER convert me\nFOO bar\nQUIT\n" \
| ./client 127.0.0.1 8103)
if [ "$ACTUAL" = "$EXPECTED" ]; then
echo "[LULUS] Test e2e: semua perintah diproses dengan benar"
exit 0
else
echo "[GAGAL] Output tidak sesuai"
echo "Expected: $EXPECTED"
echo "Actual: $ACTUAL"
exit 1
fiDemo test runner mandiri yang menguji protocol parser secara langsung:
#include <stdio.h>
#include <string.h>
#include "protocol.h"
static int lulus = 0, gagal = 0;
#define PERIKSA(kond, nama) do { \
if (kond) { lulus++; printf("[LULUS] %s\n", nama); } \
else { gagal++; printf("[GAGAL] %s\n", nama); } \
} while (0)
int main(void) {
proto_message msg;
PERIKSA(proto_parse_line("PING", &msg) == 0 &&
msg.command == PROTO_CMD_PING,
"Parse 'PING' -> PROTO_CMD_PING");
PERIKSA(proto_parse_line("ECHO hello world", &msg) == 0 &&
msg.command == PROTO_CMD_ECHO &&
strcmp(msg.args, "hello world") == 0,
"Parse 'ECHO hello world' -> command ECHO, args 'hello world'");
PERIKSA(proto_parse_line("quit", &msg) == 0 &&
msg.command == PROTO_CMD_QUIT,
"Parse 'quit' (lowercase) -> PROTO_CMD_QUIT (case insensitive)");
PERIKSA(proto_parse_line("FOO bar", &msg) == 0 &&
msg.command == PROTO_CMD_UNKNOWN,
"Parse 'FOO bar' -> PROTO_CMD_UNKNOWN");
PERIKSA(proto_parse_line("", &msg) == -1,
"Parse empty line -> error (-1)");
PERIKSA(proto_parse_line(" ", &msg) == -1,
"Parse whitespace-only -> error (-1)");
char buf[256];
int n = proto_build_reply(buf, sizeof(buf), PROTO_REPLY_PONG, "pong");
PERIKSA(n > 0 && strcmp(buf, "210 pong\n") == 0,
"Build reply '210 pong' sukses");
printf("\nRingkasan: %d lulus, %d gagal\n", lulus, gagal);
return gagal == 0 ? 0 : 1;
}Output yang Dihasilkan:
[LULUS] Parse 'PING' -> PROTO_CMD_PING
[LULUS] Parse 'ECHO hello world' -> command ECHO, args 'hello world'
[LULUS] Parse 'quit' (lowercase) -> PROTO_CMD_QUIT (case insensitive)
[LULUS] Parse 'FOO bar' -> PROTO_CMD_UNKNOWN
[LULUS] Parse empty line -> error (-1)
[LULUS] Parse whitespace-only -> error (-1)
[LULUS] Build reply '210 pong' sukses
Ringkasan: 7 lulus, 0 gagalPengujian #
Build dan jalankan test runner protocol:
gcc -std=c11 -Wall -Wextra -pedantic \
-I/home/z/my-project/content/c/proyek-c/proyek-10-client-server-sederhana \
-o /tmp/test_proto /tmp/test_proto.c \
/home/z/my-project/content/c/proyek-c/proyek-10-client-server-sederhana/protocol.c
/tmp/test_protoOutput yang Dihasilkan (sama dengan di atas):
[LULUS] Parse 'PING' -> PROTO_CMD_PING
...
[LULUS] Build reply '210 pong' sukses
Ringkasan: 7 lulus, 0 gagalBuild dan jalankan end-to-end test:
make all
chmod +x test_e2e.sh
./test_e2e.shOutput yang Dihasilkan:
[LULUS] Test e2e: semua perintah diproses dengan benarBuild dengan Makefile — verifikasi reproducibility:
make clean
make all
ls -la server clientOutput yang Dihasilkan:
rm -f server client *.o
gcc -std=c11 -Wall -Wextra -pedantic -o server server.c protocol.c
gcc -std=c11 -Wall -Wextra -pedantic -o client client.c protocol.c
-rwxrwxr-x 1 z z 17960 ... server
-rwxrwxr-x 1 z z 17400 ... clientMari kita telaah baris-baris kunci pada Makefile dan test runner:
- Baris
SRC_COMMON = protocol.c: kode yang dipakai bersama server dan client. Pemisahan ini memudahkan maintenance — bilaprotocol.cberubah, baik server maupun client di-rebuild otomatis. - Baris
$(BIN_SERVER): $(SRC_SERVER) $(SRC_COMMON): dependensi target. Make memeriksa timestamp — bilaprotocol.clebih baru dariserver, target di-rebuild. Tanpa dependensi yang benar, perubahan kode tidak terdeteksi. - Baris
$(CC) $(CFLAGS) -o $@ $^: kompilasi dengan automatic variables.$@= nama target,$^= semua prerequisites. Sintaks ringkas tetapi tidak transparan — pertimbangkan menulis eksplisit untuk pemula. - Baris
.PHONY: all run-server run-client clean: target non-file.run-serverdanrun-clienttidak menghasilkan file —.PHONYmemberitahu make agar selalu jalankan rule, bahkan bila ada file dengan nama tersebut. - Baris
trap cleanup EXIT(di test script): pastikan server di-bunghamati walau test gagal. Tanpa trap, server tetap berjalan di background bila test exit lebih awal — port tetap dipakai. - Baris
kill -INT $SERVER_PID(di cleanup): kirim SIGINT untuk graceful shutdown. Server mencetak statistik lalu exit dengan kode 0. - Baris
kill -9 $SERVER_PID 2>/dev/null(di cleanup): fallback SIGKILL bila SIGINT tidak cukup. SIGKILL tidak bisa ditangkap — process langsung mati tanpa cleanup. - Baris
EXPECTED=$(cat <<'EOF' ... EOF)(di test script): heredoc dengan single-quoted EOF mencegah ekspansi variabel dan command. Output literal dipakai sebagai expected value. - Baris
if [ "$ACTUAL" = "$EXPECTED" ](di test script): perbandingan string dengan spasi di dalam. Selalu gunakan double quote di sekitar variabel — tanpa quote, whitespace menyebabkan syntax error. - Baris
proto_parse_line("quit", &msg) == 0 && msg.command == PROTO_CMD_QUIT(di test runner): verifikasi case-insensitive parsing. Input"quit"(lowercase) harus dikenali sebagaiPROTO_CMD_QUIT, bukanPROTO_CMD_UNKNOWN.
Proyek 10 selesai. Server dan client TCP siap dipakai sebagai fondasi untuk aplikasi jaringan yang lebih kompleks — database sederhana, chat server, atau game multiplayer.
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;
}