WebSocket
WebSocket maakt het mogelijk dat de server en de browser een permanente, tweerichtingsverbinding onderhouden. In tegenstelling tot gewone HTTP, waarbij de browser altijd het verzoek initieert, kan de server via WebSocket op elk moment berichten naar clients pushen.
Veelvoorkomende toepassingen: realtime notificaties, live chat, dashboards die automatisch bijwerken, multiplayer-games.
Finch's WebSocket-ondersteuning is opgebouwd rond drie klassen:
SocketManager— beheert alle actieve WebSocket-verbindingen en stuurt berichten door naar de juiste route-handler.SocketEvent— definieert callbacks vooronConnect,onMessage,onDisconnectenonErrorop een specifiek pad.SocketClient— vertegenwoordigt één verbonden client; wordt aan elke callback doorgegeven en is waarop je.send()aanroept.
Een gewone Finch-Controller is wat het inkomende HTTP-verzoek upgradet naar een WebSocket-verbinding — er bestaat geen aparte SocketController-basisklasse om uit te breiden.
Installatie
1. Definieer een SocketManager
Maak de SocketManager aan in app.dart. Deze neemt de app-instantie, een root-SocketEvent (voor levenscyclusgebeurtenissen) en een map met benoemde route-handlers:
final socketManager = SocketManager(
app,
event: SocketEvent(
onConnect: (socket) {
// Wordt aangeroepen wanneer een client verbinding maakt
// Informeer alle andere clients over de nieuwe verbinding
app.socketManager?.sendToAll(
'A user connected. Total: ${app.socketManager?.countClients}',
path: 'output',
);
// Stuur een bevestiging naar de zojuist verbonden client
socket.send(
{'message': 'Successfully connected to socket!'},
path: 'connected',
);
},
onMessage: (socket, data) {
// Wordt aangeroepen voor berichten die niet overeenkomen met een benoemde route
},
onDisconnect: (socket) {
// Wordt aangeroepen wanneer een client de verbinding verbreekt
var count = app.socketManager?.countClients ?? 0;
app.socketManager?.sendToAll(
'A user disconnected. Total: ${count - 1}',
path: 'output',
);
},
onError: (socket, data) {
// Wordt aangeroepen wanneer het inkomende bericht niet als JSON kan worden
// gedecodeerd, of wanneer er een andere fout optreedt tijdens de verwerking ervan
},
),
routes: _getSocketRoutes(),
);
Alleen de root-event krijgt onConnect/onDisconnect/onError-aanroepen — zie de opmerking onder "Definieer benoemde routes" hieronder.
2. Definieer benoemde routes
Socketroutes zijn een Map<String, SocketEvent>. Elke sleutel is een "pad" (een logische kanaalnaam). Wanneer een client een bericht naar dat pad stuurt, wordt de bijbehorende onMessage-callback geactiveerd:
Map<String, SocketEvent> _getSocketRoutes() {
return {
// Client stuurt naar pad 'test' — server antwoordt met requestheaders
'test': SocketEvent(
onMessage: (socket, data) {
socket.send([socket.rq.headers], path: 'test');
},
),
// Client stuurt naar pad 'time' — server antwoordt met de huidige tijd
'time': SocketEvent(
onMessage: (socket, data) {
socket.send(DateTime.now().toString(), path: 'output');
},
),
};
}
Van de
SocketEventvan een benoemde route wordt alleenonMessagegebruikt —onConnect/onDisconnect/onErrordie op een route-item zijn ingesteld, worden nooit aangeroepen; die worden alleen aangeroepen vanuit de root-eventdie aanSocketManageris doorgegeven. Een bericht waarvan hetpathmet geen enkele benoemde route overeenkomt, valt in plaats daarvan terug op deonMessagevan de root-event.
3. Upgrade het verzoek in een controller
De taak van de controller is om het HTTP-verzoek over te dragen aan socketManager zodat de upgrade kan plaatsvinden — het is een gewone Controller, niets bijzonders:
class WebSocketController extends Controller {
Future<String> socket() async {
// Draag het verzoek over aan de SocketManager voor de WebSocket-upgrade
await socketManager.requestHandle(rq);
return rq.renderSocket(); // retourneert 'Socket is requested!'
}
}
4. Registreer de WebSocket-route
De WebSocket-route moet Methods.ALL accepteren, omdat de upgrade-handshake gebruikmaakt van een GET-verzoek met speciale headers:
FinchRoute(
key: 'root.ws',
path: '/ws',
methods: Methods.ALL,
index: webSocketController.socket,
),
Berichten verzenden vanaf de server
Elke callback ontvangt een SocketClient (in de voorbeelden hierboven aangeduid als socket). Deze — en SocketManager zelf — bieden methoden om berichten te verzenden:
// Verzenden naar de specifieke client die de gebeurtenis heeft geactiveerd
socket.send(data, path: 'channelName');
// Verzenden naar alle verbonden clients
app.socketManager?.sendToAll(data, path: 'channelName');
// Verzenden naar één specifieke client op basis van connectie-ID
// (let op: de echte naam van de methode bevat een typfout: "Clinet", niet "Client")
app.socketManager?.sendToClinet(clientId, data, path: 'channelName');
// Verzenden naar elke client die aan een bepaalde gebruikers-ID is gekoppeld
// (vereist dat userId werd doorgegeven aan requestHandle(rq, userId: ...) bij het verbinden)
app.socketManager?.sendToUser(userId, data, path: 'channelName');
Er bestaat geen ingebouwde methode om "naar iedereen behalve deze ene" te verzenden — als je dat nodig hebt, filter dan zelf app.socketManager?.getAllClientsKeys() en roep sendToClinet aan voor elk resterend ID.
Het path in een verzendaanroep bepaalt welke handler aan de clientzijde het bericht ontvangt. Luister aan de JavaScript-kant naar berichten op dezelfde padnaam.
Voorbeeld van client-side JavaScript
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onopen = () => console.log('Connected');
// Luister naar berichten op het pad 'connected'
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.path === 'connected') {
console.log('Server says:', msg.data.message);
}
if (msg.path === 'output') {
console.log('Output:', msg.data);
}
};
// Stuur een bericht naar de route 'time' op de server
ws.send(JSON.stringify({ path: 'time', data: {} }));
Hoeveel clients zijn er verbonden?
int count = app.socketManager?.countClients ?? 0;
int users = app.socketManager?.countUsers ?? 0; // unieke gebruikers, als je userId doorgeeft bij het verbinden