Chapter sebelumnya menuntaskan TCP: koneksi dibuka lewat handshake, data dijamin utuh dan berurutan, lalu koneksi ditutup. UDP memilih jalan yang berlawanan — tanpa koneksi, tanpa jaminan, tetapi ringan dan seketika. Satu pesan UDP disebut datagram: paket kecil yang berdiri sendiri dan membawa alamat tujuannya masing-masing. Di chapter ini kita menulis pengirim dan penerima UDP dalam satu proses — pola yang sama dengan chapter lalu, dua thread di atas loopback dengan port ephemeral — lalu membandingkan langsung dengan versi TCP-nya.

SOCK_DGRAM: Socket Tanpa Koneksi #

Perbedaan pertama ada pada tipe socket: socket(AF_INET, SOCK_DGRAM, 0) alih-alih SOCK_STREAM. Konsekuensinya menyusuri seluruh alur. Tidak ada listen() dan accept() di sisi penerima — ia cukup bind() ke alamat dan port, lalu langsung menunggu datagram. Tidak ada connect() di sisi pengirim — setiap pemanggilan sendto() menyertakan alamat tujuan, sehingga satu socket pengirim bisa mengirim ke banyak tujuan berbeda tanpa mengganti apa pun.

“Tanpa koneksi” tidak berarti tanpa alamat. Setiap datagram memuat pasangan IP:port sumber dan tujuan lengkap; yang hilang hanyalah state koneksi seperti nomor urut dan status handshake. Itulah sebabnya penerima UDP tidak pernah “menerima koneksi” — ia menerima datagram satu per satu, dan tiap datagram membawa identitas pengirimnya sendiri.

Sifat Datagram: Pesan Terpisah yang Bisa Hilang #

Dua sifat datagram perlu kita pisahkan dengan jujur. Sifat yang dijamin protokol: batas pesan. Satu sendto() menghasilkan tepat satu recvfrom() — datagram tidak digabung dan tidak dipecah seperti aliran byte TCP. Bila buffer penerima lebih kecil dari datagram, sisa byte terbuang; tidak ada sisa yang menunggu di pemanggilan berikutnya.

Sifat yang tidak dijamin: ketibaan. Pada jaringan sungguhan, datagram bisa hilang di tengah jalan, bisa tiba dua kali, dan bisa tiba dengan urutan yang berbeda dari urutan pengiriman. Namun contoh kita berjalan di loopback (127.0.0.1): data tidak benar-benar melewati kartu jaringan, hanya berpindah antar buffer kernel, sehingga datagram nyaris pasti tiba utuh, berurutan, dan tanpa duplikasi. Program di bawah memang mengandalkan sifat pragmatis itu agar keluarannya deterministik; pada jaringan nyata, kode UDP harus siap menerima pesan yang tidak datang — biasanya lewat timeout.

sendto dan recvfrom #

Duo fungsi UDP punya bentuk yang lebih panjang daripada send()/recv() karena alamat ikut dikirim-diterima. sendto(fd, buf, panjang, 0, &tujuan, panjangTujuan) mengirim satu datagram ke alamat pada parameter kelima. recvfrom(fd, buf, ukuran, 0, &sumber, &panjangSumber) menunggu satu datagram sekaligus mengisi alamat pengirimnya — dan alamat itulah yang kita pakai untuk membalas, karena UDP tidak menyimpan “siapa yang terhubung”. Pola penerima UDP khasnya berbunyi: terima lewat recvfrom, olah, balas ke alamat hasil recvfrom yang sama.

Kapan UDP Masuk Akal #

UDP menang di tiga situasi. Pertama, telemetri dan pemantauan: laporan suhu=31.5 lima detik berikutnya lebih bermakna daripada pengiriman ulang laporan yang terlambat — satu datagram hilang tidak merusak apa pun. Kedua, game dan media waktu nyata: posisi karakter terbaru menggantikan posisi lama, jadi menunggu retransmisi justru memperburuk pengalaman. Ketiga, DNS (port 53): kuerinya kecil, polanya satu permintaan-satu balasan, dan tanpa handshake respons terasa seketika — persis analisis peta protokol pada Chapter konsep.

AspekTCP (Chapter sebelumnya)UDP (chapter ini)
Fungsi kuncisend()/recv() pada socket koneksi hasil accept()/connect()sendto()/recvfrom() dengan alamat per datagram
Batas pesantidak ada — aliran byte, butuh framingterjaga — satu kirim, satu terima
Kehilangan datadiserap oleh retransmisimungkin terjadi, ditangani aplikasi
Urutandijamintidak dijamin
Biaya sebelum datahandshake TCPnol

Program Utama: Pengirim dan Penerima Datagram #

Program berikut mengirim tiga datagram telemetri dari thread utama (peran pengirim) ke thread penerima yang menunggu di port ephemeral loopback. Penerima mencatat setiap datagram ke std::vector dan membalas ACK; pengirim mencetak balasan itu satu per satu. Setelah join(), thread utama membaca catatan penerima dan mencetak ringkasannya.

cpp
// udp-datagram.cpp — pengirim dan penerima UDP dalam satu proses: thread
// penerima menunggu datagram di port ephemeral loopback, mencatat isinya,
// lalu membalas ACK; main berperan sebagai pengirim dan mencetak balasan.
#include <arpa/inet.h>   // htons, htonl
#include <netinet/in.h>  // struct sockaddr_in, INADDR_LOOPBACK
#include <sys/socket.h>  // socket, bind, sendto, recvfrom, getsockname
#include <unistd.h>      // close
#include <cerrno>        // errno
#include <cstdint>       // uint16_t
#include <cstring>       // strerror
#include <iostream>      // std::cout, std::cerr
#include <string>
#include <thread>        // std::thread, join
#include <vector>

int main() {
    // 1) Socket penerima: SOCK_DGRAM lalu bind ke loopback, port 0.
    int fdPenerima = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (fdPenerima < 0) {
        std::cerr << "socket() gagal: " << std::strerror(errno) << "\n";
        return 1;
    }

    sockaddr_in alamat{};
    alamat.sin_family = AF_INET;
    alamat.sin_port = htons(0);                       // 0 = OS memilih port bebas
    alamat.sin_addr.s_addr = htonl(INADDR_LOOPBACK);  // 127.0.0.1
    if (::bind(fdPenerima, reinterpret_cast<const sockaddr*>(&alamat), sizeof(alamat)) < 0) {
        std::cerr << "bind() gagal: " << std::strerror(errno) << "\n";
        ::close(fdPenerima);
        return 1;
    }

    sockaddr_in terikat{};
    socklen_t panjangTerikat = sizeof(terikat);
    if (::getsockname(fdPenerima, reinterpret_cast<sockaddr*>(&terikat), &panjangTerikat) < 0) {
        std::cerr << "getsockname() gagal: " << std::strerror(errno) << "\n";
        ::close(fdPenerima);
        return 1;
    }
    const int port = ntohs(terikat.sin_port);  // dipakai pengirim, tidak dicetak
    std::cout << "[penerima] socket UDP siap di 127.0.0.1 (port ephemeral dari getsockname)\n";

    // Catatan datagram ditulis thread penerima dan dibaca main() setelah join.
    std::vector<std::string> catatan;

    // 2) Thread penerima: terima 3 datagram, catat, balas ACK ke pengirim.
    std::thread threadPenerima([&fdPenerima, &catatan]() {
        for (int i = 1; i <= 3; ++i) {
            char buf[512];
            sockaddr_in dari{};
            socklen_t panjangDari = sizeof(dari);
            ssize_t n = ::recvfrom(fdPenerima, buf, sizeof(buf), 0,
                                   reinterpret_cast<sockaddr*>(&dari), &panjangDari);
            if (n < 0) {
                break;  // kasus galat tidak terjadi pada contoh loopback ini
            }
            catatan.push_back(std::string(buf, static_cast<std::size_t>(n)));

            // recvfrom mengisi alamat `dari`, jadi balasan cukup dikirim ke sana.
            std::string balasan = "ACK " + std::to_string(i) + ": " + catatan.back();
            if (::sendto(fdPenerima, balasan.data(), balasan.size(), 0,
                         reinterpret_cast<const sockaddr*>(&dari), panjangDari) < 0) {
                break;
            }
        }
        ::close(fdPenerima);
    });

    // 3) Main sebagai pengirim: socket tanpa bind, langsung sendto per datagram.
    int fdPengirim = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (fdPengirim < 0) {
        std::cerr << "socket() pengirim gagal: " << std::strerror(errno) << "\n";
        threadPenerima.join();
        return 1;
    }

    sockaddr_in tujuan{};
    tujuan.sin_family = AF_INET;
    tujuan.sin_port = htons(static_cast<uint16_t>(port));
    tujuan.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

    for (const char* pesan : {"suhu=31.5", "kelembapan=78", "tekanan=1012"}) {
        std::string datagram = pesan;
        if (::sendto(fdPengirim, datagram.data(), datagram.size(), 0,
                     reinterpret_cast<const sockaddr*>(&tujuan), sizeof(tujuan)) < 0) {
            std::cerr << "sendto() gagal: " << std::strerror(errno) << "\n";
            break;
        }
        std::cout << "[pengirim] kirim datagram  : " << pesan << "\n";

        char bufAck[512];
        ssize_t m = ::recvfrom(fdPengirim, bufAck, sizeof(bufAck), 0, nullptr, nullptr);
        if (m < 0) {
            std::cerr << "recvfrom() ACK gagal: " << std::strerror(errno) << "\n";
            break;
        }
        std::cout << "[pengirim] balasan penerima: "
                  << std::string(bufAck, static_cast<std::size_t>(m)) << "\n";
    }
    ::close(fdPengirim);

    // 4) Join thread penerima, baru baca catatannya (aman setelah join).
    threadPenerima.join();

    std::cout << "[ringkasan] penerima mencatat " << catatan.size() << " datagram\n";
    for (std::size_t i = 0; i < catatan.size(); ++i) {
        std::cout << "[ringkasan] datagram ke-" << (i + 1) << ": " << catatan[i] << "\n";
    }
    std::cout << "[ringkasan] thread penerima telah di-join dan kedua socket ditutup\n";
    return 0;
}

Output yang Dihasilkan:

text
[penerima] socket UDP siap di 127.0.0.1 (port ephemeral dari getsockname)
[pengirim] kirim datagram  : suhu=31.5
[pengirim] balasan penerima: ACK 1: suhu=31.5
[pengirim] kirim datagram  : kelembapan=78
[pengirim] balasan penerima: ACK 2: kelembapan=78
[pengirim] kirim datagram  : tekanan=1012
[pengirim] balasan penerima: ACK 3: tekanan=1012
[ringkasan] penerima mencatat 3 datagram
[ringkasan] datagram ke-1: suhu=31.5
[ringkasan] datagram ke-2: kelembapan=78
[ringkasan] datagram ke-3: tekanan=1012
[ringkasan] thread penerima telah di-join dan kedua socket ditutup

Mari kita bedah baris per baris:

  1. Baris 4-14 (header): empat header POSIX yang sama dengan chapter TCP, ditambah <vector> untuk catatan datagram dan <thread> untuk dua thread kita.
  2. Baris 18 (::socket(AF_INET, SOCK_DGRAM, 0)): meminta socket UDP; perhatikan tidak ada listen() maupun accept() yang menyusul — penerima UDP langsung siap setelah bind().
  3. Baris 24-28 (pengisian sockaddr_in): htons(0) meminta port ephemeral dan htonl(INADDR_LOOPBACK) mengikat penerima ke 127.0.0.1, sama seperti pola chapter sebelumnya.
  4. Baris 34-42 (getsockname): port yang dipilih kernel dibaca, dikonversi dengan ntohs(), dan disimpan untuk pengirim — nomornya sengaja tidak dicetak agar keluaran tetap deterministik.
  5. Baris 45-48 (thread penerima): lambda menangkap referensi descriptor dan catatan, lalu berputar tiga kali untuk tiga datagram yang dijanjikan pengirim.
  6. Baris 53-58 (recvfrom dan pencatatan): recvfrom() menunggu satu datagram, mengisi alamat pengirim ke dari, dan payload-nya disalin menjadi std::string(buf, n) lalu masuk ke catatan.
  7. Baris 61-64 (ACK lewat alamat hasil recvfrom): balasan ACK 1: suhu=31.5 dikirim dengan sendto() ke alamat dari — bukti bahwa membalas UDP tidak butuh koneksi tersimpan.
  8. Baris 71 (fdPengirim tanpa bind): socket pengirim tidak diikat alamat; kernel memilih port sumber otomatis per pengiriman, kebalikan dari penerima yang wajib bind().
  9. Baris 83-90 (sendto per datagram): setiap string telemetri dikirim sebagai satu datagram utuh ke alamat tujuan pada parameter kelima; batas pesan terjaga tanpa framing.
  10. Baris 92-99 (menerima ACK): recvfrom() pada socket pengirim memakai nullptr untuk argumen alamat — kita sudah tahu siapa lawannya, jadi identitas balasan tidak perlu dicatat; polanya tetap satu recvfrom() per datagram yang dikirim.
  11. Baris 101-104 (close dan join): socket pengirim ditutup di thread utama, thread penerima menutup socketnya sendiri di baris 67, dan join() menjamin catatan sudah final.
  12. Baris 106-110 (ringkasan setelah join): membaca catatan tanpa mutex aman karena join() adalah titik sinkronisasi; isi dan urutannya di loopback sesuai urutan pengiriman.

Perbandingan di atas menutup pasangan transport kita: TCP untuk aliran yang harus utuh, UDP untuk pesan singkat yang lebih baik hilang daripada terlambat. Persoalan berikutnya berlaku untuk keduanya — bagaimana memformat isi pesan agar kedua sisi sepakat maknanya. Chapter Desain Protokol dan Framing menjawabnya dengan length-prefix dan delimiter.

Rujukan resmi: udp(7) — Linux man-pages , sendto(2) — Linux man-pages , recvfrom(2) — Linux man-pages .