dart_libp2p 0.5.3
A comprehensive Dart implementation of the libp2p networking stack with modular architecture, multiple transports (TCP/UDX), Noise security, and peer-to-peer capabilities.
STOMP Protocol Implementation for dart-libp2p
This directory contains a complete implementation of the STOMP (Simple Text Oriented Messaging Protocol) version 1.2 for the dart-libp2p library. STOMP is a simple interoperable protocol designed for asynchronous message passing between clients via mediating servers.
Overview
The STOMP implementation provides both client and server functionality, allowing libp2p peers to communicate using the STOMP messaging protocol over libp2p streams. This enables reliable message passing, publish-subscribe patterns, and transactional messaging between peers in a libp2p network.
Features
- Full STOMP 1.2 Compliance: Implements the complete STOMP 1.2 specification
- Client and Server Support: Both client and server implementations
- Message Acknowledgments: Support for auto, client, and client-individual acknowledgment modes
- Transactions: Full transaction support with BEGIN, COMMIT, and ABORT
- Subscriptions: Subscribe to destinations and receive messages
- Receipt Handling: Optional receipt confirmation for reliable operations
- Frame Validation: Comprehensive frame validation and error handling
- Escape Sequences: Proper handling of STOMP escape sequences in headers
- Size Limits: Configurable limits for frame size, headers, and body
- Timeout Support: Configurable timeouts for all operations
Architecture
The implementation follows the libp2p pattern of separating interfaces and implementations:
Core Components
stomp_frame.dart: Frame parsing, serialization, and validationstomp_constants.dart: Protocol constants and configurationstomp_exceptions.dart: Exception hierarchy for error handlingstomp_subscription.dart: Subscription and acknowledgment managementstomp_transaction.dart: Transaction management and coordination
Client/Server Components
stomp_client.dart: STOMP client implementationstomp_server.dart: STOMP server implementationstomp_service.dart: High-level service combining client and server
Usage
Basic Setup
import 'package:dart_libp2p/p2p/protocol/stomp.dart';
// Create a STOMP service with both client and server capabilities
final stompService = await host.addStompService(
options: const StompServiceOptions.serverEnabled(
serverName: 'my-stomp-server/1.0',
),
);
Client Usage
// Connect to a STOMP server
final client = await host.connectStomp(
peerId: serverPeerId,
hostName: 'example.com',
login: 'username', // optional
passcode: 'password', // optional
);
// Send a message
await client.send(
destination: '/queue/messages',
body: 'Hello, STOMP!',
contentType: 'text/plain',
requestReceipt: true,
);
// Subscribe to a destination
final subscription = await client.subscribe(
destination: '/topic/news',
ackMode: StompAckMode.client,
);
// Listen for messages
subscription.messages.listen((message) async {
print('Received: ${message.body}');
// Acknowledge the message
if (message.requiresAck) {
await client.ack(messageId: message.ackId!);
}
});
Server Usage
// The server is automatically started with the service
final server = stompService.server!;
// Listen for new connections
server.onConnection.listen((connection) {
print('New client connected: ${connection.peerId}');
});
// Send a message to a destination
await server.sendToDestination(
destination: '/topic/announcements',
body: 'Server announcement',
contentType: 'text/plain',
);
// Broadcast to all connected clients
await server.broadcast(
body: 'System maintenance in 5 minutes',
contentType: 'text/plain',
);
Transactions
// Begin a transaction
final transaction = await client.beginTransaction();
try {
// Send multiple messages in the transaction
await client.send(
destination: '/queue/orders',
body: 'Order #1',
transactionId: transaction.id,
);
await client.send(
destination: '/queue/orders',
body: 'Order #2',
transactionId: transaction.id,
);
// Commit the transaction
await client.commitTransaction(transactionId: transaction.id);
} catch (e) {
// Abort on error
await client.abortTransaction(transactionId: transaction.id);
}
Protocol Details
Frame Structure
STOMP frames follow this structure:
COMMAND
header1:value1
header2:value2
Body^@
- Command line terminated by LF (or CRLF)
- Headers in
key:valueformat, one per line - Empty line separates headers from body
- Body terminated by NULL byte (^@)
Supported Commands
Client Commands
CONNECT/STOMP- Connect to serverSEND- Send a messageSUBSCRIBE- Subscribe to destinationUNSUBSCRIBE- Unsubscribe from destinationACK- Acknowledge messageNACK- Negative acknowledge messageBEGIN- Begin transactionCOMMIT- Commit transactionABORT- Abort transactionDISCONNECT- Disconnect from server
Server Commands
CONNECTED- Connection acknowledgmentMESSAGE- Message deliveryRECEIPT- Receipt confirmationERROR- Error notification
Destinations
Destinations are opaque strings that identify message endpoints:
/queue/name- Point-to-point queues/topic/name- Publish-subscribe topics/temp/name- Temporary destinations
Use StompUtils for destination management:
final queueDest = StompUtils.createQueueDestination('orders');
final topicDest = StompUtils.createTopicDestination('news');
final tempDest = StompUtils.createTempDestination('session123');
Acknowledgment Modes
auto: Automatic acknowledgment (default)client: Manual acknowledgment (cumulative)client-individual: Manual acknowledgment (per message)
Error Handling
The implementation provides a comprehensive exception hierarchy:
StompException- Base exceptionStompFrameException- Frame parsing/validation errorsStompConnectionException- Connection errorsStompTimeoutException- Timeout errorsStompServerErrorException- Server-sent errors- And more specific exceptions for different scenarios
Configuration
Service Options
const options = StompServiceOptions(
enableServer: true, // Enable server functionality
serverName: 'my-server/1.0', // Server identification
timeout: Duration(seconds: 30), // Default operation timeout
enableAutoReconnect: false, // Auto-reconnect clients
reconnectInterval: Duration(seconds: 5), // Reconnect interval
maxReconnectAttempts: 3, // Max reconnect attempts
);
Protocol Constants
Key constants can be found in StompConstants:
maxFrameSize: 64KB default frame size limitmaxHeaders: 100 headers per frame limitmaxHeaderLength: 1KB header length limitmaxBodySize: 8MB body size limitdefaultTimeout: 30 seconds default timeout
P2P Integration
Connection Management
The STOMP implementation leverages libp2p's connection infrastructure:
// STOMP uses libp2p streams for transport
final client = await host.connectStomp(
peerId: targetPeer,
hostName: 'peer-to-peer',
);
// Automatic reconnection on stream failures
client.onStateChange.listen((state) {
if (state == StompClientState.error) {
// Library handles automatic reconnection
print('Connection lost, reconnecting...');
}
});
Peer Discovery Integration
STOMP works with libp2p's peer discovery mechanisms:
// Example: Connect to discovered peers
host.network.onPeerConnect.listen((peerId) async {
try {
final client = await host.connectStomp(
peerId: peerId,
hostName: 'discovered-peer',
);
// Subscribe to peer's announcements
await client.subscribe(
destination: '/peer/${peerId}/announcements',
ackMode: StompAckMode.auto,
);
} catch (e) {
print('Failed to establish STOMP connection: $e');
}
});
Stream Lifecycle Management
- Automatic stream reconnection on failures
- Integration with libp2p's connection manager
- Proper resource cleanup on disconnection
- Support for libp2p context and scoping
Destination Patterns
Recommended Conventions
The library doesn't enforce destination patterns, but here are recommended conventions for P2P networks:
// Direct peer messaging
'/peer/{peerId}/inbox' // Personal inbox
'/peer/{peerId}/queue/{name}' // Peer-specific queues
'/peer/{peerId}/rpc/{service}' // RPC endpoints
// Network-wide destinations
'/network/topic/{name}' // Global topics
'/network/broadcast' // Network broadcasts
'/network/events/{type}' // Event streams
// Local destinations
'/local/queue/{name}' // Local-only queues
'/local/temp/{session}' // Temporary destinations
// Service-oriented destinations
'/service/{name}/requests' // Service requests
'/service/{name}/responses' // Service responses
'/service/{name}/events' // Service events
// Group-based destinations
'/group/{groupId}/topic/{name}' // Group topics
'/group/{groupId}/chat' // Group chat
Custom Destination Resolution
Applications can implement custom destination resolution:
class P2PDestinationResolver {
final Host host;
final Map<String, PeerId> serviceRegistry = {};
P2PDestinationResolver(this.host);
/// Resolve a destination to a target peer
PeerId? resolvePeer(String destination) {
// Direct peer destinations
if (destination.startsWith('/peer/')) {
final parts = destination.split('/');
if (parts.length >= 3) {
return PeerId.fromString(parts[2]);
}
}
// Service destinations
if (destination.startsWith('/service/')) {
final parts = destination.split('/');
if (parts.length >= 3) {
return serviceRegistry[parts[2]];
}
}
// Network destinations - could use DHT or broadcast
if (destination.startsWith('/network/')) {
return null; // Indicates broadcast needed
}
return null;
}
/// Register a service provider
void registerService(String serviceName, PeerId provider) {
serviceRegistry[serviceName] = provider;
}
}
Destination Utilities
Use the built-in utilities for common patterns:
// Create standardized destinations
final peerInbox = '/peer/${peerId}/inbox';
final networkTopic = StompUtils.createTopicDestination('global-events');
final serviceEndpoint = '/service/user-management/requests';
// Validate destination format
if (StompUtils.isValidDestination(destination)) {
await client.send(destination: destination, body: message);
}
// Determine destination type
final type = StompUtils.getDestinationType(destination);
switch (type) {
case StompDestinationType.topic:
// Handle topic subscription
break;
case StompDestinationType.queue:
// Handle queue messaging
break;
}
Multi-Peer Messaging Patterns
Direct Peer Communication
// Connect to specific peer and send direct message
final targetPeer = PeerId.fromString('12D3KooW...');
final client = await host.connectStomp(
peerId: targetPeer,
hostName: 'direct-messaging',
);
await client.send(
destination: '/peer/${targetPeer}/inbox',
body: 'Direct message to peer',
contentType: 'text/plain',
);
Network Broadcasting
Application-level broadcast implementation:
class NetworkBroadcaster {
final StompService stompService;
final Set<PeerId> connectedPeers = {};
NetworkBroadcaster(this.stompService) {
// Track connected peers
stompService.server?.onConnection.listen((connection) {
connectedPeers.add(connection.peerId);
});
stompService.server?.onDisconnection.listen((connection) {
connectedPeers.remove(connection.peerId);
});
}
/// Broadcast message to all connected peers
Future<void> broadcast(String message, {String? contentType}) async {
final futures = <Future<void>>[];
for (final peerId in connectedPeers) {
futures.add(_sendToPeer(peerId, message, contentType));
}
// Wait for all sends to complete
final results = await Future.wait(
futures,
eagerError: false, // Don't fail on individual peer errors
);
print('Broadcast sent to ${results.length} peers');
}
Future<void> _sendToPeer(PeerId peerId, String message, String? contentType) async {
try {
final client = stompService.getClient(peerId);
if (client?.isConnected == true) {
await client!.send(
destination: '/network/broadcast',
body: message,
contentType: contentType,
);
}
} catch (e) {
print('Failed to send to $peerId: $e');
}
}
}
Message Routing
Example of application-level message routing:
class MessageRouter {
final StompService stompService;
final P2PDestinationResolver resolver;
MessageRouter(this.stompService, this.resolver) {
// Listen for messages that need routing
stompService.server?.onMessage.listen(_routeMessage);
}
Future<void> _routeMessage(StompMessage message) async {
final targetPeer = resolver.resolvePeer(message.destination);
if (targetPeer == null) {
// Network-wide destination - broadcast
await _broadcastMessage(message);
} else if (targetPeer != stompService._host.id) {
// Forward to specific peer
await _forwardMessage(message, targetPeer);
}
// If targetPeer is us, message is already delivered locally
}
Future<void> _forwardMessage(StompMessage message, PeerId targetPeer) async {
try {
final client = await stompService.connect(
peerId: targetPeer,
hostName: 'message-router',
);
await client.send(
destination: message.destination,
body: message.body,
contentType: message.contentType,
headers: message.headers,
);
} catch (e) {
print('Failed to forward message to $targetPeer: $e');
}
}
Future<void> _broadcastMessage(StompMessage message) async {
// Implementation depends on network topology
// Could use DHT, gossip protocol, or known peer list
}
}
Service Discovery Pattern
class StompServiceRegistry {
final StompService stompService;
final Map<String, Set<PeerId>> services = {};
StompServiceRegistry(this.stompService) {
_setupServiceDiscovery();
}
void _setupServiceDiscovery() {
// Listen for service announcements
stompService.server?.onMessage.listen((message) {
if (message.destination.startsWith('/network/services/announce')) {
_handleServiceAnnouncement(message);
}
});
}
/// Register a service
Future<void> registerService(String serviceName) async {
await stompService.server?.sendToDestination(
destination: '/network/services/announce',
body: serviceName,
headers: {'peer-id': stompService._host.id.toString()},
);
}
/// Find providers for a service
Set<PeerId> findServiceProviders(String serviceName) {
return services[serviceName] ?? {};
}
void _handleServiceAnnouncement(StompMessage message) {
final serviceName = message.body;
final peerIdStr = message.getHeader('peer-id');
if (serviceName != null && peerIdStr != null) {
final peerId = PeerId.fromString(peerIdStr);
services.putIfAbsent(serviceName, () => {}).add(peerId);
}
}
}
Network Resilience
Library-Provided Features
The STOMP library provides several resilience features:
- Automatic Stream Reconnection: Handles libp2p stream failures
- Connection State Monitoring: Track connection health
- Timeout Handling: Configurable timeouts for all operations
- Resource Cleanup: Proper cleanup on failures
- Transaction Rollback: Automatic rollback on connection loss
// Monitor connection health
client.onStateChange.listen((state) {
switch (state) {
case StompClientState.connected:
print('Connection established');
break;
case StompClientState.disconnected:
print('Connection lost');
break;
case StompClientState.error:
print('Connection error - will retry');
break;
}
});
Application-Level Resilience
Applications should implement additional resilience patterns:
class ResilientMessaging {
final StompClient client;
final int maxRetries;
final Duration baseDelay;
ResilientMessaging(this.client, {
this.maxRetries = 3,
this.baseDelay = const Duration(seconds: 1),
});
/// Send message with exponential backoff retry
Future<void> sendWithRetry(String destination, String message) async {
var attempts = 0;
while (attempts < maxRetries) {
try {
await client.send(
destination: destination,
body: message,
requestReceipt: true, // Ensure delivery confirmation
);
return; // Success
} catch (e) {
attempts++;
if (attempts >= maxRetries) {
throw StompException('Failed after $maxRetries attempts: $e');
}
// Exponential backoff
final delay = baseDelay * pow(2, attempts - 1);
await Future.delayed(delay);
}
}
}
/// Subscribe with automatic resubscription
Future<StompSubscription> subscribeWithReconnect(
String destination, {
StompAckMode ackMode = StompAckMode.auto,
}) async {
late StompSubscription subscription;
Future<void> subscribe() async {
subscription = await client.subscribe(
destination: destination,
ackMode: ackMode,
);
}
// Initial subscription
await subscribe();
// Resubscribe on reconnection
client.onStateChange.listen((state) async {
if (state == StompClientState.connected) {
try {
await subscribe();
} catch (e) {
print('Failed to resubscribe to $destination: $e');
}
}
});
return subscription;
}
}
Network Partition Handling
class PartitionAwareMessaging {
final StompService stompService;
final List<StompMessage> pendingMessages = [];
PartitionAwareMessaging(this.stompService) {
_setupPartitionDetection();
}
void _setupPartitionDetection() {
// Monitor peer connections
stompService.server?.onDisconnection.listen((connection) {
_handlePeerDisconnection(connection.peerId);
});
}
/// Send message with partition awareness
Future<void> sendMessage(PeerId targetPeer, String destination, String message) async {
try {
final client = stompService.getClient(targetPeer);
if (client?.isConnected == true) {
await client!.send(destination: destination, body: message);
} else {
// Queue for later delivery
pendingMessages.add(StompMessage(
messageId: _generateId(),
destination: destination,
subscriptionId: '',
headers: {'target-peer': targetPeer.toString()},
body: message,
));
}
} catch (e) {
print('Failed to send message: $e');
// Could implement dead letter queue here
}
}
void _handlePeerDisconnection(PeerId peerId) {
print('Peer $peerId disconnected - messages queued for retry');
// Could implement message persistence here
}
String _generateId() => DateTime.now().millisecondsSinceEpoch.toString();
}
Integration with libp2p
The STOMP implementation integrates seamlessly with libp2p:
- Uses libp2p streams for transport
- Follows libp2p protocol negotiation
- Integrates with libp2p peer discovery
- Uses libp2p security and multiplexing
- Supports libp2p context and scoping
Thread Safety
The implementation is designed to be thread-safe:
- All managers use proper synchronization
- Stream operations are properly coordinated
- State changes are atomic where required
- Concurrent operations are supported
Performance Considerations
- Frame parsing is optimized for common cases
- Memory usage is bounded by configurable limits
- Connection pooling reduces overhead
- Efficient subscription routing
Production Deployment
Peer Discovery Strategy
Choose appropriate peer discovery mechanisms for your network:
// Local network discovery
final mdnsService = MdnsDiscovery();
await host.discovery.addService(mdnsService);
// Bootstrap peers for wide area networks
final bootstrapPeers = [
PeerId.fromString('12D3KooW...'),
PeerId.fromString('12D3KooW...'),
];
// DHT for peer discovery
final dhtService = KademliaDHT();
await host.discovery.addService(dhtService);
Message Persistence
The library provides in-memory messaging only. For production, consider:
class PersistentStompService {
final StompService stompService;
final MessageStore messageStore;
PersistentStompService(this.stompService, this.messageStore) {
_setupPersistence();
}
void _setupPersistence() {
// Persist outgoing messages
stompService.server?.onMessage.listen((message) async {
await messageStore.store(message);
});
// Handle offline peer messages
stompService.server?.onDisconnection.listen((connection) async {
final queuedMessages = await messageStore.getQueuedMessages(connection.peerId);
// Store for later delivery
});
}
}
abstract class MessageStore {
Future<void> store(StompMessage message);
Future<List<StompMessage>> getQueuedMessages(PeerId peerId);
Future<void> markDelivered(String messageId);
}
Monitoring and Metrics
Implement comprehensive monitoring:
class StompMetrics {
final StompService stompService;
int messagesRouted = 0;
int connectionsActive = 0;
final Map<String, int> destinationCounts = {};
StompMetrics(this.stompService) {
_setupMetrics();
}
void _setupMetrics() {
stompService.server?.onMessage.listen((message) {
messagesRouted++;
destinationCounts[message.destination] =
(destinationCounts[message.destination] ?? 0) + 1;
});
stompService.server?.onConnection.listen((_) {
connectionsActive++;
});
stompService.server?.onDisconnection.listen((_) {
connectionsActive--;
});
}
Map<String, dynamic> getMetrics() {
return {
'messages_routed': messagesRouted,
'connections_active': connectionsActive,
'destination_counts': destinationCounts,
'service_stats': stompService.getStats(),
};
}
}
Security Considerations
Implement appropriate security measures:
class SecureStompService {
final StompService stompService;
final Set<PeerId> allowedPeers;
SecureStompService(this.stompService, this.allowedPeers) {
_setupSecurity();
}
void _setupSecurity() {
// Connection filtering
stompService.server?.onConnection.listen((connection) {
if (!allowedPeers.contains(connection.peerId)) {
connection.close();
print('Rejected connection from unauthorized peer: ${connection.peerId}');
}
});
// Message filtering
stompService.server?.onMessage.listen((message) {
if (!_isAuthorizedDestination(message.destination)) {
print('Blocked unauthorized destination: ${message.destination}');
return;
}
});
}
bool _isAuthorizedDestination(String destination) {
// Implement your authorization logic
return !destination.startsWith('/admin/');
}
}
Load Balancing
For high-throughput scenarios:
class LoadBalancedStompService {
final List<StompService> services;
int _currentIndex = 0;
LoadBalancedStompService(this.services);
StompService getNextService() {
final service = services[_currentIndex];
_currentIndex = (_currentIndex + 1) % services.length;
return service;
}
Future<void> sendMessage(String destination, String message) async {
final service = getNextService();
await service.server?.sendToDestination(
destination: destination,
body: message,
);
}
}
Configuration Management
Use environment-based configuration:
class StompConfig {
final String serverName;
final Duration timeout;
final int maxConnections;
final bool enableMetrics;
final List<String> allowedDestinations;
StompConfig({
required this.serverName,
this.timeout = const Duration(seconds: 30),
this.maxConnections = 1000,
this.enableMetrics = true,
this.allowedDestinations = const [],
});
factory StompConfig.fromEnvironment() {
return StompConfig(
serverName: Platform.environment['STOMP_SERVER_NAME'] ?? 'dart-libp2p-stomp',
timeout: Duration(
seconds: int.parse(Platform.environment['STOMP_TIMEOUT'] ?? '30'),
),
maxConnections: int.parse(Platform.environment['STOMP_MAX_CONNECTIONS'] ?? '1000'),
enableMetrics: Platform.environment['STOMP_ENABLE_METRICS'] == 'true',
);
}
}
Common Usage Patterns
Chat Application
class P2PChatService {
final StompService stompService;
final String userId;
P2PChatService(this.stompService, this.userId);
Future<void> sendMessage(String recipientId, String message) async {
await stompService.sendMessage(
peerId: PeerId.fromString(recipientId),
destination: '/peer/$recipientId/chat',
body: jsonEncode({
'from': userId,
'message': message,
'timestamp': DateTime.now().toIso8601String(),
}),
contentType: 'application/json',
);
}
Future<void> joinChatRoom(String roomId) async {
// Subscribe to room messages
await stompService.subscribe(
peerId: PeerId.fromString(roomId), // Room server peer
destination: '/room/$roomId/messages',
ackMode: StompAckMode.auto,
);
}
}
Event Distribution
class EventDistributor {
final StompService stompService;
final Set<PeerId> subscribers = {};
EventDistributor(this.stompService);
Future<void> publishEvent(String eventType, Map<String, dynamic> data) async {
final eventData = jsonEncode({
'type': eventType,
'data': data,
'timestamp': DateTime.now().toIso8601String(),
});
for (final subscriber in subscribers) {
try {
await stompService.sendMessage(
peerId: subscriber,
destination: '/events/$eventType',
body: eventData,
contentType: 'application/json',
);
} catch (e) {
print('Failed to send event to $subscriber: $e');
}
}
}
void addSubscriber(PeerId peerId) {
subscribers.add(peerId);
}
}
RPC over STOMP
class StompRpcService {
final StompService stompService;
final Map<String, Completer<String>> pendingRequests = {};
StompRpcService(this.stompService) {
_setupResponseHandler();
}
void _setupResponseHandler() {
// Listen for RPC responses
stompService.server?.onMessage.listen((message) {
if (message.destination.startsWith('/rpc/response/')) {
final requestId = message.destination.split('/').last;
final completer = pendingRequests.remove(requestId);
completer?.complete(message.body ?? '');
}
});
}
Future<String> callRemoteMethod(
PeerId targetPeer,
String method,
Map<String, dynamic> params,
) async {
final requestId = _generateRequestId();
final completer = Completer<String>();
pendingRequests[requestId] = completer;
await stompService.sendMessage(
peerId: targetPeer,
destination: '/rpc/request/$method',
body: jsonEncode({
'id': requestId,
'method': method,
'params': params,
'responseDestination': '/rpc/response/$requestId',
}),
contentType: 'application/json',
);
return completer.future.timeout(const Duration(seconds: 30));
}
String _generateRequestId() =>
'req_${DateTime.now().millisecondsSinceEpoch}_${Random().nextInt(1000)}';
}
Testing
See the example/stomp_example.dart file for a comprehensive example demonstrating all features of the STOMP implementation.
Compliance
This implementation follows the STOMP 1.2 specification:
- Full frame format compliance
- Proper escape sequence handling
- Complete command set support
- Correct error handling
- Specification-compliant timeouts
Future Enhancements
Potential future improvements:
- Heart-beat implementation
- Message persistence
- Advanced routing patterns
- Metrics and monitoring
- Performance optimizations
- Additional destination types
Contributing
When contributing to the STOMP implementation:
- Follow the existing code patterns
- Add comprehensive tests
- Update documentation
- Ensure STOMP 1.2 compliance
- Consider backward compatibility