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.
| Aspek | TCP (Chapter sebelumnya) | UDP (chapter ini) |
|---|---|---|
| Fungsi kunci | send()/recv() pada socket koneksi hasil accept()/connect() | sendto()/recvfrom() dengan alamat per datagram |
| Batas pesan | tidak ada — aliran byte, butuh framing | terjaga — satu kirim, satu terima |
| Kehilangan data | diserap oleh retransmisi | mungkin terjadi, ditangani aplikasi |
| Urutan | dijamin | tidak dijamin |
| Biaya sebelum data | handshake TCP | nol |
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.
// 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:
[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 ditutupMari kita bedah baris per baris:
- Baris 4-14 (header): empat header POSIX yang sama dengan chapter TCP, ditambah
<vector>untuk catatan datagram dan<thread>untuk dua thread kita. - Baris 18 (
::socket(AF_INET, SOCK_DGRAM, 0)): meminta socket UDP; perhatikan tidak adalisten()maupunaccept()yang menyusul — penerima UDP langsung siap setelahbind(). - Baris 24-28 (pengisian
sockaddr_in):htons(0)meminta port ephemeral danhtonl(INADDR_LOOPBACK)mengikat penerima ke127.0.0.1, sama seperti pola chapter sebelumnya. - Baris 34-42 (
getsockname): port yang dipilih kernel dibaca, dikonversi denganntohs(), dan disimpan untuk pengirim — nomornya sengaja tidak dicetak agar keluaran tetap deterministik. - Baris 45-48 (thread penerima): lambda menangkap referensi descriptor dan
catatan, lalu berputar tiga kali untuk tiga datagram yang dijanjikan pengirim. - Baris 53-58 (
recvfromdan pencatatan):recvfrom()menunggu satu datagram, mengisi alamat pengirim kedari, dan payload-nya disalin menjadistd::string(buf, n)lalu masuk kecatatan. - Baris 61-64 (ACK lewat alamat hasil
recvfrom): balasanACK 1: suhu=31.5dikirim dengansendto()ke alamatdari— bukti bahwa membalas UDP tidak butuh koneksi tersimpan. - Baris 71 (
fdPengirimtanpabind): socket pengirim tidak diikat alamat; kernel memilih port sumber otomatis per pengiriman, kebalikan dari penerima yang wajibbind(). - Baris 83-90 (
sendtoper datagram): setiap string telemetri dikirim sebagai satu datagram utuh ke alamat tujuan pada parameter kelima; batas pesan terjaga tanpa framing. - Baris 92-99 (menerima ACK):
recvfrom()pada socket pengirim memakainullptruntuk argumen alamat — kita sudah tahu siapa lawannya, jadi identitas balasan tidak perlu dicatat; polanya tetap saturecvfrom()per datagram yang dikirim. - Baris 101-104 (close dan join): socket pengirim ditutup di thread utama, thread penerima menutup socketnya sendiri di baris 67, dan
join()menjamincatatansudah final. - Baris 106-110 (ringkasan setelah join): membaca
catatantanpa mutex aman karenajoin()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 .