Server yang kita bangun pada chapter sebelumnya akan kesepian tanpa client yang menghubunginya. Pada chapter ini kita pindah kursi ke sisi pemohon koneksi: memakai net.connect()
untuk menyambung, mengikuti event 'connect'
, 'data', dan 'close', serta menghadapi kenyataan bahwa koneksi bisa gagal. Kita juga akan membedah dua cara menutup socket — end()
yang sopan dan destroy()
yang tegas — lalu menutup dengan pola penyambungan ulang yang dipakai hampir semua client produksi. Tiga berkas contoh akan kita uji, semuanya membawa server kecilnya sendiri agar bisa dijalankan langsung.
Koneksi Pertama dan Event-nya #
Membuka koneksi TCP dari Node.js seumur hidup hanya butuh satu panggilan: net.connect({ port, host }), yang merupakan nama pendek dari net.createConnection(). Fungsi ini langsung mengembalikan object net.Socket — belum terhubung, tapi sudah bisa kita lengkapi handler. Simpan kode berikut sebagai klien-tcp.mjs:
import net from 'node:net';
const server = net.createServer((socket) => {
socket.on('error', () => {});
socket.write('Selamat datang! Siapa nama Anda? ');
socket.on('data', (chunk) => {
const nama = chunk.toString('utf8').trim();
socket.end(`Sampai jumpa, ${nama}!`);
});
});
server.on('error', () => {});
server.listen(0, '127.0.0.1', () => {
const port = server.address().port;
const client = net.connect({ port, host: '127.0.0.1' });
client.setEncoding('utf8');
client.on('connect', () => {
console.log('terhubung ke server');
});
let nomorPesan = 0;
client.on('data', (data) => {
nomorPesan += 1;
if (nomorPesan === 1) {
console.log('pesan dari server:', JSON.stringify(data));
client.write('Budi');
client.end();
} else {
console.log('pesan setelah client.end():', JSON.stringify(data));
}
});
client.on('error', (err) => {
console.log('client error:', err.code);
});
client.on('close', () => {
server.close(() => {
console.log('koneksi dan server tertutup');
process.exit(0);
});
});
});Output yang Dihasilkan:
terhubung ke server
pesan dari server: "Selamat datang! Siapa nama Anda? "
pesan setelah client.end(): "Sampai jumpa, Budi!"
koneksi dan server tertutupBagian paling menarik dari output ini adalah baris terakhir: client menerima pesan Sampai jumpa, Budi! setelah memanggil client.end(). Mari kita bedah mengapa hal itu bisa terjadi:
- Baris 3-10 (server kecil): server mengirim sambutan begitu koneksi terbuka, lalu membalas jawaban client dengan
socket.end()— data masih ikut terkirim sebelum koneksi benar-benar ditutup. - Baris 17 (
net.connect({ port, host })): memulai koneksi secara asynchronous. Object socket langsung dikembalikan meskipun jabat tangan TCP baru akan selesai nanti; itulah sebabnya kita tidak menulis apa pun di baris ini, hanya memasang handler. - Baris 18 (
client.setEncoding('utf8')): membuat event'data'menyerahkan string alih-alihBuffer. Klien yang fokus pada teks memang lebih nyaman begini, dan decoding tetap aman untuk pesan ASCII sederhana seperti ini. - Baris 20-22 (
'connect'): terbit sekali saat koneksi berhasil — titik paling awal yang aman untuk mulai mengirim data. - Baris 24-34 (penghitung pesan): pesan pertama dari server memicu balasan
Budisekaligusclient.end(). Panggilanend()hanya menutup arah kirim — socket menjadi half-open: kita berhenti mengirim, tetapi arah terima masih hidup. - Baris 31 (
else): pesan kedua dari server tetap diterima lewat event'data', terbukti oleh baris terakhir output. Baru setelah server juga menutup sisinya, event'close'terbit dan kita membubarkan server uji.
end() versus destroy() #
Dua method ini sama-sama mengakhiri socket, tetapi dengan sopan santun yang berbeda. end() adalah cara sopan: semua data yang sudah diantre akan diselesaikan, segmen FIN dikirim agar lawan tahu kita selesai, dan data yang masih berjalan masuk tetap bisa diterima — persis seperti yang didemonstrasikan klien-tcp.mjs. Sebaliknya destroy() bersifat tegas: socket langsung dibuang, data yang belum terkirim dibatalkan, dan tidak ada pemberitahuan ke lawan; lawan akan menyadari kehilangan koneksi lewat kegagalan baca berikutnya.
| Aspek | socket.end() | socket.destroy() |
|---|---|---|
| Data antrean | diselesaikan lebih dulu | dibatalkan |
| Notifikasi ke lawan | ada (FIN) | tidak ada |
| Masih bisa menerima | ya, sampai lawan menutup | tidak |
| Kapan dipakai | percakapan normal selesai | error, data tidak tepercaya, perlu berhenti segera |
Kombinasi yang sering dipakai di dunia nyata adalah end() di jalur normal dan destroy() di jalur error — misalnya di dalam handler 'error' atau saat validasi protokol menemukan data yang mencurigakan.
Ketika Koneksi Gagal #
Semua contoh sejauh ini beranggapan server selalu tersedia. Kenyataannya, client adalah pihak yang paling sering menghadapi kegagalan: server belum menyala, sedang restart, atau alamatnya salah. Kegagalan sambung di Node.js tidak dilempar sebagai exception dari net.connect() — kegagalan datang sebagai event 'error' pada socket, diikuti event 'close'. Simpan sebagai klien-gagal.mjs:
import net from 'node:net';
const server = net.createServer();
server.on('error', () => {});
server.listen(0, '127.0.0.1', () => {
const port = server.address().port;
server.close(() => {
const client = net.connect({ port, host: '127.0.0.1' });
client.on('connect', () => {
console.log('seharusnya tidak tercetak');
});
client.on('error', (err) => {
console.log('event error:', err.code);
});
client.on('close', () => {
console.log('event close terjadi setelah error');
process.exit(0);
});
});
});Output yang Dihasilkan:
event error: ECONNREFUSED
event close terjadi setelah errorServer uji sengaja dibuat lalu langsung ditutup, sehingga portnya benar-benar kosong ketika client datang. Sistem operasi menolak paket sambungan dan Node.js menerjemahkannya sebagai error ECONNREFUSED. Perhatikan dua kebiasaan penting dari output: kita hanya mencetak err.code — properti err.message memuat alamat dan port lengkap yang berubah setiap eksekusi — dan event 'close' tetap terbit setelah 'error', jadi pembersihan resource cukup ditempatkan di satu tempat. Error ECONNREFUSED, EHOSTUNREACH, dan ETIMEDOUT adalah kosakata wajib client jaringan; semuanya hadir lewat mekanisme event yang sama.
Menyambung Ulang dengan Jeda #
Client yang serius tidak menyerah pada kegagalan pertama, tetapi juga tidak menyerbu server dengan percobaan tanpa jeda. Pola standarnya: coba, tunggu sebentar, coba lagi, dengan jumlah percobaan maksimum. Berkas klien-ulang.mjs mendemonstrasikannya — server uji sengaja “menyala” hanya setelah dua kegagalan:
import net from 'node:net';
const tunggu = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function cobaKoneksi(port, percobaan) {
return new Promise((resolve) => {
const client = net.connect({ port, host: '127.0.0.1' });
const selesai = (ok) => {
client.destroy();
resolve(ok);
};
client.once('connect', () => {
console.log(`percobaan ${percobaan}: berhasil terhubung`);
selesai(true);
});
client.once('error', (err) => {
console.log(`percobaan ${percobaan}: gagal (${err.code})`);
selesai(false);
});
});
}
const main = async () => {
const server = net.createServer((socket) => {
socket.on('error', () => {});
socket.end('server akhirnya siap');
});
server.on('error', () => {});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const port = server.address().port;
// simulasi server yang belum siap: tutup socket pendengar
await new Promise((resolve) => server.close(resolve));
let terhubung = false;
const batasPercobaan = 3;
for (let i = 1; i <= batasPercobaan && !terhubung; i++) {
terhubung = await cobaKoneksi(port, i);
if (!terhubung && i < batasPercobaan) {
console.log('menunggu 50 ms sebelum mencoba lagi');
await tunggu(50);
}
if (i === 2 && !terhubung) {
// server "hidup" kembali setelah dua kegagalan
await new Promise((resolve) => server.listen(port, '127.0.0.1', resolve));
}
}
console.log('hasil akhir:', terhubung ? 'terhubung' : 'menyerah');
// satu koneksi penuh untuk membuktikan server benar-benar hidup
await new Promise((resolve) => {
const client = net.connect({ port, host: '127.0.0.1' });
client.on('error', () => {});
client.on('data', (chunk) => {
console.log('sambutan server:', chunk.toString('utf8'));
});
client.on('close', () => {
server.close(() => resolve());
});
});
process.exit(0);
};
main();Output yang Dihasilkan:
percobaan 1: gagal (ECONNREFUSED)
menunggu 50 ms sebelum mencoba lagi
percobaan 2: gagal (ECONNREFUSED)
menunggu 50 ms sebelum mencoba lagi
percobaan 3: berhasil terhubung
hasil akhir: terhubung
sambutan server: server akhirnya siapFungsi cobaKoneksi() membungkus satu percobaan menjadi promise yang selalu selesai — true lewat 'connect', false lewat 'error' — dan memanggil destroy() di kedua jalur supaya socket percobaan tidak menumpuk. Loop utama menjaga dua hal sekaligus: batas batasPercobaan agar program tidak berputar selamanya, dan jeda tunggu(50) antar percobaan agar sistem operasi dan server diberi waktu untuk pulih. Baris if (i === 2 ...) hanyalah pengaturan khusus demo untuk menghidupkan server di tengah jalan; di dunia nyata bagian itu tidak ada, dan yang tersisa persis pola inti di atas.
Dengan bekal tiga pola ini — sambung dan bertukar data, gagal dengan tertangani, sambung ulang dengan jeda — kita sudah bisa menulis client TCP yang tinggal selangkah dari kualitas produksi. Namun semua ini bergantung pada satu asumsi besar: koneksi. Bagaimana jika kita hanya ingin melempar pesan singkat tanpa jabat tangan sama sekali? Itu dunia UDP, dan module node:dgram akan memandu kita di Chapter UDP dengan Module dgram.