TCP memberi kita keandalan dengan harga: jabat tangan sebelum bicara, penomoran setiap byte, dan mekanisme kirim ulang saat paket hilang. UDP membuang semua itu dan menjadi kurir yang hanya melempar paket — tanpa jabat tangan, tanpa konfirmasi, tanpa jaminan urutan. Untuk DNS query, detak sensor, dan streaming waktu nyata, keringanan justru yang dicari. Pada chapter ini kita memakai module node:dgram untuk mengirim dan menerima datagram UDP, mengamati bagaimana batas pesan UDP dipertahankan (berbeda dari aliran byte TCP), lalu membangun komunikasi balasan sederhana dengan memanfaatkan alamat pengirim yang ikut terkirim.
Socket UDP Pertama #
Berkas TCP punya net.Server dan net.Socket, tetapi UDP hanya mengenal satu jenis object: dgram.Socket hasil dgram.createSocket('udp4'). Object yang sama bisa mengirim dan menerima; tidak ada peran server dan client yang terpisah, hanya socket yang terikat ke alamat dan port lewat bind(). Simpan kode berikut sebagai udp-dasar.mjs:
import dgram from 'node:dgram';
const penerima = dgram.createSocket('udp4');
const pengirim = dgram.createSocket('udp4');
penerima.on('error', (err) => {
console.log('penerima error:', err.code);
});
pengirim.on('error', (err) => {
console.log('pengirim error:', err.code);
});
const tungguListening = (socket) =>
new Promise((resolve) => socket.once('listening', resolve));
const tungguPesan = new Promise((resolve) => {
penerima.on('message', (msg, rinfo) => {
console.log('penerima mendapat pesan:', JSON.stringify(msg.toString('utf8')));
console.log('alamat pengirim:', rinfo.address, `(${rinfo.family})`);
resolve();
});
});
const main = async () => {
penerima.bind(0, '127.0.0.1');
pengirim.bind(0, '127.0.0.1');
await Promise.all([tungguListening(penerima), tungguListening(pengirim)]);
const target = penerima.address();
await new Promise((resolve, reject) => {
pengirim.send('ping lewat UDP', target.port, target.address, (err) => {
if (err) reject(err);
else resolve();
});
});
console.log('pengirim selesai mengirim datagram');
await tungguPesan;
penerima.close();
pengirim.close();
};
main();Output yang Dihasilkan:
pengirim selesai mengirim datagram
penerima mendapat pesan: "ping lewat UDP"
alamat pengirim: 127.0.0.1 (IPv4)Mari kita bedah baris-per-baris:
- Baris 3-4 (
createSocket('udp4')): membuat dua socket UDP versi IPv4 — satu berperan sebagai penerima, satu sebagai pengirim. Argumen'udp4'menunjukkan alamat IPv4; ada juga'udp6'untuk IPv6. Keduanya tidak terikat ke alamat mana pun sebelumbind()dipanggil. - Baris 14 (
tungguListening):bind()bersifat asynchronous, sama sepertilisten()pada TCP. Helper ini menjanjikan event'listening'sehinggamain()bisa memakaiawaitdan urutan log-nya pasti. - Baris 26-27 (
bind(0, '127.0.0.1')): mengikat setiap socket ke loopback dengan port ephemeral. Bahkan socket yang hanya mengirim perlubind()agar punya alamat balasan — di sini portnya kita biarkan dipilih sistem. - Baris 32-33 (
pengirim.send(...)): satu panggilansend()mengirim satu datagram lengkap ke alamat dan port target, dan callback-nya memberi tahu kapan paket sudah diserahkan ke sistem operasi — bukan jaminan sampai tujuan. - Baris 18-22 (event
'message'): padanan event'data'di TCP, dengan dua perbedaan penting. ParametermsgberupaBufferberisi satu datagram utuh, dan parameter keduarinfomemuat alamat serta port pengirim — informasi yang menjadi kunci balasan nanti.
Batas Pesan yang Dipertahankan #
Perbedaan paling mendasar UDP dari TCP terlihat justru dari yang tidak ada di output: tidak ada event 'connection', tidak ada end(), dan tidak ada 'close' yang menunggu lawan. Setiap send() adalah paket mandiri; penerima mendapatkan event 'message' yang persis sepanjang paket tersebut, tidak lebih tidak kurang. Dalam istilah teknis, UDP message-oriented sedangkan TCP stream-oriented.
Konsekuensinya dua arah. Sisi menyenangkan: kita tidak perlu membingkai pesan sendiri — satu send() selalu menjadi satu message, sehingga protokol sederhana bisa langsung jalan. Sisi yang harus diwaspadai: paket bisa hilang, datang dua kali, atau tiba dalam urutan berbeda, dan aplikasi kita tidak akan diberi tahu. Ukuran juga ada batasnya: satu datagram UDP maksimal sekitar 64 KB, dan di jaringan nyata yang aman jauh lebih kecil lagi.
Komunikasi Balasan dengan rinfo #
Dalam praktik, pola paling umum adalah pertukaran singkat: client melempar satu datagram, server membalas ke alamat pengirimnya. Parameter rinfo pada event 'message' memuat semua yang dibutuhkan untuk membalas. Berkas udp-echo.mjs memperlihatkan dua putaran pertukaran tersebut:
import dgram from 'node:dgram';
const server = dgram.createSocket('udp4');
const klien = dgram.createSocket('udp4');
server.on('message', (msg, rinfo) => {
console.log('server menerima datagram:',
JSON.stringify(msg.toString('utf8')));
const balasan = msg.toString('utf8').toUpperCase();
server.send(balasan, rinfo.port, rinfo.address);
});
server.on('error', (err) => {
console.log('server error:', err.code);
});
klien.on('message', (msg) => {
console.log('klien menerima balasan:', JSON.stringify(msg.toString('utf8')));
klien.emit('balasan');
});
klien.on('error', (err) => {
console.log('klien error:', err.code);
});
const tungguBalasan = () =>
new Promise((resolve) => klien.once('balasan', resolve));
const kirim = (teks) =>
new Promise((resolve, reject) => {
const target = server.address();
klien.send(teks, target.port, target.address, (err) => {
if (err) reject(err);
else resolve();
});
});
const main = async () => {
await new Promise((resolve) => server.bind(0, '127.0.0.1', resolve));
await new Promise((resolve) => klien.bind(0, '127.0.0.1', resolve));
await kirim('ping udp');
await tungguBalasan();
await kirim('pesan kedua');
await tungguBalasan();
server.close();
klien.close();
};
main();Output yang Dihasilkan:
server menerima datagram: "ping udp"
klien menerima balasan: "PING UDP"
server menerima datagram: "pesan kedua"
klien menerima balasan: "PESAN KEDUA"Alur kodenya sengaja dibuat berurutan agar output-nya stabil: kirim() menunggu callback send() selesai, lalu tungguBalasan() menahan langkah berikutnya sampai balasan tiba — client tidak mengirim pesan kedua sebelum balasan pertama dicetak. Di sisi server, balasan dikirim ke rinfo.port dan rinfo.address, yakni alamat asal datagram yang masuk; server tidak menyimpan status koneksi apa pun setelah membalas. Jika client mengirim lagi, server memperlakukannya sebagai kejadian baru yang sama sekali tidak berhubungan.
Pola klien.emit('balasan') pada handler 'message' layak mendapat catatan: event 'balasan' adalah event buatan kita sendiri di atas EventEmitter, dipakai sebagai jembatan dari callback jaringan ke promise. Ini pola yang berguna setiap kali kita ingin menunggu kejadian berbasis event di dalam function async.
Kapan Memilih UDP #
Dengan TCP dan UDP keduanya sudah di tangan, keputusan memilih bisa kita rangkum. UDP tepat ketika pesan berukuran kecil, pertukarannya singkat, dan data yang hilang masih berguna — atau justru usang — saat datang terlambat: detak sensor setiap detik, posisi pemain game, query DNS. TCP tepat ketika data harus utuh dan berurutan walau lambat: transfer berkas, percakapan protokol, apa pun yang bertumpu pada request-response.
Satu wilayah abu-abu yang menarik: aplikasi chat dan game yang serius kadang memakai UDP lalu membangun keandalan selektif di atasnya — hanya posisi terbaru yang diirim ulang, riwayat pesan lama biarkan lewat. Itu wilayah rekayasa tingkat lanjut; sebelum sampai ke sana, pastikan kebutuhannya benar-benar tidak tercukupi TCP. Dokumentasi dgram dan catatan tentang ukuran datagram menjadi rujukan lanjutan yang baik.
Sepanjang tiga chapter ini kita selalu menulis alamat dalam bentuk angka 127.0.0.1. Namun aplikasi nyata jarang berbicara begitu — manusia menyebut example.com dan DNS yang menerjemahkannya. Bagaimana Node.js menyelesaikan nama menjadi alamat, dan apa bedanya lookup dari resolve — itu topik Chapter Resolusi DNS.