Annotation:
@WebSocket
WebSockets enable real-time, bidirectional communication between client and server. Unlike HTTP's request-response pattern, WebSockets allow both sides to send messages at any time.
Creating a WebSocket Handler#
Create a WebSocket handler by annotating a method with @WebSocket:
import 'package:revali_router/revali_router.dart';
@Controller('chat')
class ChatController {
const ChatController();
@WebSocket('messages')
String handleMessage(@Body() String message) {
return 'Echo: $message';
}
}
WebSocket Modes#
Two-Way Communication (Default)#
Both client and server can send messages:
@WebSocket('messages', mode: WebSocketMode.twoWay)
String handleMessage(@Body() String message) {
return 'Echo: $message';
}
Receive-Only#
Server only receives messages from client:
@WebSocket('messages', mode: WebSocketMode.receiveOnly)
void handleMessage(@Body() String message) {
// Process message without sending response
print('Received: $message');
}
Send-Only#
Server only sends messages to client:
@WebSocket('notifications', mode: WebSocketMode.sendOnly)
String sendNotification() {
return 'Server notification';
}
Handling Messages#
Basic Message Handling#
@WebSocket('chat')
String handleChatMessage(@Body() String message) {
// Process the message
final processedMessage = processMessage(message);
// Return response to client
return 'Server: $processedMessage';
}
JSON Messages#
class ChatMessage {
const ChatMessage({required this.user, required this.text});
final String user;
final String text;
factory ChatMessage.fromJson(Map<String, dynamic> json) {
return ChatMessage(
user: json['user'] as String,
text: json['text'] as String,
);
}
}
@WebSocket('chat')
String handleChatMessage(@Body() ChatMessage message) {
return '${message.user}: ${message.text}';
}
Async Message Handling#
@WebSocket('async')
Future<String> handleAsyncMessage(@Body() String message) async {
// Simulate async processing
await Future.delayed(const Duration(seconds: 1));
return 'Processed: $message';
}
Sending Messages Without Client Input#
Use AsyncWebSocketSender to send messages without waiting for client input:
@WebSocket('notifications')
String sendNotifications(AsyncWebSocketSender<String> sender) {
// Send periodic notifications
Timer.periodic(const Duration(seconds: 5), (timer) {
sender.send('Notification ${DateTime.now()}');
});
return 'Notifications started';
}
Connection Management#
Closing Connections#
Send-Only Mode
Connections close automatically when the handler returns:
@WebSocket('send-only', mode: WebSocketMode.sendOnly)
String sendData() {
return 'Final message'; // Connection closes after this
}
Two-Way and Receive-Only Modes
Connections stay open until manually closed:
@WebSocket('persistent', mode: WebSocketMode.twoWay)
String handleMessage(CloseWebSocket closer) {
// Check if connection should close
if (shouldCloseConnection()) {
closer.close(1000, 'Normal closure');
return '';
}
return 'Message processed';
}
Close Codes#
WebSocket close codes follow HTTP status code principles but use the range 1000-4999:
// Normal closure
closer.close(1000, 'Normal closure');
// Going away
closer.close(1001, 'Going away');
// Protocol error
closer.close(1002, 'Protocol error');
// Unsupported data
closer.close(1003, 'Unsupported data');
// Custom application codes (4000-4999)
closer.close(4000, 'Custom application error');
Advanced Features#
Ping/Pong#
Enable automatic ping/pong to keep connections alive:
@WebSocket.ping('heartbeat', ping: Duration(seconds: 30))
String handleHeartbeat(@Body() String? message) {
return 'Pong: ${DateTime.now()}';
}
On Connect Trigger#
Run handler when connection is established:
@WebSocket('connection', triggerOnConnect: true)
String onConnect(@Body() String? message) {
if (message == null) {
return 'Connected to WebSocket';
}
return 'Message: $message';
}
Real-World Examples#
Chat Application#
@Controller('chat')
class ChatController {
final List<String> _messages = [];
@WebSocket('messages')
String handleMessage(@Body() String message) {
_messages.add('${DateTime.now()}: $message');
// Broadcast to all connected clients
broadcastMessage(message);
return 'Message received';
}
void broadcastMessage(String message) {
// Implementation depends on your broadcasting strategy
}
}
Real-Time Notifications#
@Controller('notifications')
class NotificationController {
@WebSocket('stream', mode: WebSocketMode.sendOnly)
String streamNotifications(AsyncWebSocketSender<String> sender) {
// Stream notifications from a service
notificationService.stream.listen((notification) {
sender.send(notification.toJson());
});
return 'Notification stream started';
}
}
File Upload Progress#
@Controller('upload')
class UploadController {
@WebSocket('progress')
String handleUpload(@Body() Map<String, dynamic> data) {
final progress = data['progress'] as int;
final filename = data['filename'] as String;
if (progress == 100) {
return 'Upload complete: $filename';
}
return 'Upload progress: $progress%';
}
}
Client Connection#
Using web_socket_channel#
import 'dart:convert';
import 'package:web_socket_channel/io.dart';
void main() {
final channel = IOWebSocketChannel.connect('ws://localhost:8080/chat/messages');
// Listen for messages
channel.stream.listen(
(message) {
print('Received: $message');
},
onError: (error) {
print('Error: $error');
},
onDone: () {
print('Connection closed');
},
);
// Send message
channel.sink.add('Hello WebSocket!');
}
Using dart:io WebSocket#
import 'dart:io';
void main() async {
final socket = await WebSocket.connect('ws://localhost:8080/chat/messages');
socket.listen(
(message) {
print('Received: $message');
},
onError: (error) {
print('Error: $error');
},
onDone: () {
print('Connection closed');
},
);
socket.add('Hello WebSocket!');
}
WebSocket Lifecycle#
The WebSocket lifecycle follows this pattern:
- Open Connection - Client connects
- Observer - Pre-connection observers run
- Middleware - Middleware components run
- Guard - Guard components run
- On Connect - Handler runs if
triggerOnConnect: true -
Message Loop - For each message:
- Interceptor (Pre)
- Endpoint Handler
- Interceptor (Post)
- Observer - Post-connection observers run
- Close Connection - Connection closes
Best Practices#
Use Appropriate Modes#
// ✅ Good - Use send-only for notifications
@WebSocket('notifications', mode: WebSocketMode.sendOnly)
String sendNotification() {
return 'Notification';
}
// ✅ Good - Use receive-only for logging
@WebSocket('logs', mode: WebSocketMode.receiveOnly)
void logMessage(@Body() String message) {
logger.info(message);
}
Handle Connection Cleanup#
// ✅ Good - Proper cleanup
@WebSocket('data')
String handleData(CloseWebSocket closer, CleanUp cleanUp) {
cleanUp.add(() {
// Cleanup resources when connection closes
resourceService.dispose();
});
return 'Data processed';
}
Use Type-Safe Messages#
// ✅ Good - Type-safe message handling
class ChatMessage {
const ChatMessage({required this.user, required this.text});
final String user;
final String text;
factory ChatMessage.fromJson(Map<String, dynamic> json) {
return ChatMessage(
user: json['user'] as String,
text: json['text'] as String,
);
}
}
@WebSocket('chat')
String handleChat(@Body() ChatMessage message) {
return '${message.user}: ${message.text}';
}
What's Next?#
- Learn about Server-Sent Events for one-way streaming
- Explore response body for HTTP responses
- See response headers for HTTP headers