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| Aspect | TCP | UDP |
|---|---|---|
| Connection | Connection-oriented (needs handshake) | Connectionless |
| Packet order | Guaranteed | Not guaranteed |
| Data integrity | Guaranteed | Not guaranteed |
| Speed | Slower (confirmation overhead) | Faster |
| Best for | Chat, file transfer, APIs, databases | Real-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._tanganiKlienwithoutawait— 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 calldestroy()on error to make sure file descriptors aren’t leaked.- TLS for public connections — use
SecureServerSocketandSecureSocketfor 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 = truebefore sending to255.255.255.255.- Backpressure with
addStream— for sending large data, useaddStreaminstead of repeatedwriteso 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.