API socket adalah jembatan antara aplikasi C dan tumpukan jaringan kernel. Tujuh panggilan inti membentuk sebagian besar program jaringan: socket, bind, listen, accept, connect, send, dan recv. Bab ini menelusuri ketujuh panggilan tersebut dalam satu demo terintegrasi yang menjalankan server dan klien dalam satu proses (lewat fork). Kita juga akan menggunakan getaddrinfo sebagai pengganti gethostbyname yang sudah obsolete, agar kode tetap berfungsi untuk IPv4 maupun IPv6 tanpa perubahan.
Tujuh Panggilan Inti Socket #
Tabel berikut merangkum tujuh panggilan yang membentuk dasar pemrograman socket:
| Panggilan | Sisi | Peran |
|---|---|---|
socket() | keduanya | Membuat file descriptor socket baru |
bind() | server | Mengaitkan socket ke alamat (IP:port atau path) |
listen() | server | Menandai socket sebagai pasif (siap menerima koneksi) |
accept() | server | Memblokir hingga klien connect, lalu mengembalikan fd baru |
connect() | klien | Menghubungkan socket ke alamat server |
send() | keduanya | Mengirim data ke socket |
recv() | keduanya | Membaca data dari socket |
Server selalu memanggil empat panggilan pertama secara berurutan: socket -> bind -> listen -> accept. Klien cukup memanggil tiga: socket -> connect -> send/recv. Setelah koneksi terbentuk, baik server maupun klien menggunakan send dan recv secara simetris—dari sudut pandang kernel, koneksi TCP adalah stream dua arah yang setara.
Mengapa getaddrinfo, Bukan gethostbyname #
Fungsi lama gethostbyname hanya mengembalikan IPv4 dan tidak thread-safe. Penggantinya, getaddrinfo, mengembalikan linked-list struct addrinfo yang dapat memuat hasil untuk IPv4 dan IPv6 sekaligus. Fungsi ini juga menerima parameter hints (ai_family, ai_socktype, ai_flags) untuk memfilter hasil sesuai kebutuhan aplikasi.
Karena getaddrinfo mengembalikan linked-list, kode kita tidak perlu mengasumsikan jumlah alamat atau jenisnya. Cukup iterasi dari ai_next hingga NULL dan coba socket() + connect() untuk setiap entri hingga satu berhasil. Pola ini disebut “coba-alamat-satu-per-satu” dan rekomendasi resmi dari Beej’s Guide to Network Programming.
Demo Lengkap: Server dan Klien dalam Satu Proses #
Berikut program yang memanggil ketujuh API socket dalam satu alur. Proses parent berperan sebagai server, child berperan sebagai klien, sehingga kita bisa melihat kedua sisi dalam satu eksekusi.
/* File: sockprog.c - Demo lengkap 7 socket API calls */
#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 <sys/types.h>
#include <netdb.h>
#include <time.h>
#define PORT "9092"
static void jeda_ms(int ms) {
struct timespec ts = { ms / 1000, (ms % 1000) * 1000000L };
nanosleep(&ts, NULL);
}
static void jalankan_server(void) {
struct addrinfo hints = {0}, *res;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; /* Bind ke semua interface */
int rc = getaddrinfo(NULL, PORT, &hints, &res);
if (rc != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rc));
fflush(stderr);
_exit(1);
}
/* 1) socket() */
int srv = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (srv < 0) { perror("socket"); _exit(1); }
int opt = 1;
setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
/* 2) bind() */
if (bind(srv, res->ai_addr, res->ai_addrlen) != 0) {
perror("bind"); _exit(1);
}
freeaddrinfo(res);
/* 3) listen() */
if (listen(srv, 5) != 0) { perror("listen"); _exit(1); }
printf("[SERVER] socket/bind/listen OK di port %s\n", PORT);
fflush(stdout);
/* 4) accept() */
int cli = accept(srv, NULL, NULL);
if (cli < 0) { perror("accept"); _exit(1); }
printf("[SERVER] koneksi diterima, fd klien=%d\n", cli);
fflush(stdout);
/* 6) recv() */
char buf[256];
ssize_t n = recv(cli, buf, sizeof(buf) - 1, 0);
if (n > 0) {
buf[n] = '\0';
printf("[SERVER] recv %zd bytes: %s", n, buf);
fflush(stdout);
/* 7) send() - echo balik */
send(cli, buf, (size_t)n, 0);
}
close(cli);
close(srv);
}
static void jalankan_klien(void) {
jeda_ms(200);
struct addrinfo hints = {0}, *res;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
int rc = getaddrinfo("127.0.0.1", PORT, &hints, &res);
if (rc != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rc));
fflush(stderr);
_exit(1);
}
/* 1) socket() di sisi klien */
int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fd < 0) { perror("socket"); _exit(1); }
/* 5) connect() */
if (connect(fd, res->ai_addr, res->ai_addrlen) != 0) {
perror("connect"); _exit(1);
}
freeaddrinfo(res);
printf("[KLIEN] connect sukses\n");
fflush(stdout);
/* 7) send() dari sisi klien */
const char *msg = "Halo, ini pesan lewat socket!\n";
ssize_t sent = send(fd, msg, strlen(msg), 0);
printf("[KLIEN] send %zd bytes\n", sent);
fflush(stdout);
/* 6) recv() di sisi klien */
char buf[256];
ssize_t n = recv(fd, buf, sizeof(buf) - 1, 0);
if (n > 0) {
buf[n] = '\0';
printf("[KLIEN] recv %zd bytes: %s", n, buf);
fflush(stdout);
}
close(fd);
}
int main(void) {
fflush(stdout);
pid_t pid = fork();
if (pid == 0) { jalankan_klien(); fflush(stdout); _exit(0); }
jalankan_server();
fflush(stdout);
waitpid(pid, NULL, 0);
return 0;
}Compile dan jalankan:
gcc -std=c11 -Wall -Wextra -o sockprog sockprog.c
timeout 5 ./sockprogOutput yang Dihasilkan:
[SERVER] socket/bind/listen OK di port 9092
[KLIEN] connect sukses
[SERVER] koneksi diterima, fd klien=4
[KLIEN] send 30 bytes
[SERVER] recv 30 bytes: Halo, ini pesan lewat socket!
[KLIEN] recv 30 bytes: Halo, ini pesan lewat socket!Mari kita telaah baris-baris kode di atas:
- Baris 17-22 (
getaddrinfo(NULL, PORT, &hints, &res)di server): Argumen pertamaNULLdenganai_flags = AI_PASSIVEmemberitahugetaddrinfountuk mengembalikan alamat wildcard0.0.0.0(semua interface) yang cocok untukbind. Bila kita ingin bind ke alamat spesifik, gantiNULLdengan string alamat (mis."127.0.0.1"). - Baris 24 (
socket(res->ai_family, res->ai_socktype, res->ai_protocol)): Membuat socket dengan parameter yang sesuai hasilgetaddrinfo. Pendekatan ini membuat kode otomatis berfungsi untuk IPv4 maupun IPv6—tidak ada hardcodingAF_INET. - Baris 31 (
bind(srv, res->ai_addr, res->ai_addrlen)): Mengaitkan socket ke alamat.res->ai_addradalahstruct sockaddr *generik yang sudah diisi dengan IPv4 atau IPv6 olehgetaddrinfo. Kita tidak perlu tahu jenis alamat yang dikembalikan—cukup pass-through. - Baris 33 (
freeaddrinfo(res)): Setelahbindberhasil, linked-listrestidak lagi diperlukan dan wajib dibebaskan. Panggilan ini wajib—tanpafreeaddrinfo, terjadi memory leak setiap kaligetaddrinfodipanggil. - Baris 38 (
accept(srv, NULL, NULL)): Memblokir hingga klien connect. Argumen kedua dan ketigaNULLberarti kita tidak peduli alamat klien—bila kita ingin mencatat IP klien (mis. untuk logging), isi denganstruct sockaddr_storagedan pointer panjang. - Baris 80-81 (
getaddrinfo("127.0.0.1", PORT, ...)di klien): TanpaAI_PASSIVE,getaddrinfomengembalikan alamat tujuan untukconnect. Klien menentukan hostname tujuan sebagai string ("127.0.0.1"atau"example.com"). - Baris 92 (
send(fd, msg, strlen(msg), 0)): Mengirim data ke socket yang sudah connect. Argumen keempat adalah flags—0berarti default.MSG_NOSIGNALberguna untuk mencegah SIGPIPE bila server menutup koneksi mendadak (lihat bab Network Error Handling). - Baris 97 (
recv(fd, buf, sizeof(buf) - 1, 0)): Membaca data dari socket. Selalu sisakan satu byte untuk null-terminator (sizeof(buf) - 1) karenarecvtidak menambah\0secara otomatis.
Membaca dan Menulis dengan Benar #
Banyak pemula mengasumsikan satu send di sisi klien selalu dipetakan ke satu recv di sisi server. Ini salah untuk TCP. TCP adalah stream, bukan sequence of messages. Tiga panggilan send mungkin diterima sebagai satu recv (jika datanya kecil), atau satu send besar sebagai beberapa recv (jika data melebihi buffer kernel).
Untuk mengatasi hal ini, ada dua pendekatan umum:
- Delimiter-based: akhiri setiap pesan dengan karakter khusus (mis.
\n). Server membaca byte demi byte hingga menemukan delimiter. Pendekatan ini dipakai HTTP, SMTP, IRC. - Length-prefix: sertakan panjang pesan sebagai header biner di awal setiap pesan. Server membaca 4 byte pertama sebagai panjang, lalu membaca sisa pesan sesuai panjang tersebut. Pendekatan ini dipakai di banyak protokol binary modern seperti gRPC, MQTT, AMQP.
Bab Parsing Protokol akan membahas implementasi kedua pendekatan. Untuk sekarang, perhatikan bahwa loop recv hingga seluruh pesan terbaca adalah praktik wajib.