Setelah memahami konsep socket pada Bagian 1, kini kita menulis server.c yang mengimplementasikan empat langkah TCP server: socket, bind, listen, dan accept. Server yang kita bangun menerima koneksi satu per satu, memproses setiap perintah yang dikirim client, lalu menutup koneksi dan kembali ke accept. Tujuan bagian ini adalah memahami urutan panggilan system dan bagaimana data mengalir lewat socket.
Konsep #
Server TCP punya dua jenis socket: (1) listen socket — socket pasif yang menerima koneksi masuk, dibuat sekali saat startup; (2) client socket — socket aktif untuk komunikasi dengan satu client, dibuat oleh accept dan ditutup setelah client selesai. Pemisahan ini penting: listen socket tidak dipakai untuk kirim data, hanya menerima koneksi.
Urutan panggilan server: (1) socket(AF_INET, SOCK_STREAM, 0) buat listen socket; (2) setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, ...) aktifkan reuse-address agar restart server cepat; (3) bind(fd, &addr, sizeof(addr)) kaitkan socket ke port; (4) listen(fd, 8) tandai sebagai pasif dengan backlog 8; (5) loop accept(fd, &client_addr, &client_len) yang memblokir hingga client connect, lalu mengembalikan fd baru; (6) handle_client(client_fd) memproses perintah client; (7) close(client_fd) menutup koneksi setelah client keluar; (8) kembali ke langkah 5.
Mekanisme accept: system call memblokir thread server hingga client connect. Saat connect terjadi, kernel membuat socket baru untuk komunikasi dan mengembalikan fd-nya. Listen socket tetap pasif — siap menerima koneksi berikutnya. Parameter client_addr diisi alamat client (IP + port) yang baru connect — berguna untuk logging.
Implementasi #
Inti dari server.c — fungsi main dengan urutan panggilan server:
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; }
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;
}Demo minimum server echo yang menerima satu koneksi lalu menutup:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(void) {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) { perror("socket"); return 1; }
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(8094);
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind"); return 1;
}
if (listen(listen_fd, 1) < 0) {
perror("listen"); return 1;
}
printf("Listen di port 8094, menunggu koneksi...\n");
int client_fd = accept(listen_fd, NULL, NULL);
if (client_fd < 0) { perror("accept"); return 1; }
printf("Client terhubung.\n");
char buf[256];
ssize_t n = read(client_fd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("Diterima: %s", buf);
write(client_fd, buf, (size_t)n); /* echo kembali */
}
close(client_fd);
close(listen_fd);
printf("Server selesai.\n");
return 0;
}Pengujian #
Build server utama proyek dan jalankan:
gcc -std=c11 -Wall -Wextra -pedantic -o server server.c protocol.c
./server 8095 &Server mencetak Server berjalan di 0.0.0.0:8095 (Ctrl+C untuk berhenti) lalu memblokir di accept. Kirim koneksi dari client:
echo "PING" | nc 127.0.0.1 8095Output yang Dihasilkan (di sisi server, setelah koneksi):
Server berjalan di 0.0.0.0:8095 (Ctrl+C untuk berhenti)
[+] koneksi dari 127.0.0.1:40124
[-] koneksi ditutupOutput yang Dihasilkan (di sisi client nc):
210 pongTekan Ctrl+C di terminal server untuk graceful shutdown:
^CShutting down...
Total koneksi: 1, total requests: 1Mari kita telaah baris-baris kunci pada main server:
- Baris
signal(SIGINT, handle_signal);: daftarkan handler untuk SIGINT (Ctrl+C). Handler mengubahserver_running = 0agar loop keluar. Tanpa handler, Ctrl+C langsung membunuh process tanpa mencetak statistik. - Baris
signal(SIGPIPE, SIG_IGN);: abaikan SIGPIPE. Saat client menutup koneksi mendadak,writeke socket akan memicu SIGPIPE yang membunuh server.SIG_IGNmenjadikanwritemengembalikan-1denganerrno = EPIPE— bisa ditangani secara graceful. - Baris
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));: aktifkan reuse-address. Tanpa ini, restart server cepat setelah shutdown gagal denganAddress already in usekarena kernel masih dalam TIME_WAIT state. - Baris
addr.sin_addr.s_addr = htonl(INADDR_ANY);: bind ke semua interface.INADDR_ANY=0.0.0.0— server menerima koneksi di semua interface (loopback, LAN, WAN). Untuk restrict ke localhost saja, pakaiinet_addr("127.0.0.1"). - Baris
if (bind(listen_fd, ..., sizeof(addr)) < 0): kaitkan socket ke alamat. Bila port sudah dipakai aplikasi lain,bindgagal denganEADDRINUSE. Selalu periksa dan tutup socket bila gagal. - Baris
listen(listen_fd, 8): tandai socket sebagai pasif. Argumen8adalah backlog — jumlah maksimum koneksi yang menunggu di kernel queue sebelumaccept. Bila queue penuh, koneksi baru ditolak client. - Baris
int client_fd = accept(listen_fd, ...);: blokir hingga client connect. Mengembalikan fd baru untuk komunikasi dengan client tersebut. Listen socket tetap pasif. - Baris
if (errno == EINTR) continue;: bilaacceptdiinterupsi signal (mis. SIGINT), errno = EINTR — kita continue loop, bukan error. Loop akan cekserver_runningdan keluar bila handler sudah set ke 0. - Baris
inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip));: konversi alamat client dari binary ke string. Berguna untuk logging siapa yang connect. - Baris
handle_client(client_fd);: delegasikan pemrosesan ke fungsi terpisah. Loopmainfokus pada accept loop — separation of concerns.
Pada Bagian 3 kita akan menulis client yang dapat menghubungi server ini dan mengirim perintah interaktif.
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;
}