Pola client-server adalah arsitektur dominan di jaringan modern: satu server listen di port tertentu, banyak klien connect ke port tersebut, dan setiap koneksi menerima pelayanan. Pada bab ini kita membangun echo server yang menerima beberapa pesan sekaligus dari satu klien, lalu membalasnya satu per satu. Server ini menggunakan loop recv hingga klien menutup koneksi, sehingga tidak harus melakukan accept ulang untuk setiap pesan. Kita juga akan melihat bagaimana format pesan berbasis baris (newline-delimited) membuat protokol aplikasi sederhana tetapi robust.
Arsitektur Echo Server #
Echo server adalah aplikasi paling sederhana yang menggambarkan pola client-server: server menerima byte, server mengirim kembali byte yang sama. Meskipun sederhana, echo server mencakup tiga komponen penting:
- Accept loop: server memanggil
acceptuntuk menerima koneksi baru, lalu menyerahkan koneksi tersebut ke handler. - Message loop: handler membaca pesan satu per satu hingga klien menutup koneksi atau mengirim token terminasi.
- Echo response: untuk setiap pesan yang diterima, server mengirimkan balik ke klien.
Berikut implementasi server yang membaca pesan per baris (newline-delimited) dan membalas setiap baris dengan verbatim:
/* File: cs_server.c - Echo server multi-pesan (loop recv sampai klien tutup) */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 9093
#define BUF 512
/* Baca byte demi byte hingga newline */
static int recv_line(int fd, char *buf, size_t cap) {
size_t i = 0;
while (i < cap - 1) {
char c;
ssize_t n = recv(fd, &c, 1, 0);
if (n <= 0) return (int) n; /* 0 = klien tutup, -1 = error */
buf[i++] = c;
if (c == '\n') break;
}
buf[i] = '\0';
return (int) i;
}
int main(void) {
int srv = socket(AF_INET, SOCK_STREAM, 0);
if (srv < 0) { perror("socket"); return 1; }
int opt = 1;
setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (bind(srv, (struct sockaddr *) &addr, sizeof(addr)) != 0) {
perror("bind"); close(srv); return 1;
}
if (listen(srv, 5) != 0) {
perror("listen"); close(srv); return 1;
}
printf("[SERVER] echo server di 127.0.0.1:%d\n", PORT);
fflush(stdout);
int cli = accept(srv, NULL, NULL);
if (cli < 0) { perror("accept"); close(srv); return 1; }
printf("[SERVER] klien terhubung\n");
fflush(stdout);
char buf[BUF];
int rc;
while ((rc = recv_line(cli, buf, sizeof(buf))) > 0) {
printf("[SERVER] echo: %s", buf);
fflush(stdout);
/* Kirim balik ke klien */
send(cli, buf, strlen(buf), 0);
}
printf("[SERVER] klien memutus (%d)\n", rc);
close(cli);
close(srv);
return 0;
}Klien mengirim tiga pesan berurutan dan menunggu echo balik untuk masing-masing:
/* File: cs_client.c - Klien echo, kirim 3 pesan */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 9093
static void jeda_ms(int ms) {
struct timespec ts = { ms / 1000, (ms % 1000) * 1000000L };
nanosleep(&ts, NULL);
}
int main(void) {
jeda_ms(200); /* beri waktu server listen() */
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(PORT);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (connect(fd, (struct sockaddr *) &addr, sizeof(addr)) != 0) {
perror("connect"); close(fd); return 1;
}
printf("[KLIEN] tersambung ke 127.0.0.1:%d\n", PORT);
const char *msgs[] = {
"PING\n",
"echo ini teks\n",
"QUIT\n"
};
char buf[512];
for (int i = 0; i < 3; i++) {
send(fd, msgs[i], strlen(msgs[i]), 0);
printf("[KLIEN] kirim: %s", msgs[i]);
fflush(stdout);
ssize_t n = recv(fd, buf, sizeof(buf) - 1, 0);
if (n <= 0) break;
buf[n] = '\0';
printf("[KLIEN] balasan: %s", buf);
fflush(stdout);
}
close(fd);
printf("[KLIEN] tutup koneksi\n");
return 0;
}Jalankan server dan klien di terminal terpisah atau dengan server di background:
gcc -std=c11 -Wall -Wextra -o cs_server cs_server.c
gcc -std=c11 -Wall -Wextra -o cs_client cs_client.c
./cs_server &
sleep 0.3
./cs_client
waitOutput yang Dihasilkan (Server):
[SERVER] echo server di 127.0.0.1:9093
[SERVER] klien terhubung
[SERVER] echo: PING
[SERVER] echo: echo ini teks
[SERVER] echo: QUIT
[SERVER] klien memutus (0)Output yang Dihasilkan (Klien):
[KLIEN] tersambung ke 127.0.0.1:9093
[KLIEN] kirim: PING
[KLIEN] balasan: PING
[KLIEN] kirim: echo ini teks
[KLIEN] balasan: echo ini teks
[KLIEN] kirim: QUIT
[KLIEN] balasan: QUIT
[KLIEN] tutup koneksiMari kita telaah baris-baris kode di atas:
- Baris 12-22 (
recv_line): Membaca byte demi byte hingga newline. Pendekatan ini memastikan saturecv_lineselalu mengembalikan tepat satu pesan, terlepas dari bagaimana TCP memecah stream menjadi segmen. Tanpa pembacaan byte-per-byte, saturecvbisa mendapat bagian dari dua pesan atau dua pesan sekaligus—kita harus menyimpan sisa byte untuk pesan berikutnya (lebih kompleks). - Baris 28 (
recv_line(cli, buf, sizeof(buf))): Loop terus berjalan selamarc > 0.rc = 0menandakan klien menutup koneksi (FIN received),rc < 0menandakan error. - Baris 32 (
send(cli, buf, strlen(buf), 0)): Mengirim kembali string yang diterima.strlen(buf)sudah termasuk\nkarenarecv_linemenyimpannya di buffer. - Baris 50 (
jeda_ms(200)di klien): Klien menunggu 200 ms sebelum connect untuk memastikan server sudah memanggillisten. Tanpa jeda,connectbisa gagal denganECONNREFUSEDbila server belum siap. Pada production, klien harus retry dengan exponential backoff (lihat bab Timeout dan Retry). - Baris 67-68 (
recv(fd, buf, sizeof(buf) - 1, 0)): Klien membaca balasan dari server. Pada demo ini, server selalu mengirim balik persis pesan yang diterima, jadi saturecvbiasanya cukup. Namun pada umumnya, kita perlu loop recv untuk memastikan seluruh balasan diterima—TCP bisa memecah respons.
Pola Request-Response #
Pada echo server, setiap pesan klien selalu diikuti satu respons server. Pola ini disebut request-response dan merupakan basis dari hampir semua protokol aplikasi: HTTP (request GET, response 200 OK), SMTP (HELO, 250 OK), DNS (query, response). Untuk mengimplementasikan pola ini:
- Klien:
send(request)->recv(response)->send(request)->recv(response)… - Server:
recv(request)-> proses ->send(response)->recv(request)…
Pola ini sinkron dan sederhana, tetapi tidak efisien bila respons lama. Server modern sering menggunakan pipelining (klien mengirim banyak request tanpa menunggu respons) atau asinkron (klien mengirim, lalu melakukan hal lain sambil menunggu respons).
Penanganan Beberapa Klien #
Server di atas hanya melayani satu klien pada satu waktu—accept dipanggil sekali, lalu handler loop recv_line hingga klien keluar. Untuk melayani banyak klien secara berurutan (serial), kita bungkus handler dalam loop:
/* Pola server serial multi-klien (cuplikan kode) */
while (1) {
int cli = accept(srv, NULL, NULL);
if (cli < 0) { perror("accept"); continue; }
/* Tangani klien ini sampai selesai */
char buf[BUF];
int rc;
while ((rc = recv_line(cli, buf, sizeof(buf))) > 0) {
printf("[SERVER] echo: %s", buf);
send(cli, buf, strlen(buf), 0);
}
close(cli);
printf("[SERVER] klien memutus (%d)\n", rc);
}Pola ini disebut iterative server dan cocok bila setiap klien hanya sebentar (mis. query DNS). Bila satu klien bisa memegang koneksi lama (mis. file upload), klien lain harus menunggu—di sinilah concurrency menjadi penting.
Untuk concurrency, ada tiga pilihan utama:
forkper klien: parent loopaccept, setiap klien dilayani child process terpisah. Sederhana, tetapi mahal (process creation).- Thread per klien: mirip
forktetapi dengan thread, lebih ringan. Perlu hati-hati dengan thread safety (lihat bab race-condition). - Event loop (
poll,epoll): satu proses, satu thread, handle ribuan klien dengan I/O multiplexing. Pendekatan ini dipakai nginx, Redis, dan sebagian besar server modern.