22

I am struggling how to implement websockets autoreconnect in flutter. I use web_socket_channel, however, the plugin just wraps dart.io WebSocket, hence any solution based on WebSocket class will work for me as well.

I already figured out, how to catch the socket disconnection, see the code snippet below:

    try {
      _channel = IOWebSocketChannel.connect(
        wsUrl,
      );

      ///
      /// Start listening to new notifications / messages
      ///
      _channel.stream.listen(
        _onMessageFromServer,
        onDone: () {
          debugPrint('ws channel closed');
        },
        onError: (error) {
          debugPrint('ws error $error');
        },
      );
    } catch (e) {
      ///
      /// General error handling
      /// TODO handle connection failure
      ///
      debugPrint('Connection exception $e');
    }

I was thinking to call IOWebSocketChannel.connect from within onDone, however, this leads to a kind of infinite loop - since I have to close the _channel prior calling connect again, this on its turn calls onDone again and so on.

Any help would be greatly appreciated!

Angel Todorov
  • 1,443
  • 2
  • 19
  • 37
  • I'm using socket.io and the reconnect is handled by the plugin automatically. Any reason must use websocket? – Kenneth Li Apr 04 '19 at 17:21
  • `socket.io` has way too overhead compared to `websocket`. I am considering `socket.io` as a backup plan however. Which particular `socket.io` package are you using? – Angel Todorov Apr 05 '19 at 00:11
  • adhara_socket_io – Kenneth Li Apr 05 '19 at 11:43
  • I've created another thread to run a timer that send a heartbeat every 10 seconds to the server. this timer function also has a print('HB'); statement. The timer can run 24/7 even after app in background and with screen off(i.e. console see non-stop 'HB' 24/7), however, the heartbeat is stopped to send, around 10mins after screen off. Can websocket survive in this scenario? – Kenneth Li Apr 05 '19 at 11:50
  • So after all you created a hb to keep socket alive? In my experience when I was working with socket.io in javascript clients (browser and mobile webview), I don't have to create anything like that - socket.io handles this on its own. I am surprised to understand this is not the case in flutter. I believe the above approach described by you should apply to websocket as well. Re HB stop in background mode after awhile - this is due to OS battery optimization mode and you can hardly overcome it. The only viable solution is to reconnect once your app is back in foreground mode. – Angel Todorov Apr 05 '19 at 16:15
  • Have you had solution to reconnect websocket yet? I have same problem but I can't find a way to solve it – hoangquyy Oct 18 '19 at 05:27
  • Yup, [this](https://stackoverflow.com/questions/50663012/flutter-websocket-disconnect-listening/55502749#55502749) is what I ended up. – Angel Todorov Oct 19 '19 at 08:49

5 Answers5

23

With the package:web_socket_channel (IOWebSocketChannel) there is not any way in order to implement reconnection for the socket connections. But you can use WebSocket class in order to implement a reconnectable connection.

You can implement the WebSocket channel and then broadcast messages with StreamController class. Working example:

import 'dart:async';
import 'dart:io';

class NotificationController {

  static final NotificationController _singleton = new NotificationController._internal();

  StreamController<String> streamController = new StreamController.broadcast(sync: true);

  String wsUrl = 'ws://YOUR_WEBSERVICE_URL';

  WebSocket channel;

  factory NotificationController() {
    return _singleton;
  }

  NotificationController._internal() {
    initWebSocketConnection();
  }

  initWebSocketConnection() async {
    print("conecting...");
    this.channel = await connectWs();
    print("socket connection initializied");
    this.channel.done.then((dynamic _) => _onDisconnected());
    broadcastNotifications();
  }

  broadcastNotifications() {
    this.channel.listen((streamData) {
      streamController.add(streamData);
    }, onDone: () {
      print("conecting aborted");
      initWebSocketConnection();
    }, onError: (e) {
      print('Server error: $e');
      initWebSocketConnection();
    });
  }

  connectWs() async{
    try {
      return await WebSocket.connect(wsUrl);
    } catch  (e) {
      print("Error! can not connect WS connectWs " + e.toString());
      await Future.delayed(Duration(milliseconds: 10000));
      return await connectWs();
    }

  }

  void _onDisconnected() {
    initWebSocketConnection();
  }
}

Because the notification controller returns a singleton instance, then there will be always one Socket connection between the server and device. And with the broadcast method of StreamController, we can share the data sent by Websocket between multiple consumers

var _streamController = new NotificationController().streamController;

_streamController.stream.listen(pushNotifications);
Mehmet Esen
  • 6,156
  • 3
  • 25
  • 44
Isco msyv
  • 251
  • 2
  • 5
  • Clarification quesiton: how is `WebSocket.channel.done` different from the `onDone`, `onError` of the `IOWebSocketChannel`'s `stream`? – ifnotak Feb 12 '21 at 17:50
  • This when a websocket is closed by either the user or server itself, may be cause u got the data needed or the user closed the app – John Kinyanjui Sep 19 '21 at 17:47
  • I tried this and am using API Gateway which times out every idle 10 minutes. None of the done or error methods get called when gateway shuts down the socket – West Feb 08 '22 at 11:52
4

Most of the time, when we create a WebSocketChannel, we will use its stream to receive messages and sink to send messages.

The idea to reconnect is when the error happens or the socket is closed, we will create a new WebSocketChannel instance and assign it into a global shared var. But the hard thing is other places where use its stream & sink will be invalid.

To overcome this, we will create a fixed stream & sink to forward & transfer messages with the equivalent one of the new WebSocketChannel instance.

class AutoReconnectWebSocket {
  final Uri _endpoint;
  final int delay;
  final StreamController<dynamic> _recipientCtrl = StreamController<dynamic>();
  final StreamController<dynamic> _sentCtrl = StreamController<dynamic>();

  WebSocketChannel? webSocketChannel;

  get stream => _recipientCtrl.stream;

  get sink => _sentCtrl.sink;

  AutoReconnectWebSocket(this._endpoint, {this.delay = 5}) {
    _sentCtrl.stream.listen((event) {
      webSocketChannel!.sink.add(event);
    });
    _connect();
  }

  void _connect() {
    webSocketChannel = WebSocketChannel.connect(_endpoint);
    webSocketChannel!.stream.listen((event) {
      _recipientCtrl.add(event);
    }, onError: (e) async {
      _recipientCtrl.addError(e);
      await Future.delayed(Duration(seconds: delay));
      _connect();
    }, onDone: () async {
      await Future.delayed(Duration(seconds: delay));
      _connect();
    }, cancelOnError: true);
  }
}
yelliver
  • 5,648
  • 5
  • 34
  • 65
3

Here's what I do:

void reconnect() {
    setState(() {
      _channel = IOWebSocketChannel.connect(wsUrl);
    });
    _channel.stream.listen((data) => processMessage(data), onDone: reconnect);
  }

Then to start up your websocket, just do an initial call to reconnect(). Basically what this does is re-create your WebSocket when the onDone callback is called, which happens when the connection is destroyed. So, connection destroyed -- ok, let's reconnect automatically. I haven't found a way to do this without re-creating _channel. Like, ideally, there would be a _channel.connect() that would reconnect to the existing URL, or some kind of auto-reconnect feature, but that doesn't seem to exist.

Oh, here's something a bit better that will get rid of ugly reconnect exception tracebacks if the remote server is down, and add a reconnect delay of 4 seconds. In this case, the cancelOnError argument triggers socket closure on any error.

  wserror(err) async {
    print(new DateTime.now().toString() + " Connection error: $err");
    await reconnect();
  }

 reconnect() async {
    if (_channel != null) {
      // add in a reconnect delay
      await Future.delayed(Duration(seconds: 4));
    }
    setState(() {
      print(new DateTime.now().toString() + " Starting connection attempt...");
      _channel = IOWebSocketChannel.connect(wsUrl);
      print(new DateTime.now().toString() + " Connection attempt completed.");
    });
    _channel.stream.listen((data) => processMessage(data), onDone: reconnect, onError: wserror, cancelOnError: true);
  }
  • So essentially, you also ended up with [this solution](https://stackoverflow.com/questions/50663012/flutter-websocket-disconnect-listening/55502749#55502749), didn't you :-) – Angel Todorov May 20 '20 at 07:24
  • This doesnt reconnect when a server terminates the connection. For example, using API Gateway the `onDone` doesnt get called when the socket shuts down after the 10min idle time. – West Feb 08 '22 at 10:52
  • i think that it don't work about sink and stream of websocketchannel... – Lâm Lê Thạch Jul 22 '22 at 05:56
1

I recommend you to use this multiplatform websocket package https://pub.dev/packages/websocket_universal . There you can configure SocketConnectionOptions with [timeoutConnectionMs] , [failedReconnectionAttemptsLimit] and even [maxReconnectionAttemptsPerMinute]. And that package allows you to listen to webSocket events

Dmitrii Matunin
  • 275
  • 4
  • 5
0

This is how I'm doing it:

class MyAppState extends ChangeNotifier {
  WebSocketChannel? channel;
  bool webSocketConnected = false;
  int webSocketReconnectAttempts = 0;

  MyAppState() {
    connect();
  }

  void onMessage(message) {
    webSocketConnected = true;
    webSocketReconnectAttempts = 0;
    notifyListeners();
  }

  void onDone() async {
    var delay = 1 + 1 * webSocketReconnectAttempts;
    if (delay > 10) {
      delay = 10;
    }
    print(
        "Done, reconnecting in $delay seconds, attempt $webSocketReconnectAttempts ");
    webSocketConnected = false;
    channel = null;
    await Future.delayed(Duration(seconds: delay));
    connect();
  }

  void onError(error) {
    print(error);
    if (error is WebSocketChannelException) {
      webSocketReconnectAttempts += 1;
    }
  }

  void connect() {
    try {
      channel = WebSocketChannel.connect(
        Uri.parse('ws://localhost:8000/ws/generate/'),
      );
      channel!.stream.listen(onMessage, onDone: onDone, onError: onError);
    } catch (e) {
      print(e);
    }
  }
}

I'm not claiming this to be the best or even the most efficient, but it's reliable for me so far. I'm still learning Flutter myself, so I hope it helps.

If you want a full example with django channels as the host you can see that code here: https://github.com/BillSchumacher/HardDiffusion

The flutter code is in the hard_diffusion directory.

If you reconnect onError without having it close on error, I think that could be a problem.

I'm sending the client a message when I accept their connection as well.

Bill Schumacher
  • 231
  • 2
  • 6