Di lapisan Transport, kita dihadapkan pada pilihan dua protokol utama: TCP (Transmission Control Protocol) dan UDP (User Datagram Protocol). Pilihan ini bukan sekadar detail teknis—ia menentukan bagaimana aplikasi kita menangani kehilangan paket, urutan pesan, kecepatan, dan kompleksitas kode. TCP menjanjikan pengiriman yang andal dan berurutan dengan biaya overhead koneksi tiga-arah (three-way handshake), manajemen window, dan acknowledgement. UDP membuang semua itu dan menyodorkan kecepatan tinggi serta kemampuan broadcast, tetapi aplikasi sendirian yang harus menangani kehilangan dan urutan. Bab ini membandingkan keduanya lewat dua pasang program client-server nyata yang bisa Anda kompilasi dan jalankan.
Perbandingan TCP vs UDP #
Sebelum melihat kode, mari kita petakan perbedaan utama antara TCP dan UDP:
| Aspek | TCP | UDP |
|---|---|---|
| Tipe socket | SOCK_STREAM | SOCK_DGRAM |
| Koneksi | Wajib (connect, accept) | Connectionless (sendto, recvfrom) |
| Keandalan | Dijamin kernel (retransmission, ACK) | Tidak (packet bisa hilang/duplikat) |
| Urutan pesan | Terjamin sesuai pengiriman | Tidak terjamin |
| Header overhead | 20 byte | 8 byte |
| Cocok untuk | HTTP, SSH, file transfer | DNS, video streaming, game real-time |
TCP cocok ketika data harus sampai lengkap dan berurutan—transfer file, sesi SSH, halaman web. UDP cocok ketika kecepatan lebih penting daripada kelengkapan—streaming video live, query DNS singkat, atau game real-time di mana paket yang hilang lebih baik dibuang daripada ditunda.
Echo Server TCP #
Server TCP echo menerima koneksi, membaca pesan yang dikirim klien, lalu membalasnya verbatim. Karena TCP adalah stream (bukan pesan terpisah), kita perlu memanggil recv secara berulang hingga klien menutup koneksi.
/* File: tcp_server.c - Server TCP echo di 127.0.0.1:9090 */
#define _POSIX_C_SOURCE 200809L
#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>
#define PORT 9090
#define BUF_SIZE 256
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, 1) != 0) {
perror("listen"); close(srv); return 1;
}
printf("[SERVER TCP] menunggu koneksi 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 TCP] koneksi diterima\n");
fflush(stdout);
char buf[BUF_SIZE];
ssize_t n;
while ((n = recv(cli, buf, sizeof(buf) - 1, 0)) > 0) {
buf[n] = '\0';
printf("[SERVER TCP] menerima (%zd bytes): %s", n, buf);
/* Echo balik ke klien */
send(cli, buf, (size_t)n, 0);
fflush(stdout);
}
printf("[SERVER TCP] klien menutup koneksi (recv=%zd)\n", n);
close(cli);
close(srv);
return 0;
}Klien TCP-nya melakukan connect, lalu kirim tiga pesan dan menunggu echo balik untuk masing-masing:
/* File: tcp_client.c - Klien TCP echo ke 127.0.0.1:9090 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>
#define PORT 9090
#define BUF_SIZE 256
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 TCP] tersambung ke 127.0.0.1:%d\n", PORT);
const char *pesan[] = {
"Halo dari klien!\n",
"Pesan kedua lewat TCP.\n",
"Selesai.\n"
};
char buf[BUF_SIZE];
for (int i = 0; i < 3; i++) {
size_t len = strlen(pesan[i]);
send(fd, pesan[i], len, 0);
ssize_t n = recv(fd, buf, sizeof(buf) - 1, 0);
if (n <= 0) break;
buf[n] = '\0';
printf("[KLIEN TCP] echo balik: %s", buf);
fflush(stdout);
}
close(fd);
printf("[KLIEN TCP] koneksi ditutup\n");
return 0;
}Untuk menjalankan demo, kita perlu dua terminal: satu untuk server, satu untuk klien. Atau lebih mudah, jalankan server di background lalu klien, kemudian tunggu server selesai:
gcc -std=c11 -Wall -Wextra -o tcp_server tcp_server.c
gcc -std=c11 -Wall -Wextra -o tcp_client tcp_client.c
./tcp_server &
sleep 0.3
./tcp_client
waitOutput yang Dihasilkan (Server):
[SERVER TCP] menunggu koneksi di 127.0.0.1:9090...
[SERVER TCP] koneksi diterima
[SERVER TCP] menerima (17 bytes): Halo dari klien!
[SERVER TCP] menerima (23 bytes): Pesan kedua lewat TCP.
[SERVER TCP] menerima (9 bytes): Selesai.
[SERVER TCP] klien menutup koneksi (recv=0)Output yang Dihasilkan (Klien):
[KLIEN TCP] tersambung ke 127.0.0.1:9090
[KLIEN TCP] echo balik: Halo dari klien!
[KLIEN TCP] echo balik: Pesan kedua lewat TCP.
[KLIEN TCP] echo balik: Selesai.
[KLIEN TCP] koneksi ditutupMari kita telaah baris-baris kode di atas:
- Baris
socket(AF_INET, SOCK_STREAM, 0):AF_INETmemilih IPv4,SOCK_STREAMmemilih TCP. Argumen ketiga0berarti “pilih protokol default”, yaitu TCP untukSOCK_STREAMdi IPv4. setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, ...): OpsiSO_REUSEADDRmemungkinkan bind ulang ke port yang baru saja dipakai proses keluar dalam keadaanTIME_WAIT. Tanpa opsi ini, restart server cepat sering gagal denganEADDRINUSE.bind(srv, ...): Mengaitkan socket ke alamat127.0.0.1port9090.INADDR_LOOPBACKadalah konstanta untuk127.0.0.1. Untuk menerima koneksi dari interface apa pun, gunakanINADDR_ANY.listen(srv, 1): Mengubah socket menjadi pasif (listening). Angka1adalah backlog—maksimum koneksi yang dapat menunggu di antrianacceptsebelum ditolak.accept(srv, NULL, NULL): Memblokir hingga klien connect. Mengembalikan file descriptor baru (cli) untuk koneksi ini. Socket aslisrvtetap mendengarkan koneksi lain.recv(cli, buf, sizeof(buf) - 1, 0): Membaca byte dari koneksi. Mengembalikan jumlah byte yang dibaca,0bila klien menutup koneksi, atau-1bila error. Loop terus berjalan hinggan <= 0.
Echo Server UDP #
UDP tidak memiliki konsep “koneksi”. Server tinggal membuat socket, bind ke port, lalu memanggil recvfrom yang akan memblokir hingga datagram tiba. Setiap datagram membawa alamat pengirim, jadi server bisa membalas dengan sendto ke alamat tersebut.
/* File: udp_server.c - Server UDP di 127.0.0.1:9091 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 9091
#define BUF_SIZE 256
int main(void) {
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) { perror("socket"); return 1; }
int opt = 1;
setsockopt(fd, 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(fd, (struct sockaddr *) &addr, sizeof(addr)) != 0) {
perror("bind"); close(fd); return 1;
}
printf("[SERVER UDP] siap menerima datagram di 127.0.0.1:%d\n", PORT);
fflush(stdout);
char buf[BUF_SIZE];
struct sockaddr_in cli;
socklen_t cli_len = sizeof(cli);
for (int i = 0; i < 3; i++) {
ssize_t n = recvfrom(fd, buf, sizeof(buf) - 1, 0,
(struct sockaddr *) &cli, &cli_len);
if (n < 0) { perror("recvfrom"); break; }
buf[n] = '\0';
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof(ip));
printf("[SERVER UDP] dari %s:%d -> %s", ip, ntohs(cli.sin_port), buf);
/* Balas datagram */
sendto(fd, buf, (size_t)n, 0,
(struct sockaddr *) &cli, cli_len);
fflush(stdout);
}
close(fd);
return 0;
}Klien UDP juga tidak memanggil connect. Ia langsung memanggil sendto dengan alamat server:
/* File: udp_client.c - Klien UDP ke 127.0.0.1:9091 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>
#define PORT 9091
#define BUF_SIZE 256
static void jeda_ms(int ms) {
struct timespec ts = { ms / 1000, (ms % 1000) * 1000000L };
nanosleep(&ts, NULL);
}
int main(void) {
jeda_ms(200);
int fd = socket(AF_INET, SOCK_DGRAM, 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);
const char *pesan[] = {
"Datagram 1\n",
"Datagram 2\n",
"Datagram 3\n"
};
char buf[BUF_SIZE];
for (int i = 0; i < 3; i++) {
size_t len = strlen(pesan[i]);
sendto(fd, pesan[i], len, 0,
(struct sockaddr *) &addr, sizeof(addr));
printf("[KLIEN UDP] mengirim (%zu bytes): %s", len, pesan[i]);
struct sockaddr_in from;
socklen_t from_len = sizeof(from);
ssize_t n = recvfrom(fd, buf, sizeof(buf) - 1, 0,
(struct sockaddr *) &from, &from_len);
if (n < 0) { perror("recvfrom"); break; }
buf[n] = '\0';
printf("[KLIEN UDP] balasan : %s", buf);
fflush(stdout);
}
close(fd);
return 0;
}Jalankan kedua program di terminal terpisah atau dengan server di background:
gcc -std=c11 -Wall -Wextra -o udp_server udp_server.c
gcc -std=c11 -Wall -Wextra -o udp_client udp_client.c
./udp_server &
sleep 0.3
./udp_client
waitOutput yang Dihasilkan (Server):
[SERVER UDP] siap menerima datagram di 127.0.0.1:9091
[SERVER UDP] dari 127.0.0.1:32956 -> Datagram 1
[SERVER UDP] dari 127.0.0.1:32956 -> Datagram 2
[SERVER UDP] dari 127.0.0.1:32956 -> Datagram 3Output yang Dihasilkan (Klien):
[KLIEN UDP] mengirim (11 bytes): Datagram 1
[KLIEN UDP] balasan : Datagram 1
[KLIEN UDP] mengirim (11 bytes): Datagram 2
[KLIEN UDP] balasan : Datagram 2
[KLIEN UDP] mengirim (11 bytes): Datagram 3
[KLIEN UDP] balasan : Datagram 3Kapan Memilih TCP, Kapan UDP #
Pilihan antara TCP dan UDP berdampak pada seluruh arsitektur aplikasi. Berikut panduan umum yang biasa dipakai di industri:
- Gunakan TCP bila: data harus sampai lengkap dan berurutan (transfer file, query SQL, request HTTP, sesi shell jarak jauh). TCP juga lebih ramah firewall karena koneksi terlihat sebagai satu aliran dua arah.
- Gunakan UDP bila: latensi rendah lebih penting daripada keandalan (VoIP, video conference, game real-time), atau bila aplikasi sudah memiliki protokol reliabilitas sendiri di atas UDP (QUIC, HTTP/3, custom reliability layer).
- Hindari UDP untuk transaksi seperti query tunggal-respon-tunggal kecuali Anda siap menangani kehilangan paket dengan retry (lihat bab Timeout dan Retry).