Socket

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.
  • _tanganiKlien tanpa await — 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 panggil destroy() saat error untuk memastikan file descriptor tidak leaked.
  • TLS untuk koneksi publik — gunakan SecureServerSocket dan SecureSocket untuk 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 = true sebelum mengirim ke 255.255.255.255.
  • Backpressure dengan addStream — untuk mengirim data besar, gunakan addStream daripada write berulang agar buffer tidak overflow.
  • Lacak semua koneksi aktif di server dengan Map — sehingga bisa dibersihkan saat server mati dan broadcast ke semua client.

← Sebelumnya: I/O   Berikutnya: Web Socket →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact