I'm having trouble getting my Dart WebSocket
to respect pingInterval
.
If I set pingInterval
on the client connection, the server doesn't seem to respond, and the connection gets closed with a status 1001 ("Going Away").
Here's an example:
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
void main() async {
final addr = InternetAddress('0.0.0.0', type: InternetAddressType.IPv4);
Server.start(addr);
final client = await Client.connect(addr);
await client.listen();
print('Script done');
}
class Server {
Server._(this._server) {
init();
}
final HttpServer _server;
Future<void> init() async {
await for (final request in _server) {
final socket = await WebSocketTransformer.upgrade(request);
print('[Server] Waiting for requests');
await for (final payload in socket) {
print('[Server] Incoming request: $payload');
print('[Server] Waiting 5 seconds before sending a response');
await Future.delayed(const Duration(seconds: 5));
socket.add('Hello from Server');
}
}
}
static Future<Server> start(InternetAddress adr) async {
final server = await HttpServer.bind(adr, 7444);
return Server._(server);
}
}
class Client {
Client._(this._socket) {
_socket.done.then((_) => print('Socket was done'));
}
final WebSocket _socket;
Future<void> listen() async {
_socket.add('Hello from Client');
await for (final String payload in _socket) {
print('Client incoming payload: $payload');
}
print('Close code: ${_socket.closeCode}');
if (_socket.closeCode == WebSocketStatus.goingAway) {
print('Code was "Going Away", meaning the server did not respond to set pingInterval');
}
}
static Future<WebSocket> _createWebSocket(InternetAddress adr) async {
final r = Random();
final key = base64.encode(List<int>.generate(8, (_) => r.nextInt(256)));
final client = HttpClient();
client.connectionTimeout = const Duration(seconds: 6);
final uri = Uri.parse('http://${adr.address}:7444');
print('Connecting to ${uri}');
final request = await client.getUrl(uri);
request.headers.add('Connection', 'upgrade');
request.headers.add('Upgrade', 'websocket');
request.headers.add('sec-websocket-version', '13');
request.headers.add('sec-websocket-key', key);
final response = await request.close();
final socket = await response.detachSocket();
return WebSocket.fromUpgradedSocket(socket, serverSide: false);
}
static Future<Client> connect(InternetAddress adr) async {
final socket = await Client._createWebSocket(adr)
..pingInterval = const Duration(seconds: 2);
return Client._(socket);
}
}
You can paste the example into a test.dart
file, and run it with dart test.dart
.
A pingInterval of 2 seconds (according to the docs) waits 2 seconds before sending a PING and then waits 2 seconds to receive a PONG, from the server.
However, it seems that if I haven't explicitly sent data from the server, this never arrives, as it falls below the my hardcoded response time of 5 seconds.
If I set the pingInterval
to 5 seconds, for example, the server replies after 5 seconds, and the connection never "Goes Away".
This is highly unintuitive. It seems weird that the PING/PONG logic is dependent on me explicitly sending a response before it starts.
Am I doing something wrong? How do I get the PING/PONG logic to work without an explicit reply from the server?
That issue is unrelated to the socket, the await Future.delayed
is blocking the messages (the pong) and the client is dropping the connection as a result. The ping-pong is invisible, it wont appear in the stream (see RFC6455 for more details), but they will be counted in the total send/receive in the devtools.
To run this example use this command. Open de network tap before resuming the execution to record the events.
> dart run --observe --pause-isolates-on-start .\bin\main.dart
In this case, it's better to use the Future
"api" than await
, mainly because it's easier to run thing in "parallel". You should note that await for
will only process one loop at the time, if the loop is not fast enough the delay may compound and clients may timeout.
import 'dart:io';
import 'dart:async';
class Server {
Server._(this.http_socket) : websockets = <WebSocket>{};
static Future<Server> bind(InternetAddress address, int port) async {
final instance = Server._(await HttpServer.bind(address, port));
instance.http_socket.listen(
instance.handle_request,
onError: (error, stacktrace) => instance.close(),
onDone: () => instance.close(),
);
return instance;
}
void close({bool force = false, int? websocket_code, String? websocket_reason}) async {
await http_socket.close(force: force);
for (final socket in websockets) {
await socket.close(websocket_code, websocket_reason);
}
}
final HttpServer http_socket;
final Set<WebSocket> websockets;
void handle_request(HttpRequest request) {
if (request.uri.path == '/example_socket') {
WebSocketTransformer.upgrade(request).then((socket) {
websockets.add(socket);
socket.listen(
(payload) => handle_websocket_message(socket, request.uri, payload),
onError: (error, stacktrace) => websockets.remove(socket),
onDone: () => websockets.remove(socket),
);
socket.add('Server ready!');
});
return;
}
print('Unhandled request ${request.requestedUri}');
}
void handle_websocket_message(WebSocket socket, Uri uri, dynamic payload) {
print('Server: $payload');
}
}
void main() async {
final server = await Server.bind(InternetAddress.loopbackIPv4, 7444);
Future.delayed(const Duration(seconds: 20), server.close);
final client = await WebSocket.connect('ws://localhost:7444/example_socket');
client.listen((payload) => print('Client: $payload'));
client.pingInterval = const Duration(seconds: 2);
client.add('Client ready!');
print('Setup done');
}