Socket.IO v4 server for Dart. It works with the Dox framework and with plain dart:io.
Part of Dox, a web backend framework for Dart. Source code: github.com/dartondox/dox-socketio.
dart pub add dox_socketioTo see what is ready and what may still change, read Status and roadmap.
import 'package:dox_socketio/dox_socketio.dart';
Future<void> main() async {
SocketIoServer io = SocketIoServer();
io.on('connection', onConnection);
await io.listen(3001);
}
void onConnection(Socket socket) {
socket.emit('welcome', 'hello');
socket.join('lobby');
socket.on('chat', onChat);
}
void onChat(SocketEvent event) {
event.socket.to('lobby').emit('chat', event.data); // everyone in lobby but me
event.ack('ok'); // answer the client callback
}The official clients connect with no changes: socket.io-client (JavaScript v4) and the Dart socket_io_client package.
In a Dox app, extend SocketIoProvider in lib/. The generated container finds it, and Socket.IO runs on the app port:
import 'package:dox_socketio/dox_socketio.dart';
class AppSocketIoProvider extends SocketIoProvider {
AppSocketIoProvider()
: super(SocketIoConfig(namespaces: <NamespaceHandler>[ChatNamespace()]));
}SocketIoProvider.server is the server of this isolate: SocketIoProvider.server?.to('room').emit('news', data).
Socket.IO gets its requests before the Dox router, so route middleware (session, auth) does not run for it.
Use SocketAuth as a namespace middleware. Your function gets the handshake and gives the user, or null.
class ChatNamespace extends NamespaceHandler {
@override
String get path => '/chat';
@override
List<Middleware> get middleware => <Middleware>[SocketAuth(findUser).handle];
static Future<User?> findUser(Handshake handshake) async {
String? token = handshake.token;
if (token == null) return null;
return User().where('api_token', token).getFirst();
}
@override
void onConnection(Socket socket) {
User user = socket.user! as User;
socket.join('user:${user.id}');
}
}- With a user, it is kept in
socket.user. - With null, the client gets a
connect_errorwith the messageUnauthorized, andonConnectiondoes not run. Change it withSocketAuth(findUser, message: '...'). handshake.tokenisauth.tokenfrom the client (io(url, { auth: { token } })), else theAuthorization: Bearerheader.handshake.cookie('name')reads a cookie and decrypts it with the APP_KEY, likereq.cookie('name'). Usedecrypt: falsefor a plain cookie.await handshake.session()gives the Dox session of the client, from its session cookie. It has the same checks asSessionMiddleware. A changed, foreign or expired cookie gives an empty session. It is read only. With noallowedOrigins, or with'*'in it, it prints a warning once.io.use(SocketAuth(findUser).handle)does the same for the default namespace.
With dox_auth, check a JWT or the web login session:
static Future<User?> findUser(Handshake handshake) async {
String? token = handshake.token;
if (token != null) return Auth(guard: 'api').userFromToken(token);
return Auth(guard: 'web').userFromSession(await handshake.session());
}- Engine.IO protocol v4 and Socket.IO protocol v5 (the protocols of Socket.IO v4).
- Polling and websocket transports, upgrade, heartbeat and CORS.
- Namespaces (also as classes), middleware, rooms, broadcast, acks and binary data.
- In-memory adapter, an isolate adapter for the isolates of one Dox app (no Redis, the default with many isolates), and a Redis adapter for many servers.
- The Redis adapter uses the message format of
@socket.io/redis-adapter. Dox and Node.js servers can share one Redis. fetchSockets,socketsJoin,socketsLeave,disconnectSockets,serverSideEmitand broadcast acks work across all servers.SocketIoEmittersends events from any Dart program, like@socket.io/redis-emitter.
By default, the server does not check the Origin header of polling requests. It checks a websocket only against cors, when cors is set. So a browser page on another site can connect, with the cookies of your users. The server logs one warning about this when it starts.
To turn on the check, set allowedOrigins:
ServerOptions(allowedOrigins: <String>['https://app.example.com'])[]: only pages of the same host can connect.- A list: the same host and the listed sites can connect.
['*']: every site can connect.
Then a websocket or polling request from another origin gets 403. A client with no Origin header (not a browser) is always allowed. Case and a trailing / do not matter. cors still sets the CORS headers for polling.
Use the Redis adapter on each server:
Redis subscriber = Redis(RedisOptions(host: '127.0.0.1', port: 6379));
SocketIoServer io = SocketIoServer(
adapter: RedisAdapter(
subscriber: subscriber,
publisher: subscriber.duplicate(),
),
);Then these act on the sockets of all servers:
List<RemoteSocket> sockets = await io.to('room1').fetchSockets();
await io.to('room1').socketsJoin('vip');
await io.to('vip').socketsLeave('room1');
await io.except('vip').disconnectSockets();
// Acks from clients on all servers. A timeout is needed.
List<Object?> answers = await io
.timeout(Duration(seconds: 5))
.emitWithAck('are-you-there');
// Events between servers. Clients do not get them.
io.onServerSideEmit('reload', (ServerSideEvent event) => event.ack('done'));
await io.serverSideEmit('reload', <Object?>['config']);
List<Object?> done = await io.serverSideEmitWithAck('reload');fetchSockets()gives aSocketfor sockets of this server and aRemoteSocketfor the others. ARemoteSockethasid,handshake,roomsanddata(copies) andemit,join,leaveanddisconnect, which go through Redis.socketsJoin,socketsLeaveanddisconnectSocketscomplete when this server is done and the request is sent. They do not wait for the other servers.fetchSocketsandserverSideEmitWithAckwait for an answer from each server, for at mostrequestsTimeout(default 5 seconds, like Node.js). When a server does not answer in time, they throw, like Node.js:fetchSocketsthrows aTimeoutException, andserverSideEmitWithAckthrows anAckTimeoutExceptionwhoseresponsesholds the answers that came.emitWithAckon a broadcast throws anAckTimeoutExceptionwhen not all clients answer in time. Itsresponsesholds the acks that came.- With the in-memory adapter, all of this acts on the local server only.
MIT. See LICENSE.