Socket #

A socket is a low-level communication endpoint that lets two programs exchange data over a network — whether on the same machine or across machines on the internet. Dart supports TCP (reliable, ordered) and UDP (fast, no guarantees) through dart:io. Unlike HTTP, which has a predefined protocol, sockets give you full control over the format of data sent — this is both a strength and a responsibility: you must design your own protocol, handle packet fragmentation, and ensure connections are cleaned up properly. This article covers all those aspects from a simple server to secure multi-client communication with TLS.

TCP vs UDP — When to Use Each #

flowchart TD
    A{Communication needs?} --> B{Are data order and\\nintegrity important?}
    B -- Yes --> C{Need delivery\\nconfirmation?}
    C -- Yes --> D[TCP\\nSocket / ServerSocket]
    C -- No --> E[Consider an\\napplication protocol over UDP]
    B -- No --> F{Is low latency\\nmore important than reliability?}
    F -- Yes --> G[UDP\\nRawDatagramSocket]
    F -- No --> D
AspectTCPUDP
ConnectionConnection-oriented (needs handshake)Connectionless
Packet orderGuaranteedNot guaranteed
Data integrityGuaranteedNot guaranteed
SpeedSlower (confirmation overhead)Faster
Best forChat, file transfer, APIs, databasesReal-time games, video streaming, DNS

Basic TCP Server #

import 'dart:io';
import 'dart:convert';

Future<void> main() async {
  // Bind to all IPv4 interfaces, port 3000
  final server = await ServerSocket.bind(
    InternetAddress.anyIPv4,
    3000,
    shared: true,    // allow repeated binding (useful for fast restarts)
  );

  print('TCP server running at ${server.address.address}:${server.port}');

  // Accept incoming connections — the server is a Stream<Socket>
  await for (final socket in server) {
    print('Client connected: ${socket.remoteAddress.address}:${socket.remotePort}');
    _tanganiKlien(socket); // handle each client without await — non-blocking!
  }
}

void _tanganiKlien(Socket socket) {
  // Basic socket configuration
  socket.setOption(SocketOption.tcpNoDelay, true); // disable the Nagle algorithm

  // Listen to data from the client
  socket
      .transform(utf8.decoder)        // bytes → String
      .listen(
    (data) {
      print('Received from ${socket.remoteAddress.address}: $data');
      socket.write('Echo: $data\n');  // send back
    },
    onError: (error) {
      print('Error from client: $error');
      socket.destroy(); // force-close the socket on error
    },
    onDone: () {
      print('Client ${socket.remoteAddress.address} disconnected');
      socket.destroy();
    },
    cancelOnError: true,
  );
}

The Framing Problem — TCP Data Fragmentation #

This is one of the biggest, often overlooked traps in socket programming. TCP is a stream protocol — there are no message boundaries. Data sent in one write() can be received in several pieces, or several write()s can be received at once in a single chunk.

// ANTI-PATTERN: assuming one write() = one receive()
// The client sends: "Halo Server"
socket.write('Halo Server');

// The server may receive:
// "Halo Ser" then "ver"  ← two pieces
// "Halo Server" ← one whole piece
// No guarantees!
socket.listen((data) {
  final pesan = utf8.decode(data); // ✗ may only be a fragment!
  prosesPesan(pesan);
});

Solution 1: Length-Prefixed Framing #

Send the message length before the message itself — the receiver can know when a message is complete:

import 'dart:io';
import 'dart:typed_data';
import 'dart:convert';

// Send a message with a 4-byte length prefix
void kirimPesan(Socket socket, String pesan) {
  final pesanBytes = utf8.encode(pesan);
  final panjang = pesanBytes.length;

  // Write the header: message length in 4 big-endian bytes
  final header = ByteData(4)..setUint32(0, panjang, Endian.big);
  socket.add(header.buffer.asUint8List());
  socket.add(pesanBytes);
}

// Read messages with length-prefixed framing
Stream<String> bacaPesan(Socket socket) async* {
  final buffer = <int>[];

  await for (final chunk in socket) {
    buffer.addAll(chunk);

    // Process all complete messages in the buffer
    while (buffer.length >= 4) {
      // Read the length from the first 4 bytes
      final panjang = ByteData.sublistView(
        Uint8List.fromList(buffer.sublist(0, 4)),
      ).getUint32(0, Endian.big);

      // Check whether the entire message has arrived
      if (buffer.length < 4 + panjang) break;

      // Extract the message
      final pesanBytes = buffer.sublist(4, 4 + panjang);
      yield utf8.decode(pesanBytes);

      // Remove the processed message from the buffer
      buffer.removeRange(0, 4 + panjang);
    }
  }
}

Solution 2: Delimiter-Based Framing #

Use a delimiter character to mark the end of a message — simplest for text messages:

import 'dart:io';
import 'dart:convert';

// Use a newline as the delimiter
void kirimPesan(Socket socket, String pesan) {
  // The message must not contain \n except as the delimiter
  assert(!pesan.contains('\n'), 'Message must not contain a newline');
  socket.write('$pesan\n');
}

// Read newline-delimited messages
Stream<String> bacaPesan(Socket socket) {
  return socket
      .transform(utf8.decoder)
      .transform(const LineSplitter()); // split per \n
}

Multi-Client Server with Connection Management #

A production server must handle many clients at once, track active connections, and clean up resources correctly:

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 running on 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('New client: $id (total: ${_klien.length})');

    // Broadcast to all clients that someone joined
    _broadcast('[$id has joined]', kecuali: id);

    socket
        .transform(utf8.decoder)
        .transform(const LineSplitter())
        .listen(
      (pesan) {
        print('[$id]: $pesan');
        _broadcast('[$id]: $pesan', kecuali: id); // relay to everyone
      },
      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 (_) {
          // Ignore send errors — the client may already be disconnected
        }
      }
    }
  }

  void _putuskan(String id) {
    _klien.remove(id)?.destroy();
    print('Client disconnected: $id (total: ${_klien.length})');
    _broadcast('[$id has left]');
  }

  Future<void> tutup() async {
    // Close all client connections
    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 with Timeout and Reconnect #

A reliable client must handle connection timeouts, operation timeouts, and reconnect logic:

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 {
    // Connection timeout — throws SocketException if not connected in 5 seconds
    _socket = await Socket.connect(
      host,
      port,
      timeout: const Duration(seconds: 5),
    );
    _terhubung = true;
    print('Connected to $host:$port');

    // Configure keepalive so idle connections aren't dropped by firewalls
    _socket!.setOption(SocketOption.tcpNoDelay, true);
  }

  void dengarkan(void Function(String) onData) {
    _socket!
        .transform(utf8.decoder)
        .transform(const LineSplitter())
        .listen(
      onData,
      onError: (e) {
        print('Network error: $e');
        _terhubung = false;
      },
      onDone: () {
        print('Server closed the connection');
        _terhubung = false;
      },
    );
  }

  Future<void> kirim(String pesan) async {
    if (!_terhubung || _socket == null) {
      throw StateError('Not connected to the server');
    }
    _socket!.writeln(pesan);
    await _socket!.flush(); // make sure the data is sent
  }

  // Reconnect with exponential backoff
  Future<void> hubungkanDenganRetry({int maxCoba = 5}) async {
    for (int i = 0; i < maxCoba; i++) {
      try {
        await hubungkan();
        return; // success
      } on SocketException catch (e) {
        final tunda = Duration(seconds: (1 << i)); // 1, 2, 4, 8, 16 seconds
        print('Failed to connect (attempt ${i + 1}): $e');
        print('Retrying in ${tunda.inSeconds}s...');
        await Future.delayed(tunda);
      }
    }
    throw StateError('Failed to connect after $maxCoba attempts');
  }

  Future<void> putuskan() async {
    _terhubung = false;
    await _socket?.close();
    _socket = null;
  }
}

TLS/SSL — Secure Connections #

For communication over public networks, always encrypt with TLS. Dart supports TLS via SecureServerSocket and SecureSocket:

import 'dart:io';

// Server with TLS
Future<void> jalankanServerTLS() async {
  // Load the certificate and private key
  final konteks = SecurityContext()
    ..useCertificateChain('server.crt')  // server certificate
    ..usePrivateKey('server.key');        // private key

  // SecureServerSocket = ServerSocket + TLS
  final server = await SecureServerSocket.bind(
    InternetAddress.anyIPv4,
    443,
    konteks,
  );

  print('HTTPS/TLS server running on port 443');

  await for (final socket in server) {
    socket
        .transform(utf8.decoder)
        .listen(
      (data) {
        print('Encrypted data received: $data');
        socket.write('OK\n');
      },
      onDone: () => socket.destroy(),
    );
  }
}

// Client with TLS
Future<void> hubungkanTLS() async {
  // For development with a self-signed cert — NOT for production without verification!
  final socket = await SecureSocket.connect(
    'localhost',
    443,
    onBadCertificate: (cert) => true, // ✗ only for dev/testing
  );

  // For production — certificate verification happens automatically (default behavior)
  final socketProduksi = await SecureSocket.connect(
    'api.example.com',
    443,
    // onBadCertificate not set = automatic verification
  );

  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; // only read the first response
  }

  await socketProduksi.close();
}
Don’t use onBadCertificate: (cert) => true in production — this disables certificate verification and makes the connection vulnerable to man-in-the-middle attacks. Only use it for local testing with a self-signed certificate.

UDP with RawDatagramSocket #

UDP fits cases where speed matters more than reliability — multiplayer games, service discovery on a local network, or metric monitoring:

import 'dart:io';

// UDP server
Future<void> serverUDP() async {
  final socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 4000);
  print('UDP server running on 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 received from ${datagram.address.address}:${datagram.port}: $pesan');

      // Send back to the sender
      final balasan = 'ACK: $pesan';
      socket.send(balasan.codeUnits, datagram.address, datagram.port);
    }
  });
}

// UDP client
Future<void> clientUDP() async {
  // Port 0 = the OS picks an available port automatically
  final socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);

  // Send a datagram to the server
  final pesan = 'Ping from client';
  socket.send(pesan.codeUnits, InternetAddress.loopbackIPv4, 4000);

  // Wait for a reply with a timeout
  bool dapatBalasan = false;
  socket.listen((RawSocketEvent event) {
    if (event == RawSocketEvent.read) {
      final datagram = socket.receive();
      if (datagram != null) {
        print('UDP reply: ${String.fromCharCodes(datagram.data)}');
        dapatBalasan = true;
        socket.close();
      }
    }
  });

  // Timeout if there's no reply in 3 seconds
  await Future.delayed(const Duration(seconds: 3));
  if (!dapatBalasan) {
    print('Timeout — the server didn't respond');
    socket.close();
  }
}

UDP Broadcast — Service Discovery on the LAN #

import 'dart:io';

// Broadcast service discovery to the entire local network
Future<void> broadcast() async {
  final socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
  socket.broadcastEnabled = true; // must be enabled for broadcast

  final alamatBroadcast = InternetAddress('255.255.255.255');
  final pesan = 'DISCOVER:MyService:v1';
  socket.send(pesan.codeUnits, alamatBroadcast, 5000);

  print('Broadcast sent, waiting for responses...');

  // Collect responses for 2 seconds
  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 arrives as an error

  socket.close();
  print('Services found: $layananDitemukan');
}

Socket Anti-Patterns #

Not Closing Sockets #

// ANTI-PATTERN: the socket is never closed — resource leak
Future<void> buruk() async {
  final socket = await Socket.connect('localhost', 3000);
  socket.write('data');
  // ✗ the socket is never closed — file descriptor leaked!
}

// CORRECT: always close with try-finally
Future<void> baik() async {
  final socket = await Socket.connect('localhost', 3000);
  try {
    socket.write('data');
    await socket.flush();
  } finally {
    await socket.close(); // ✓ always executed
  }
}

Not Handling Fragmentation #

// ANTI-PATTERN: assuming one listen callback = one complete message
socket.listen((data) {
  final pesan = utf8.decode(data); // ✗ could just be a message fragment!
  jsonDecode(pesan);               // will throw if the JSON is incomplete
});

// CORRECT: use a buffer and proper framing
final buffer = StringBuffer();
socket.transform(utf8.decoder).listen((chunk) {
  buffer.write(chunk);
  // Process only when the message is complete (e.g. newline as the 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); // the incomplete remainder
  }
});

Blocking Writes Without Backpressure #

// ANTI-PATTERN: writing without checking whether the buffer is full
for (int i = 0; i < 1_000_000; i++) {
  socket.write('Data ke-$i\n'); // ✗ can overflow the internal buffer
}

// CORRECT: check the done future and use addStream for backpressure
final stream = Stream.fromIterable(
  List.generate(1_000_000, (i) => 'Data ke-$i\n'),
).map(utf8.encode);

// addStream handles backpressure automatically
await socket.addStream(stream);
await socket.flush();

Summary #

  • TCP for reliability, UDP for speed — TCP guarantees order and data integrity; UDP is faster but with no delivery guarantees. Choose according to your application’s needs.
  • TCP is a stream protocol — there are no built-in message boundaries. Always implement framing: length-prefix (4-byte length in front) or a delimiter (\n) to separate messages.
  • _tanganiKlien without await — in a multi-client server, handle each connection without awaiting so the server can keep accepting new connections. The Dart event loop handles concurrency automatically.
  • socket.setOption(tcpNoDelay: true) disables the Nagle algorithm — useful for interactive apps where low latency matters more than optimal throughput.
  • Always close sockets in finally — or call destroy() on error to make sure file descriptors aren’t leaked.
  • TLS for public connections — use SecureServerSocket and SecureSocket for encryption. Never disable certificate verification in production.
  • Reconnect with exponential backoff — don’t retry immediately; wait 1, 2, 4, 8 seconds progressively to avoid flooding a server that’s having problems.
  • UDP broadcast for LAN service discovery — enable socket.broadcastEnabled = true before sending to 255.255.255.255.
  • Backpressure with addStream — for sending large data, use addStream instead of repeated write so the buffer doesn’t overflow.
  • Track all active connections on the server with a Map — so they can be cleaned up when the server shuts down and broadcast to all clients.

← Previous: I/O   Next: Web Socket →

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