websocket_core
O Backend WebSocket Definitivo para Dart.
Sessões persistentes, reconexão automática, validação de schema e arquitetura escalável — zero dependências externas.
🚀 Por que websocket_core?
- Zero Boilerplate: Validação, serialização e roteamento prontos.
- Sessão > Conexão: Se a internet cair, a sessão (e o estado) sobrevive.
- Client SDK Incluso: Cliente Dart/Flutter que já fala o protocolo.
- Protocolo Explícito: Versionamento e tipagem forte em cada mensagem.
- Zero Dependências: Puro
dart:io. Leve e rápido.
📦 Instalação
dependencies:
websocket_core: ^1.2.1
⚡ Quick Start
1. Crie o Servidor
import 'package:websocket_core/websocket_core.dart';
void main() async {
// Configuração rápida para desenvolvimento
final server = WsServer(
config: WsServerConfig.dev(port: 8080),
);
// Define o schema do payload (Opcional, mas recomendado)
final chatSchema = {
'text': (v) => v is String && v.isNotEmpty,
'roomId': (v) => v is String,
};
// Handler moderno com Auto-Reply
server.on('chat.message', (ctx) async {
final text = ctx.payload['text'];
final roomId = ctx.payload['roomId'];
print('Msg: $text');
// Broadcast para outros usuários
ctx.broadcastToRoom(roomId, 'chat.new_message', {
'text': text,
'sender': ctx.userId ?? 'anon',
});
// Em 1.2.0, basta retornar o valor para responder ao remetente!
return {'status': 'sent', 'timestamp': DateTime.now().millisecondsSinceEpoch};
}, schema: chatSchema);
// Handler de Join
server.on('room.join', (ctx) async {
final roomId = ctx.payload['roomId'];
server.rooms.join(roomId, ctx.session);
return {'joined': roomId};
});
await server.start();
print('Server listening on ws://localhost:8080/ws');
}
2. Conecte o Cliente (Flutter/Dart)
import 'package:websocket_core/websocket_core.dart';
void main() async {
final client = WsClient('ws://localhost:8080/ws');
// Reconexão e Handshake são automáticos
await client.connect();
client.on('chat.new_message', (data) {
print('Nova mensagem de ${data['sender']}: ${data['text']}');
});
client.on('ack', (data) => print('Mensagem entregue!'));
// Entra na sala
client.send('room.join', {'roomId': 'geral'});
// Envia mensagem
client.send('chat.message', {
'roomId': 'geral',
'text': 'Olá mundo!',
});
}
🔄 Ciclo de Vida & Arquitetura
O diferencial do websocket_core é tratar Sessão e Conexão como entidades distintas.
sequenceDiagram
participant App
participant Server
participant SessionManager
App->>Server: Connect (WebSocket)
Server->>SessionManager: Create Session (ID: A1)
Server-->>App: Connected (Session: A1)
Note over App, Server: 💥 Conexão cai (4G instável)
App->>Server: Reconnect (Session: A1)
Server->>SessionManager: Validate Session A1
SessionManager-->>Server: Restore State
Server-->>App: Reconnected (Session Restored)
- Conexão: O socket TCP/IP. Pode cair a qualquer momento.
- Sessão: O estado do usuário (autenticação, salas, variáveis). Sobrevive à queda da conexão.
- Reconexão: O cliente tenta reconectar enviando o ID da sessão anterior. Se válida, tudo é restaurado.
📚 Cookbook: Receitas Práticas
Validação Declarativa
Esqueça os if (data['id'] == null). Use schemas:
server.on('transfer', (ctx) async {
// Lógica de transferência...
}, schema: {
'amount': (v) => v is num && v > 0,
'toAccount': (v) => v is String && v.length == 10,
});
// O servidor retorna erro automaticamente se a validação falhar
Autenticação JWT
Implemente WsAuthenticator para proteger seu servidor.
class JwtAuth extends WsAuthenticator {
@override
Future<AuthResult> authenticate(WsConnection conn, String? token) async {
if (token == null) return AuthResult.failure(error: 'Token missing');
try {
final userId = verifyJwt(token); // Use sua lib de JWT preferida
return AuthResult.success(userId: userId);
} catch (_) {
return AuthResult.failure(error: 'Invalid token');
}
}
}
// Uso:
final server = WsServer(
config: WsServerConfig.prod(port: 8080, requireAuth: true),
authenticator: JwtAuth(),
);
Tipagem Forte com DTOs
Use o método bind<T> para converter payloads em objetos.
// Seu DTO
class MessageDto {
final String text;
MessageDto(this.text);
factory MessageDto.fromMap(Map<String, dynamic> map) {
if (map['text'] is! String) throw Exception('Invalid text');
return MessageDto(map['text']);
}
}
// Handler
server.on('msg', (ctx) async {
// Valida e converte em uma linha
final msg = ctx.bind(MessageDto.fromMap);
print(msg.text);
});
Request-Response (RPC)
Ao invés de apenas enviar e torcer para chegar, aguarde uma resposta específica:
// Client
try {
final response = await client.request('get.user', {'id': '123'});
print('User: ${response['name']}');
} catch (e) {
print('Error: $e');
}
// Server
server.on('get.user', (ctx) async {
// Auto-Reply: Retorne um Map/List e o servidor responde automaticamente!
return {'id': ctx.payload['id'], 'name': 'Murillo'};
});
Organizando com Controllers
Para apps grandes, não encha seu main.dart de handlers. Use WsController:
class ChatController extends WsController {
@override
void register(WsServer server) {
server.on('chat.send', _onSend);
server.on('chat.join', _onJoin);
}
Future<dynamic> _onSend(WsContext ctx) async {
// Lógica aqui...
return {'status': 'sent'};
}
Future<dynamic> _onJoin(WsContext ctx) async { ... }
}
// No main:
server.registerController(ChatController());
⚙️ Configuração Avançada
Produção vs Desenvolvimento
// Dev: Timeouts relaxados, sem auth obrigatória
final dev = WsServerConfig.dev();
// Prod: Timeouts agressivos, auth forçada, limpeza rápida
final prod = WsServerConfig.prod(
port: 8080,
host: '0.0.0.0',
corsHeaders: {'Access-Control-Allow-Origin': '*'},
);
Protocolo
O WsClient lida com tudo isso, mas se você criar um cliente customizado, as mensagens seguem este formato JSON:
{
"v": "1.0", // Versão do protocolo
"e": "chat.message", // Nome do Evento
"p": { "text": "oi" },// Payload (Dados)
"c": "uuid-123", // Correlation ID (Opcional, para request-response)
"t": 1234567890 // Timestamp (ms)
}
🚀 Deployment & Produção
Nginx (Reverse Proxy)
WebSockets precisam de headers específicos para funcionarem através de proxies.
location /ws {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
# Timeouts longos para evitar desconexão
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
Docker
O servidor roda em qualquer container Dart. Lembre-se de expor a porta.
FROM dart:stable AS build
WORKDIR /app
COPY pubspec.* ./
RUN dart pub get
COPY . .
RUN dart compile exe bin/server.dart -o bin/server
FROM scratch
COPY --from=build /runtime/ /
COPY --from=build /app/bin/server /app/bin/
CMD ["/app/bin/server"]
⚠️ Códigos de Erro
O servidor envia estes códigos no evento sys.error ou no fechamento da conexão.
| Código | Nome | Descrição |
|---|---|---|
| 1001 | invalidProtocol | Formato de mensagem incorreto |
| 1003 | authRequired | Autenticação necessária |
| 1004 | authFailed | Credenciais inválidas |
| 1005 | tokenExpired | Token JWT expirou |
| 1006 | sessionNotFound | Sessão inválida ou expirada |
| 1008 | handlerNotFound | Ninguém ouvindo esse evento |
| 1009 | validationFailed | Schema ou bind falhou |
| 1010 | rateLimitExceeded | Calma, muitas requisições |
🤝 Contribuindo
Pull requests são bem-vindos. Para mudanças maiores, abra uma issue primeiro.
📄 Licença
MIT
Libraries
- websocket_core
- WebSocket Core - Backend WebSocket package for Dart