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:

AspekTCPUDP
Tipe socketSOCK_STREAMSOCK_DGRAM
KoneksiWajib (connect, accept)Connectionless (sendto, recvfrom)
KeandalanDijamin kernel (retransmission, ACK)Tidak (packet bisa hilang/duplikat)
Urutan pesanTerjamin sesuai pengirimanTidak terjamin
Header overhead20 byte8 byte
Cocok untukHTTP, SSH, file transferDNS, 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.

c
/* 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:

c
/* 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:

bash
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
wait

Output yang Dihasilkan (Server):

text
[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):

text
[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 ditutup

Mari kita telaah baris-baris kode di atas:

  1. Baris socket(AF_INET, SOCK_STREAM, 0): AF_INET memilih IPv4, SOCK_STREAM memilih TCP. Argumen ketiga 0 berarti “pilih protokol default”, yaitu TCP untuk SOCK_STREAM di IPv4.
  2. setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, ...): Opsi SO_REUSEADDR memungkinkan bind ulang ke port yang baru saja dipakai proses keluar dalam keadaan TIME_WAIT. Tanpa opsi ini, restart server cepat sering gagal dengan EADDRINUSE.
  3. bind(srv, ...): Mengaitkan socket ke alamat 127.0.0.1 port 9090. INADDR_LOOPBACK adalah konstanta untuk 127.0.0.1. Untuk menerima koneksi dari interface apa pun, gunakan INADDR_ANY.
  4. listen(srv, 1): Mengubah socket menjadi pasif (listening). Angka 1 adalah backlog—maksimum koneksi yang dapat menunggu di antrian accept sebelum ditolak.
  5. accept(srv, NULL, NULL): Memblokir hingga klien connect. Mengembalikan file descriptor baru (cli) untuk koneksi ini. Socket asli srv tetap mendengarkan koneksi lain.
  6. recv(cli, buf, sizeof(buf) - 1, 0): Membaca byte dari koneksi. Mengembalikan jumlah byte yang dibaca, 0 bila klien menutup koneksi, atau -1 bila error. Loop terus berjalan hingga n <= 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.

c
/* 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:

c
/* 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:

bash
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
wait

Output yang Dihasilkan (Server):

text
[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 3

Output yang Dihasilkan (Klien):

text
[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 3

Kapan 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).