Socket #
Socket adalah endpoint komunikasi tingkat rendah yang memungkinkan dua program saling bertukar data melalui jaringan — baik dalam satu mesin maupun lintas mesin di internet. Dart mendukung TCP (andal, berurutan) dan UDP (cepat, tanpa jaminan) melalui dart:io. Berbeda dari HTTP yang punya protokol yang sudah terdefinisi, socket memberi kontrol penuh atas format data yang dikirim — ini kekuatan sekaligus tanggung jawab: kamu harus mendesain protokol sendiri, menangani fragmentasi paket, dan memastikan koneksi dibersihkan dengan benar. Artikel ini membahas semua aspek tersebut dari server sederhana hingga komunikasi multi-client yang aman dengan TLS.
TCP vs UDP — Kapan Menggunakan Masing-masing #
flowchart TD
A{Kebutuhan komunikasi?} --> B{Urutan dan keutuhan\ndata penting?}
B -- Ya --> C{Butuh konfirmasi\npengiriman?}
C -- Ya --> D[TCP\nSocket / ServerSocket]
C -- Tidak --> E[Pertimbangkan\nprotokol aplikasi di atas UDP]
B -- Tidak --> F{Latency rendah\nlebih penting dari keandalan?}
F -- Ya --> G[UDP\nRawDatagramSocket]
F -- Tidak --> D
| Aspek | TCP | UDP |
|---|---|---|
| Koneksi | Connection-oriented (perlu handshake) | Connectionless |
| Urutan paket | Dijamin | Tidak dijamin |
| Keutuhan data | Dijamin | Tidak dijamin |
| Kecepatan | Lebih lambat (overhead konfirmasi) | Lebih cepat |
| Cocok untuk | Chat, transfer file, API, database | Game real-time, video streaming, DNS |
TCP Server Dasar #
import 'dart:io';
import 'dart:convert';
Future<void> main() async {
// Bind ke semua interface IPv4, port 3000
final server = await ServerSocket.bind(
InternetAddress.anyIPv4,
3000,
shared: true, // izinkan binding berulang (berguna saat restart cepat)
);
print('Server TCP berjalan di ${server.address.address}:${server.port}');
// Terima koneksi masuk — server adalah Stream<Socket>
await for (final socket in server) {
print('Client terhubung: ${socket.remoteAddress.address}:${socket.remotePort}');
_tanganiKlien(socket); // handle setiap client tanpa await — non-blocking!
}
}
void _tanganiKlien(Socket socket) {
// Konfigurasi dasar socket
socket.setOption(SocketOption.tcpNoDelay, true); // nonaktifkan Nagle algorithm
// Listen ke data dari client
socket
.transform(utf8.decoder) // bytes → String
.listen(
(data) {
print('Diterima dari ${socket.remoteAddress.address}: $data');
socket.write('Echo: $data\n'); // kirim balik
},
onError: (error) {
print('Error dari client: $error');
socket.destroy(); // paksa tutup socket saat error
},
onDone: () {
print('Client ${socket.remoteAddress.address} terputus');
socket.destroy();
},
cancelOnError: true,
);
}
Masalah Framing — Fragmentasi Data TCP #
Ini adalah salah satu jebakan terbesar dalam pemrograman socket yang sering diabaikan. TCP adalah stream protocol — tidak ada batas pesan. Data yang dikirim dalam satu write() bisa diterima dalam beberapa potongan, atau beberapa write() bisa diterima sekaligus dalam satu chunk.
// ANTI-PATTERN: asumsi satu write() = satu receive()
// Client mengirim: "Halo Server"
socket.write('Halo Server');
// Server menerima — bisa jadi:
// "Halo Ser" lalu "ver" ← dua potongan
// "Halo Server" ← satu utuh
// Tidak ada jaminan!
socket.listen((data) {
final pesan = utf8.decode(data); // ✗ mungkin hanya sebagian!
prosesPesan(pesan);
});
Solusi 1: Length-Prefixed Framing #
Kirim panjang pesan sebelum pesan itu sendiri — receiver bisa tahu kapan pesan lengkap:
import 'dart:io';
import 'dart:typed_data';
import 'dart:convert';
// Kirim pesan dengan prefix panjang 4 byte
void kirimPesan(Socket socket, String pesan) {
final pesanBytes = utf8.encode(pesan);
final panjang = pesanBytes.length;
// Tulis header: panjang pesan dalam 4 byte big-endian
final header = ByteData(4)..setUint32(0, panjang, Endian.big);
socket.add(header.buffer.asUint8List());
socket.add(pesanBytes);
}
// Baca pesan dengan length-prefixed framing
Stream<String> bacaPesan(Socket socket) async* {
final buffer = <int>[];
await for (final chunk in socket) {
buffer.addAll(chunk);
// Proses semua pesan lengkap yang ada di buffer
while (buffer.length >= 4) {
// Baca panjang dari 4 byte pertama
final panjang = ByteData.sublistView(
Uint8List.fromList(buffer.sublist(0, 4)),
).getUint32(0, Endian.big);
// Cek apakah seluruh pesan sudah diterima
if (buffer.length < 4 + panjang) break;
// Ekstrak pesan
final pesanBytes = buffer.sublist(4, 4 + panjang);
yield utf8.decode(pesanBytes);
// Hapus pesan yang sudah diproses dari buffer
buffer.removeRange(0, 4 + panjang);
}
}
}
Solusi 2: Delimiter-Based Framing #
Gunakan karakter pemisah (delimiter) untuk menandai akhir pesan — paling sederhana untuk pesan teks:
import 'dart:io';
import 'dart:convert';
// Gunakan newline sebagai delimiter
void kirimPesan(Socket socket, String pesan) {
// Pesan tidak boleh mengandung \n kecuali sebagai delimiter
assert(!pesan.contains('\n'), 'Pesan tidak boleh mengandung newline');
socket.write('$pesan\n');
}
// Baca pesan yang dipisahkan newline
Stream<String> bacaPesan(Socket socket) {
return socket
.transform(utf8.decoder)
.transform(const LineSplitter()); // split per \n
}
Server Multi-Client dengan Manajemen Koneksi #
Server produksi harus bisa menangani banyak client sekaligus, melacak koneksi aktif, dan membersihkan resource dengan benar:
import 'dart:io';
import 'dart:convert';
class ServerTCP {
final Map<String, Socket> _klien = {};
ServerSocket? _server;
Future<void> mulai(int port) async {
_server = await ServerSocket.bind(InternetAddress.anyIPv4, port);
print('Server berjalan di port $port');
await for (final socket in _server!) {
_sambut(socket);
}
}
void _sambut(Socket socket) {
final id = '${socket.remoteAddress.address}:${socket.remotePort}';
_klien[id] = socket;
print('Client baru: $id (total: ${_klien.length})');
// Broadcast ke semua client bahwa ada yang bergabung
_broadcast('[$id telah bergabung]', kecuali: id);
socket
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(
(pesan) {
print('[$id]: $pesan');
_broadcast('[$id]: $pesan', kecuali: id); // relay ke semua
},
onError: (_) => _putuskan(id),
onDone: () => _putuskan(id),
cancelOnError: true,
);
}
void _broadcast(String pesan, {String? kecuali}) {
for (final entry in _klien.entries) {
if (entry.key != kecuali) {
try {
entry.value.writeln(pesan);
} catch (_) {
// Abaikan error saat kirim — client mungkin sudah disconnected
}
}
}
}
void _putuskan(String id) {
_klien.remove(id)?.destroy();
print('Client terputus: $id (total: ${_klien.length})');
_broadcast('[$id telah keluar]');
}
Future<void> tutup() async {
// Tutup semua koneksi client
for (final socket in _klien.values) {
socket.destroy();
}
_klien.clear();
await _server?.close();
}
}
void main() async {
final server = ServerTCP();
await server.mulai(3000);
}
TCP Client dengan Timeout dan Reconnect #
Client yang andal harus menangani timeout koneksi, timeout operasi, dan logika reconnect:
import 'dart:io';
import 'dart:async';
import 'dart:convert';
class KlienTCP {
final String host;
final int port;
Socket? _socket;
bool _terhubung = false;
KlienTCP({required this.host, required this.port});
Future<void> hubungkan() async {
// Timeout saat koneksi — lempar SocketException jika tidak tersambung dalam 5 detik
_socket = await Socket.connect(
host,
port,
timeout: const Duration(seconds: 5),
);
_terhubung = true;
print('Terhubung ke $host:$port');
// Konfigurasi keepalive agar koneksi idle tidak diputus oleh firewall
_socket!.setOption(SocketOption.tcpNoDelay, true);
}
void dengarkan(void Function(String) onData) {
_socket!
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(
onData,
onError: (e) {
print('Error jaringan: $e');
_terhubung = false;
},
onDone: () {
print('Server menutup koneksi');
_terhubung = false;
},
);
}
Future<void> kirim(String pesan) async {
if (!_terhubung || _socket == null) {
throw StateError('Tidak terhubung ke server');
}
_socket!.writeln(pesan);
await _socket!.flush(); // pastikan data terkirim
}
// Reconnect dengan exponential backoff
Future<void> hubungkanDenganRetry({int maxCoba = 5}) async {
for (int i = 0; i < maxCoba; i++) {
try {
await hubungkan();
return; // berhasil
} on SocketException catch (e) {
final tunda = Duration(seconds: (1 << i)); // 1, 2, 4, 8, 16 detik
print('Gagal terhubung (percobaan ${i + 1}): $e');
print('Mencoba lagi dalam ${tunda.inSeconds}s...');
await Future.delayed(tunda);
}
}
throw StateError('Gagal terhubung setelah $maxCoba percobaan');
}
Future<void> putuskan() async {
_terhubung = false;
await _socket?.close();
_socket = null;
}
}
TLS/SSL — Koneksi Aman #
Untuk komunikasi yang melewati jaringan publik, selalu enkripsi dengan TLS. Dart mendukung TLS melalui SecureServerSocket dan SecureSocket:
import 'dart:io';
// Server dengan TLS
Future<void> jalankanServerTLS() async {
// Muat sertifikat dan private key
final konteks = SecurityContext()
..useCertificateChain('server.crt') // sertifikat server
..usePrivateKey('server.key'); // private key
// SecureServerSocket = ServerSocket + TLS
final server = await SecureServerSocket.bind(
InternetAddress.anyIPv4,
443,
konteks,
);
print('Server HTTPS/TLS berjalan di port 443');
await for (final socket in server) {
socket
.transform(utf8.decoder)
.listen(
(data) {
print('Data terenkripsi diterima: $data');
socket.write('OK\n');
},
onDone: () => socket.destroy(),
);
}
}
// Client dengan TLS
Future<void> hubungkanTLS() async {
// Untuk development dengan self-signed cert — JANGAN di production tanpa verifikasi!
final socket = await SecureSocket.connect(
'localhost',
443,
onBadCertificate: (cert) => true, // ✗ hanya untuk dev/testing
);
// Untuk production — verifikasi sertifikat otomatis (default behavior)
final socketProduksi = await SecureSocket.connect(
'api.example.com',
443,
// onBadCertificate tidak di-set = verifikasi otomatis
);
socketProduksi.writeln('GET / HTTP/1.1\r\nHost: api.example.com\r\n\r\n');
await for (final data in socketProduksi.transform(utf8.decoder)) {
print(data);
break; // hanya baca respons pertama
}
await socketProduksi.close();
}
Jangan gunakan onBadCertificate: (cert) => true di production — ini menonaktifkan verifikasi sertifikat dan membuat koneksi rentan terhadap serangan man-in-the-middle. Hanya gunakan untuk testing lokal dengan self-signed certificate.
UDP dengan RawDatagramSocket
#
UDP cocok untuk kasus di mana kecepatan lebih penting dari keandalan — game multiplayer, penemuan layanan di jaringan lokal, atau monitoring metrik:
import 'dart:io';
// Server UDP
Future<void> serverUDP() async {
final socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 4000);
print('Server UDP berjalan di port 4000');
socket.listen((RawSocketEvent event) {
if (event == RawSocketEvent.read) {
final datagram = socket.receive();
if (datagram == null) return;
final pesan = String.fromCharCodes(datagram.data);
print('UDP diterima dari ${datagram.address.address}:${datagram.port}: $pesan');
// Kirim balik ke pengirim
final balasan = 'ACK: $pesan';
socket.send(balasan.codeUnits, datagram.address, datagram.port);
}
});
}
// Client UDP
Future<void> clientUDP() async {
// Port 0 = OS pilihkan port yang tersedia secara otomatis
final socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
// Kirim datagram ke server
final pesan = 'Ping dari client';
socket.send(pesan.codeUnits, InternetAddress.loopbackIPv4, 4000);
// Tunggu balasan dengan timeout
bool dapatBalasan = false;
socket.listen((RawSocketEvent event) {
if (event == RawSocketEvent.read) {
final datagram = socket.receive();
if (datagram != null) {
print('Balasan UDP: ${String.fromCharCodes(datagram.data)}');
dapatBalasan = true;
socket.close();
}
}
});
// Timeout jika tidak ada balasan dalam 3 detik
await Future.delayed(const Duration(seconds: 3));
if (!dapatBalasan) {
print('Timeout — server tidak merespons');
socket.close();
}
}
UDP Broadcast — Penemuan Layanan di LAN #
import 'dart:io';
// Broadcast penemuan layanan ke seluruh jaringan lokal
Future<void> broadcast() async {
final socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
socket.broadcastEnabled = true; // wajib diaktifkan untuk broadcast
final alamatBroadcast = InternetAddress('255.255.255.255');
final pesan = 'DISCOVER:MyService:v1';
socket.send(pesan.codeUnits, alamatBroadcast, 5000);
print('Broadcast terkirim, menunggu respons...');
// Kumpulkan respons selama 2 detik
final layananDitemukan = <String>[];
await socket.timeout(const Duration(seconds: 2)).forEach((event) {
if (event == RawSocketEvent.read) {
final dg = socket.receive();
if (dg != null) {
layananDitemukan.add(
'${dg.address.address}:${dg.port} - ${String.fromCharCodes(dg.data)}');
}
}
}).catchError((_) {}); // timeout diterima sebagai error
socket.close();
print('Layanan ditemukan: $layananDitemukan');
}
Anti-Pattern Socket #
Tidak Menutup Socket #
// ANTI-PATTERN: socket tidak pernah ditutup — resource leak
Future<void> buruk() async {
final socket = await Socket.connect('localhost', 3000);
socket.write('data');
// ✗ socket tidak pernah ditutup — file descriptor leaked!
}
// BENAR: selalu tutup dengan try-finally
Future<void> baik() async {
final socket = await Socket.connect('localhost', 3000);
try {
socket.write('data');
await socket.flush();
} finally {
await socket.close(); // ✓ selalu dieksekusi
}
}
Tidak Menangani Fragmentasi #
// ANTI-PATTERN: asumsi satu listen callback = satu pesan utuh
socket.listen((data) {
final pesan = utf8.decode(data); // ✗ bisa jadi hanya potongan pesan!
jsonDecode(pesan); // akan throw jika JSON tidak lengkap
});
// BENAR: gunakan buffer dan framing yang tepat
final buffer = StringBuffer();
socket.transform(utf8.decoder).listen((chunk) {
buffer.write(chunk);
// Proses hanya jika pesan lengkap (misal: ada newline sebagai delimiter)
final teks = buffer.toString();
if (teks.contains('\n')) {
final baris = teks.split('\n');
for (int i = 0; i < baris.length - 1; i++) {
prosesPesanLengkap(baris[i]);
}
buffer.clear();
buffer.write(baris.last); // sisa yang belum lengkap
}
});
Blocking Write Tanpa Backpressure #
// ANTI-PATTERN: menulis tanpa cek apakah buffer penuh
for (int i = 0; i < 1_000_000; i++) {
socket.write('Data ke-$i\n'); // ✗ bisa overflow buffer internal
}
// BENAR: cek done future dan gunakan addStream untuk backpressure
final stream = Stream.fromIterable(
List.generate(1_000_000, (i) => 'Data ke-$i\n'),
).map(utf8.encode);
// addStream menangani backpressure secara otomatis
await socket.addStream(stream);
await socket.flush();
Ringkasan #
- TCP untuk keandalan, UDP untuk kecepatan — TCP menjamin urutan dan keutuhan data; UDP lebih cepat tapi tidak ada jaminan pengiriman. Pilih sesuai kebutuhan aplikasi.
- TCP adalah stream protocol — tidak ada batas pesan bawaan. Selalu implementasikan framing: length-prefix (4 byte panjang di depan) atau delimiter (
\n) untuk memisahkan pesan._tanganiKlientanpaawait— di server multi-client, handle setiap koneksi tanpa await agar server bisa terus menerima koneksi baru. Dart event loop menangani konkurrency secara otomatis.socket.setOption(tcpNoDelay: true)menonaktifkan Nagle algorithm — berguna untuk aplikasi interaktif di mana latency rendah lebih penting dari throughput optimal.- Selalu tutup socket di
finally— atau panggildestroy()saat error untuk memastikan file descriptor tidak leaked.- TLS untuk koneksi publik — gunakan
SecureServerSocketdanSecureSocketuntuk enkripsi. Jangan pernah nonaktifkan verifikasi sertifikat di production.- Reconnect dengan exponential backoff — jangan retry segera; tunggu 1, 2, 4, 8 detik secara bertingkat untuk menghindari flood ke server yang sedang bermasalah.
- UDP broadcast untuk penemuan layanan di LAN — aktifkan
socket.broadcastEnabled = truesebelum mengirim ke255.255.255.255.- Backpressure dengan
addStream— untuk mengirim data besar, gunakanaddStreamdaripadawriteberulang agar buffer tidak overflow.- Lacak semua koneksi aktif di server dengan Map — sehingga bisa dibersihkan saat server mati dan broadcast ke semua client.