local_websocket 0.0.1 copy "local_websocket: ^0.0.1" to clipboard
local_websocket: ^0.0.1 copied to clipboard

A pure Dart library for local network WebSocket communication with automatic server discovery and scanning capabilities.

Local WebSocket #

Apache 2.0 License Flutter Dart

A pure Dart library for local network WebSocket communication with automatic server discovery and scanning capabilities. Build real-time communication between devices on the same network with minimal setup.


Table of Contents #


Features #

  • πŸ” Automatic Server Discovery - Scan local networks to find WebSocket servers automatically
  • πŸš€ Easy Server Setup - Create WebSocket servers with minimal configuration
  • πŸ“± Client Connection Management - Simple client connection and real-time messaging
  • 🌐 Cross-Platform - Works on all Dart platforms (Flutter, CLI, Desktop, Server)
  • πŸ”§ Pure Dart - No platform-specific dependencies, works everywhere Dart runs
  • πŸ’¬ Broadcast & Echo Modes - Choose between broadcasting to all clients or echoing back to sender
  • 🏷️ Metadata Support - Attach custom details to servers and clients
  • πŸ“‘ Real-time Streaming - Reactive streams for messages, connections, and client updates
  • πŸ†” Unique Client IDs - Automatic UUID generation for each client
  • πŸ›‘οΈ Type-Safe - Fully typed API with Dart's null safety
  • πŸ” Flexible Authentication - Extensible delegate-based authentication system
  • βœ… Request & Message Validation - Validate requests, clients, and messages with custom delegates
  • πŸ”Œ Connection Lifecycle Hooks - Handle client connection and disconnection events

Installation #

Add local_websocket to your pubspec.yaml:

dependencies:
  local_websocket: ^0.0.1

Then run:

dart pub get

Or for Flutter projects:

flutter pub get

Quick Start #

1. Create a WebSocket Server #

import 'package:local_websocket/local_websocket.dart';

void main() async {
  // Create server with custom details
  final server = Server(
    echo: false, // Broadcast mode: messages go to all OTHER clients
    details: {
      'name': 'My Local Server',
      'description': 'A local WebSocket server',
    },
  );

  // Start server on localhost:8080
  await server.start('127.0.0.1', port: 8080);
  print('Server running at ${server.address}');

  // Listen for client connections
  server.clientsStream.listen((clients) {
    print('Connected clients: ${clients.length}');
  });
}

2. Discover Servers on Network #

import 'package:local_websocket/local_websocket.dart';

void main() async {
  // Scan localhost for servers on port 8080
  // Returns a Stream that continuously scans every 3 seconds
  await for (final servers in Scanner.scan('localhost', port: 8080)) {
    print('Found ${servers.length} servers:');
    for (final server in servers) {
      print('- ${server.path}');
      print('  Details: ${server.details}');
    }
  }
}

3. Connect Client and Send Messages #

import 'package:local_websocket/local_websocket.dart';

void main() async {
  // Create client with metadata
  final client = Client(
    details: {
      'username': 'john_doe',
      'device': 'mobile',
    },
  );

  // Connect to server
  await client.connect('ws://127.0.0.1:8080/ws');
  print('Connected! Client ID: ${client.uid}');

  // Listen for messages
  client.messageStream.listen((message) {
    print('Received: $message');
  });

  // Listen for connection changes
  client.connectionStream.listen((isConnected) {
    print('Connection status: ${isConnected ? "Connected" : "Disconnected"}');
  });

  // Send messages (supports String, Map, List, etc.)
  client.send('Hello, server!');
  client.send({'type': 'chat', 'message': 'Hello from client'});
  client.send(['data', 123, true]);
  
  // Disconnect when done
  await Future.delayed(Duration(seconds: 5));
  await client.disconnect();
}

Core Concepts #

Server #

The Server is the central hub that accepts WebSocket connections and manages message routing between clients. It runs on a specified host and port, provides server information via HTTP, and handles WebSocket connections.

Two Messaging Modes:

  • Broadcast Mode (echo: false): Messages from one client are sent to all OTHER clients (sender doesn't receive their own message)
  • Echo Mode (echo: true): Messages from one client are sent to ALL clients including the sender

Client #

The Client connects to a server via WebSocket and can send/receive messages in real-time. Each client has a unique UUID and can include custom metadata (username, device type, etc.) that gets passed to the server.

Scanner #

The Scanner automatically discovers servers on the local network by scanning IP addresses in a subnet. It checks each IP for the server's HTTP endpoint and validates it's a local-websocket server by checking the Server header.

DiscoveredServer #

A simple model representing a discovered server with:

  • path: The WebSocket URL (e.g., ws://192.168.1.100:8080/ws)
  • details: The server's metadata (returned from the HTTP endpoint)

Detailed Guide #

Server #

Creating a Server

final server = Server(
  echo: false, // Broadcast mode
  details: {
    'name': 'Game Server',
    'maxPlayers': 4,
    'gameType': 'multiplayer',
  },
  // Optional: Add authentication
  requestAuthenticationDelegate: RequestTokenAuthenticator(
    validTokens: {'secret123', 'secret456'},
  ),
  // Optional: Handle client connections
  clientConnectionDelegate: MyConnectionHandler(),
);

Starting the Server

// Start on localhost (127.0.0.1)
await server.start('127.0.0.1', port: 8080);

// Start on all network interfaces (0.0.0.0)
await server.start('0.0.0.0', port: 8080);

// Start on specific IP address
await server.start('192.168.1.100', port: 9000);

Important:

  • 127.0.0.1 (localhost): Only accessible from the same machine
  • 0.0.0.0: Accessible from any network interface
  • Specific IP: Accessible via that IP address

Server Properties

server.isConnected;      // bool: Is server running?
server.address;          // Uri: Server address (throws if not running)
server.clients;          // Set<Client>: Currently connected clients
server.clientsStream;    // Stream<Set<Client>>: Stream of client changes
server.connectionStream; // Stream<bool>: Stream of server connection status
server.messageStream;    // Stream<dynamic>: Stream of all messages received

Sending Messages from Server

// Send to all connected clients
server.send('Server announcement!');
server.send({'type': 'notification', 'message': 'New player joined'});

Stopping the Server

await server.stop();
// Automatically disconnects all clients

Server HTTP Endpoint

When running, the server provides an HTTP endpoint at the root path (/) that returns server details as JSON:

curl http://127.0.0.1:8080/
# Response: {"name":"Game Server","maxPlayers":4,"gameType":"multiplayer"}

This endpoint includes a custom header: Server: local-websocket/1.0.0, which the Scanner uses for discovery.


Client #

Creating a Client

final client = Client(
  details: {
    'username': 'Alice',
    'deviceType': 'iOS',
    'appVersion': '1.0.0',
  },
);

Note: The details map values must be String type. They're passed to the server as query parameters.

Connecting to a Server

// Connect using discovered server
final servers = await Scanner.scan('localhost').first;
await client.connect(servers.first.path);

// Or connect directly
await client.connect('ws://127.0.0.1:8080/ws');

The client details are automatically added as query parameters:

ws://127.0.0.1:8080/ws?username=Alice&deviceType=iOS&appVersion=1.0.0

Client Properties

client.uid;              // String: Unique UUID for this client
client.details;          // Map<String, String>: Client metadata (unmodifiable)
client.isConnected;      // bool: Is client connected?
client.messageStream;    // Stream<dynamic>: Incoming messages
client.connectionStream; // Stream<bool>: Connection status changes

Sending Messages

// Send string
client.send('Hello!');

// Send JSON-encodable data
client.send({'action': 'move', 'x': 10, 'y': 20});

// Send list
client.send([1, 2, 3, 4, 5]);

Receiving Messages

client.messageStream.listen((message) {
  // Messages arrive as dynamic - cast as needed
  if (message is String) {
    print('Text message: $message');
  } else if (message is Map) {
    print('JSON message: $message');
  }
});

Monitoring Connection Status

client.connectionStream.listen((isConnected) {
  if (isConnected) {
    print('Connected to server');
  } else {
    print('Disconnected from server');
  }
});

Disconnecting

await client.disconnect();

Scanner #

Basic Scanning

// Scan localhost continuously (every 3 seconds)
await for (final servers in Scanner.scan('localhost')) {
  print('Found ${servers.length} servers');
}

Scanning a Subnet

// Scan 192.168.1.0-255 subnet on port 8080
await for (final servers in Scanner.scan('192.168.1')) {
  for (final server in servers) {
    print('Server at ${server.path}');
    print('Details: ${server.details}');
  }
}

Custom Port and Interval

// Scan every 5 seconds on port 9000
await for (final servers in Scanner.scan(
  '192.168.1',
  port: 9000,
  interval: Duration(seconds: 5),
)) {
  // Handle discovered servers
}

One-Time Scan

// Get first scan result and stop
final servers = await Scanner.scan('localhost').first;
print('Found ${servers.length} servers');

Scanner Parameters

Scanner.scan(
  String host,              // 'localhost', '127.0.0.1', or '192.168.1'
  {
    int port = 8080,        // Port to scan
    Duration interval = const Duration(seconds: 3), // Scan interval
    String type = 'local-websocket', // Server type identifier
  }
)

Host Formats:

  • 'localhost' or '127.0.0.1': Scans 127.0.0.0-255
  • '192.168.1': Scans 192.168.1.0-255
  • Must be at least 3 parts when using IP format

Delegates #

The package provides a powerful delegate system for customizing server behavior. Delegates allow you to add authentication, validation, and event handling without modifying the core server logic.

Overview #

There are four types of delegates:

  1. RequestAuthenticationDelegate - Authenticate HTTP requests before WebSocket upgrade
  2. ClientValidationDelegate - Validate clients after WebSocket connection established
  3. ClientConnectionDelegate - Handle client connection/disconnection events
  4. MessageValidationDelegate - Validate individual messages from clients

RequestAuthenticationDelegate #

Authenticates incoming HTTP requests before the WebSocket upgrade occurs. This is your first line of defense for security.

Interface

abstract interface class RequestAuthenticationDelegate {
  FutureOr<RequestAuthenticationResult> authenticateRequest(Request request);
}

Built-in Authenticators

1. RequestTokenAuthenticator

Validates a token passed as a query parameter:

final server = Server(
  requestAuthenticationDelegate: RequestTokenAuthenticator(
    validTokens: {'secret123', 'admin_token', 'user_pass'},
    parameterName: 'token', // Default parameter name
  ),
);

Clients must include the token in the connection URL:

await client.connect('ws://127.0.0.1:8080/ws?token=secret123');

Or use the details parameter:

final client = Client(details: {'token': 'secret123'});
await client.connect('ws://127.0.0.1:8080/ws');
2. RequestHeaderAuthenticator

Validates HTTP headers (useful for authorization tokens):

final server = Server(
  requestAuthenticationDelegate: RequestHeaderAuthenticator(
    headerName: 'Authorization',
    validValues: {'Bearer secret123', 'Bearer admin_token'},
    caseSensitive: true, // Default is true
  ),
);

Note: WebSocket clients from browsers cannot set custom headers during initial handshake. This is best used for server-to-server communication or non-browser clients.

3. RequestIPAuthenticator

Restricts access based on IP address whitelist:

final server = Server(
  requestAuthenticationDelegate: RequestIPAuthenticator(
    allowedIPs: {
      '127.0.0.1',      // Localhost
      '192.168.1.100',  // Specific device
      '10.0.0.50',      // Another device
    },
  ),
);
4. MultiRequestAuthenticator

Combines multiple authenticators - all must pass:

final server = Server(
  requestAuthenticationDelegate: MultiRequestAuthenticator([
    RequestTokenAuthenticator(validTokens: {'secret123'}),
    RequestIPAuthenticator(allowedIPs: {'127.0.0.1', '192.168.1.100'}),
  ]),
);

In this example, clients must provide a valid token AND connect from an allowed IP.

Custom Authentication

Create your own authenticator by implementing the interface:

class CustomAuthenticator implements RequestAuthenticationDelegate {
  final String apiKey;
  
  const CustomAuthenticator({required this.apiKey});
  
  @override
  Future<RequestAuthenticationResult> authenticateRequest(Request request) async {
    final providedKey = request.url.queryParameters['api_key'];
    
    if (providedKey == null) {
      return RequestAuthenticationResult.failure(
        reason: 'Missing API key',
        statusCode: 401,
      );
    }
    
    // Validate against database, external API, etc.
    final isValid = await validateApiKey(providedKey);
    
    if (!isValid) {
      return RequestAuthenticationResult.failure(
        reason: 'Invalid API key',
        statusCode: 403,
      );
    }
    
    return RequestAuthenticationResult.success(
      metadata: {'apiKey': providedKey},
    );
  }
  
  Future<bool> validateApiKey(String key) async {
    // Your validation logic here
    return key == apiKey;
  }
}

// Usage
final server = Server(
  requestAuthenticationDelegate: CustomAuthenticator(apiKey: 'my-secret-key'),
);

RequestAuthenticationResult

The result object provides factory methods for common responses:

// Success - allow connection
RequestAuthenticationResult.success(metadata: {'user': 'john'});

// Success - simple allow
RequestAuthenticationResult.allow();

// Failure - custom reason and status code
RequestAuthenticationResult.failure(
  reason: 'Invalid credentials',
  statusCode: 403,
);

// Failure - simple deny
RequestAuthenticationResult.deny();

ClientValidationDelegate #

Validates clients after the WebSocket connection is established but before they're added to the active clients list.

Interface

abstract class ClientValidationDelegate {
  FutureOr<bool> validateClient(Client client, Request request);
}

Use Cases

  • Check if client details are valid
  • Enforce maximum client limits
  • Validate client metadata
  • Check client against database

Example: Limit Maximum Clients

class MaxClientsValidator implements ClientValidationDelegate {
  final int maxClients;
  final Server server;
  
  MaxClientsValidator({required this.maxClients, required this.server});
  
  @override
  Future<bool> validateClient(Client client, Request request) async {
    if (server.clients.length >= maxClients) {
      print('Server full: ${server.clients.length}/$maxClients');
      return false;
    }
    return true;
  }
}

// Usage
final server = Server();
server.clientValidationDelegate = MaxClientsValidator(
  maxClients: 10,
  server: server,
);

Example: Require Username

class UsernameValidator implements ClientValidationDelegate {
  @override
  Future<bool> validateClient(Client client, Request request) async {
    final username = client.details['username'];
    
    if (username == null || username.isEmpty) {
      print('Client rejected: missing username');
      return false;
    }
    
    if (username.length < 3) {
      print('Client rejected: username too short');
      return false;
    }
    
    return true;
  }
}

// Usage
final server = Server(
  clientValidationDelegate: UsernameValidator(),
);

// Client must provide username
final client = Client(details: {'username': 'Alice'});
await client.connect('ws://127.0.0.1:8080/ws');

ClientConnectionDelegate #

Handles client connection and disconnection events. Perfect for logging, analytics, or triggering side effects.

Interface

abstract interface class ClientConnectionDelegate {
  FutureOr<void> onClientConnected(Client client);
  FutureOr<void> onClientDisconnected(Client client);
}

Example: Connection Logging

class ConnectionLogger implements ClientConnectionDelegate {
  @override
  Future<void> onClientConnected(Client client) async {
    print('βœ… Client connected: ${client.uid}');
    print('   Details: ${client.details}');
    
    // Log to database, send analytics, etc.
    await logConnection(client.uid, client.details);
  }
  
  @override
  Future<void> onClientDisconnected(Client client) async {
    print('❌ Client disconnected: ${client.uid}');
    
    // Cleanup, save state, etc.
    await cleanupClientData(client.uid);
  }
  
  Future<void> logConnection(String uid, Map<String, String> details) async {
    // Your logging logic
  }
  
  Future<void> cleanupClientData(String uid) async {
    // Your cleanup logic
  }
}

// Usage
final server = Server(
  clientConnectionDelegate: ConnectionLogger(),
);

Example: Broadcast Join/Leave Messages

class JoinLeaveAnnouncer implements ClientConnectionDelegate {
  final Server server;
  
  JoinLeaveAnnouncer({required this.server});
  
  @override
  Future<void> onClientConnected(Client client) async {
    final username = client.details['username'] ?? 'Anonymous';
    server.send({
      'type': 'user_joined',
      'username': username,
      'timestamp': DateTime.now().toIso8601String(),
    });
  }
  
  @override
  Future<void> onClientDisconnected(Client client) async {
    final username = client.details['username'] ?? 'Anonymous';
    server.send({
      'type': 'user_left',
      'username': username,
      'timestamp': DateTime.now().toIso8601String(),
    });
  }
}

// Usage
final server = Server();
server.clientConnectionDelegate = JoinLeaveAnnouncer(server: server);

MessageValidationDelegate #

Validates individual messages from clients before broadcasting. This is useful for content filtering, rate limiting, or message format validation.

Interface

abstract class MessageValidationDelegate {
  FutureOr<bool> validateMessage(Client client, String message);
}

Example: Profanity Filter

class ProfanityFilter implements MessageValidationDelegate {
  final Set<String> bannedWords = {'badword1', 'badword2'};
  
  @override
  Future<bool> validateMessage(Client client, String message) async {
    final lowerMessage = message.toString().toLowerCase();
    
    for (final word in bannedWords) {
      if (lowerMessage.contains(word)) {
        print('Blocked message from ${client.uid}: contains profanity');
        return false;
      }
    }
    
    return true;
  }
}

// Usage
final server = Server(
  messageValidationDelegate: ProfanityFilter(),
);

Example: Rate Limiting

class RateLimiter implements MessageValidationDelegate {
  final Map<String, List<DateTime>> _messageTimes = {};
  final int maxMessages;
  final Duration timeWindow;
  
  RateLimiter({
    this.maxMessages = 10,
    this.timeWindow = const Duration(seconds: 10),
  });
  
  @override
  Future<bool> validateMessage(Client client, String message) async {
    final now = DateTime.now();
    final clientId = client.uid;
    
    // Initialize or get message times for this client
    _messageTimes.putIfAbsent(clientId, () => []);
    
    // Remove old messages outside time window
    _messageTimes[clientId]!.removeWhere(
      (time) => now.difference(time) > timeWindow,
    );
    
    // Check if limit exceeded
    if (_messageTimes[clientId]!.length >= maxMessages) {
      print('Rate limit exceeded for ${client.uid}');
      return false;
    }
    
    // Add this message
    _messageTimes[clientId]!.add(now);
    return true;
  }
}

// Usage
final server = Server(
  messageValidationDelegate: RateLimiter(
    maxMessages: 5,
    timeWindow: Duration(seconds: 10),
  ),
);

Example: Message Format Validation

class MessageFormatValidator implements MessageValidationDelegate {
  @override
  Future<bool> validateMessage(Client client, String message) async {
    // Expecting JSON messages
    try {
      final decoded = jsonDecode(message);
      
      if (decoded is! Map) {
        print('Invalid message format: not a map');
        return false;
      }
      
      if (!decoded.containsKey('type')) {
        print('Invalid message format: missing type field');
        return false;
      }
      
      return true;
    } catch (e) {
      print('Invalid message format: not valid JSON');
      return false;
    }
  }
}

// Usage
final server = Server(
  messageValidationDelegate: MessageFormatValidator(),
);

Combining Multiple Delegates #

You can use all delegates together for comprehensive control:

final server = Server(
  echo: false,
  details: {'name': 'Secure Chat Server'},
  
  // 1. Authenticate requests
  requestAuthenticationDelegate: MultiRequestAuthenticator([
    RequestTokenAuthenticator(validTokens: {'secret123'}),
    RequestIPAuthenticator(allowedIPs: {'127.0.0.1'}),
  ]),
  
  // 2. Validate clients
  clientValidationDelegate: UsernameValidator(),
  
  // 3. Handle connections
  clientConnectionDelegate: JoinLeaveAnnouncer(server: server),
  
  // 4. Validate messages
  messageValidationDelegate: MultiMessageValidator([
    ProfanityFilter(),
    RateLimiter(maxMessages: 5, timeWindow: Duration(seconds: 10)),
    MessageFormatValidator(),
  ]),
);

Note: Create a MultiMessageValidator similar to MultiRequestAuthenticator if you need to combine multiple message validators.

Delegate Execution Order #

When a client attempts to connect, delegates are executed in this order:

  1. RequestAuthenticationDelegate - Before WebSocket upgrade

    • If fails: Returns HTTP 401/403, connection rejected
  2. WebSocket Upgrade - Connection established

  3. ClientValidationDelegate - After connection, before adding to clients

    • If fails: WebSocket closed with code 1008, client not added
  4. Client Added - Client joins the active clients set

  5. ClientConnectionDelegate.onClientConnected - After client added

    • Runs asynchronously, doesn't block
  6. Message Loop - For each message:

    • MessageValidationDelegate - Validate message
    • If valid: Broadcast to clients
    • If invalid: Silently drop message
  7. On Disconnect:

    • ClientConnectionDelegate.onClientDisconnected - Cleanup
    • Runs asynchronously, doesn't block

Security Best Practices #

  1. Always use RequestAuthenticationDelegate for authentication (happens before WebSocket upgrade)
  2. Use ClientValidationDelegate for business logic validation (max clients, metadata checks)
  3. Use MessageValidationDelegate for content filtering and rate limiting
  4. Use HTTPS/WSS in production - These delegates don't replace transport security
  5. Validate all input - Don't trust client data
  6. Log authentication failures - Monitor for attacks
  7. Use IP whitelisting carefully - IPs can be spoofed on some networks

Use Cases #

1. Local Multiplayer Games #

Create real-time multiplayer games where players on the same WiFi network can discover and join games.

// Game host creates server
final server = Server(details: {'gameName': 'Chess Match', 'players': 0});
await server.start('0.0.0.0');

// Players scan and join
final games = await Scanner.scan('192.168.1').first;
final client = Client(details: {'playerName': 'Alice'});
await client.connect(games.first.path);

2. LAN Chat Application #

Build a local chat room for devices on the same network.

// Chat server
final server = Server(echo: false, details: {'room': 'General'});
server.messageStream.listen((msg) => print('Message: $msg'));

// Chat client
final client = Client(details: {'username': 'Bob'});
client.messageStream.listen((msg) => print('New message: $msg'));
client.send('Hello everyone!');

3. Device Synchronization #

Sync data between multiple devices without cloud services.

// Device A (server)
final serverDevice = Server(details: {'deviceName': 'Desktop'});
await serverDevice.start('0.0.0.0');

// Device B (client)
final clientDevice = Client(details: {'deviceName': 'Laptop'});
await clientDevice.connect(discoveredServer.path);
clientDevice.send({'syncData': [...]}); // Share data

4. IoT Device Discovery #

Discover and communicate with IoT devices on the local network.

// IoT device runs server
final iotServer = Server(details: {
  'deviceType': 'SmartLight',
  'firmwareVersion': '2.0.1',
});

// Control app scans for devices
await for (final devices in Scanner.scan('192.168.1')) {
  for (final device in devices) {
    if (device.details['deviceType'] == 'SmartLight') {
      // Connect and control
    }
  }
}

5. File Sharing #

Share files between devices on the same network.

// Sender
final sender = Server(details: {'sharing': 'documents.pdf'});
server.messageStream.listen((request) {
  // Send file chunks
  server.send(fileData);
});

// Receiver
final receiver = Client();
receiver.messageStream.listen((chunk) {
  // Receive and reconstruct file
});

Examples #

Complete Chat Application #

import 'package:local_websocket/local_websocket.dart';

void main() async {
  print('Choose mode: (1) Server or (2) Client');
  // In real app, get user input
  final mode = 1; // Example: server mode
  
  if (mode == 1) {
    // Server mode
    final server = Server(
      echo: false,
      details: {'chatRoom': 'Main Lobby'},
    );
    
    await server.start('0.0.0.0', port: 8080);
    print('Chat server started at ${server.address}');
    
    server.clientsStream.listen((clients) {
      print('Users online: ${clients.length}');
    });
    
    server.messageStream.listen((message) {
      print('Message received: $message');
    });
    
  } else {
    // Client mode
    print('Scanning for chat servers...');
    final servers = await Scanner.scan('192.168.1').first;
    
    if (servers.isEmpty) {
      print('No servers found');
      return;
    }
    
    final client = Client(details: {'username': 'Alice'});
    await client.connect(servers.first.path);
    print('Connected to chat!');
    
    client.messageStream.listen((message) {
      print('Message: $message');
    });
    
    // Send messages
    client.send('Hello everyone!');
  }
}

Real-time Game Example #

import 'package:local_websocket/local_websocket.dart';

class GameServer {
  final server = Server(
    echo: false,
    details: {
      'gameName': 'Tic Tac Toe',
      'maxPlayers': 2,
      'status': 'waiting',
    },
  );
  
  Future<void> start() async {
    await server.start('0.0.0.0', port: 8080);
    
    server.messageStream.listen((message) {
      if (message is Map) {
        handleGameAction(message);
      }
    });
  }
  
  void handleGameAction(Map action) {
    // Process game logic
    final response = {'type': 'gameUpdate', 'board': [...]};
    server.send(response); // Broadcast to all players
  }
}

class GameClient {
  final client = Client(details: {'playerName': 'Bob'});
  
  Future<void> join() async {
    final games = await Scanner.scan('192.168.1').first;
    final gameServer = games.firstWhere(
      (s) => s.details['gameName'] == 'Tic Tac Toe',
    );
    
    await client.connect(gameServer.path);
    
    client.messageStream.listen((message) {
      if (message is Map && message['type'] == 'gameUpdate') {
        updateGameBoard(message['board']);
      }
    });
  }
  
  void makeMove(int x, int y) {
    client.send({'type': 'move', 'x': x, 'y': y});
  }
  
  void updateGameBoard(dynamic board) {
    // Update UI
  }
}

Architecture #

How It Works #

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     Server      β”‚
β”‚  (0.0.0.0:8080) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β”‚  HTTP GET /  β†’ Returns server details (JSON)
         β”‚  WS /ws      β†’ WebSocket endpoint
         β”‚
    β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”
    β”‚         β”‚
β”Œβ”€β”€β”€β–Όβ”€β”€β”€β” β”Œβ”€β”€β–Όβ”€β”€β”€β”€β”
β”‚Client1β”‚ β”‚Client2β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”˜

Server Responsibilities:

  1. Listen for HTTP requests at / (returns server details)
  2. Accept WebSocket connections at /ws
  3. Manage connected clients
  4. Route messages between clients (broadcast or echo)
  5. Emit streams for clients, connections, and messages

Client Responsibilities:

  1. Connect to server's WebSocket endpoint
  2. Send messages to server
  3. Receive messages from server
  4. Emit streams for messages and connection status
  5. Include metadata via query parameters

Scanner Responsibilities:

  1. Generate IP addresses in subnet range
  2. Send HTTP GET requests to each IP
  3. Check Server header for local-websocket
  4. Parse response JSON for server details
  5. Return list of discovered servers
  6. Repeat scan at specified intervals

Best Practices #

1. Error Handling #

Always wrap server/client operations in try-catch:

try {
  await server.start('0.0.0.0', port: 8080);
} catch (e) {
  print('Failed to start server: $e');
}

try {
  await client.connect('ws://127.0.0.1:8080/ws');
} catch (e) {
  print('Failed to connect: $e');
}

2. Resource Cleanup #

Always clean up resources when done:

// Stop server
await server.stop();

// Disconnect clients
await client.disconnect();

// Cancel stream subscriptions
subscription.cancel();

3. Message Validation #

Validate incoming messages:

client.messageStream.listen((message) {
  if (message is! Map) {
    print('Invalid message format');
    return;
  }
  
  if (!message.containsKey('type')) {
    print('Message missing type field');
    return;
  }
  
  // Process valid message
});

4. Connection State Management #

Track connection state:

bool isConnected = false;

client.connectionStream.listen((connected) {
  isConnected = connected;
  if (!connected) {
    // Handle disconnection, try reconnect
  }
});

5. Scanning Optimization #

Don't scan continuously if you don't need to:

// One-time scan
final servers = await Scanner.scan('192.168.1').first;

// Limited scanning
final subscription = Scanner.scan('192.168.1').listen((servers) {
  if (servers.isNotEmpty) {
    subscription.cancel(); // Stop scanning once found
  }
});

6. Server Details Best Practices #

Include useful metadata:

final server = Server(
  details: {
    'name': 'My Server',
    'version': '1.0.0',
    'maxClients': 10,
    'requiresAuth': false,
    'description': 'A friendly server',
  },
);

Troubleshooting #

Problem: Scanner doesn't find any servers #

Solutions:

  1. Verify server is running: Check server.isConnected
  2. Check firewall: Ensure port is not blocked
  3. Verify correct subnet: Use ipconfig (Windows) or ifconfig (Mac/Linux) to find your subnet
  4. Check port: Ensure scanner and server use the same port
  5. Wait longer: Scanner needs time to check all IPs
// Debug: Check if server is accessible via HTTP
final response = await http.get(Uri.parse('http://127.0.0.1:8080/'));
print(response.body); // Should print server details

Problem: Client can't connect #

Solutions:

  1. Verify WebSocket URL format: Must start with ws://
  2. Check server address: Use IP address instead of hostname
  3. Ensure server is running on correct interface (use 0.0.0.0 for all interfaces)
// Correct format
await client.connect('ws://192.168.1.100:8080/ws');

// Incorrect formats
await client.connect('http://192.168.1.100:8080/ws'); // Wrong scheme
await client.connect('192.168.1.100:8080/ws'); // Missing scheme

Problem: Messages not being received #

Solutions:

  1. Check echo mode: If echo: false, sender won't receive their own messages
  2. Verify message format: Ensure messages are JSON-encodable
  3. Check stream subscriptions: Ensure you're listening to messageStream
// Always listen BEFORE sending
client.messageStream.listen((msg) => print(msg));
await Future.delayed(Duration(milliseconds: 100)); // Let subscription establish
client.send('Hello');

Problem: StateError when starting/stopping #

Solutions:

  1. Don't start an already running server
  2. Don't stop an already stopped server
  3. Check isConnected before operations
if (!server.isConnected) {
  await server.start('0.0.0.0');
}

if (server.isConnected) {
  await server.stop();
}

Problem: Port already in use #

Solutions:

  1. Use different port
  2. Stop other application using the port
  3. Wait a few seconds after stopping server before restarting
await server.start('0.0.0.0', port: 8081); // Try different port

Contributing #

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License #

This project is licensed under the MIT License - see the LICENSE file for details.


Author #

Created by Ehsan Rashidi


Support #

If you find this package helpful, please give it a ⭐️ on GitHub!

DiscoveredServer #

String path                 // WebSocket connection path
Map<String, dynamic> details // Server details/metadata

Network Scanning #

The scanner automatically detects servers by:

  1. HTTP Header Detection - Looks for Server: local-websocket/* header
  2. Response Validation - Verifies JSON response format
  3. Subnet Resolution - Automatically resolves network subnets

Supported Host Formats #

// Localhost scanning
await Scanner.scan('localhost');       // Scans 127.0.0.*
await Scanner.scan('127.0.0.1');      // Scans 127.0.0.*

// Network subnet scanning  
await Scanner.scan('192.168.1');      // Scans 192.168.1.*
await Scanner.scan('10.0.0');         // Scans 10.0.0.*

Error Handling #

try {
  final server = Server();
  await server.start(host: 'localhost');
  
  final client = Client();
  await client.connect('ws://localhost:8080/ws');
  
} on StateError catch (e) {
  print('State error: $e'); // Server already running, client already connected, etc.
} on ArgumentError catch (e) {
  print('Invalid argument: $e'); // Invalid host format, etc.
} catch (e) {
  print('Network error: $e'); // Connection failed, timeout, etc.
}
4
likes
0
points
269
downloads

Publisher

unverified uploader

Weekly Downloads

A pure Dart library for local network WebSocket communication with automatic server discovery and scanning capabilities.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

dio, shelf, shelf_router, shelf_web_socket, uuid, web_socket_channel

More

Packages that depend on local_websocket