dart-lang / web_socket_channel

StreamChannel wrappers for WebSockets.
https://pub.dev/packages/web_socket_channel
BSD 3-Clause "New" or "Revised" License
417 stars 109 forks source link

Lack of ability to configure connection timeouts #61

Open nailgilaziev opened 5 years ago

nailgilaziev commented 5 years ago

On mobile connection quality can vary depending on the environment conditions. if connection is bad for the moment sometimes connecting to a websocket servers can't be established now, and in this situation ConnectionTimeOut exception must thrown. Actually this exception throws: WebSocketChannelException: WebSocketChannelException: SocketException: OS Error: Connection timed out, errno = 110, address = 192.168.2.2, port = 56472 But it take around a 2min before happen.

This is too long for creating a realtime applications with websockets, and sometimes we need to fail faster.

Additionally, this 2 min behaviour is annoying because of issue #21 that closed because issue #25 and https://github.com/flutter/flutter/issues/18204 created. (connecting and already connected states still not differentiable)

How this connection timeout can be configured in this package?

One year ago in issue #22 nex3 answered me, that dart io implementation doesn't expose this API. But it is a superficial answer, because: 1) HttpClient has this property https://api.dartlang.org/stable/2.4.0/dart-io/HttpClient/connectionTimeout.html

2) if we use dart.io or html implementation directly we can use .timeout function on futures:

_wsFuture = WebSocket.connect(backendUrl)
        .timeout(Duration(seconds: 15))
        .then(_configureWsAfterConnecting);

But when using this package I can't manipulate with connection timeout. If connection doesn't establish in acceptable time I can't fail gracefully.

MarcelGarus commented 4 years ago

Any progress on this? 👀

kavinda1995 commented 4 years ago

What i was doing is create a native socket and wrap it in IOWebSocketChannel

Like this

WebSocket.connect(_SERVER_ADDRESS)
        .timeout(Duration(seconds: 20))
        .then((ws) {
      try {
        _channel = new IOWebSocketChannel(ws);
      } catch (e) {
        print(
            'Error happened when opening a new websocket connection. ${e.toString()}');
      }
ziedReg commented 4 years ago

@kavinda1995 I got an error. The WebSocket that comes from connect is from flutter/bin/cache/pkg/sky_engine/lib/_http/websocket.dart while the IOWebSocketChannel requires a WebSocket from flutter/bin/cache/pkg/sky_engine/lib/html/html_dart2js.dart.

kavinda1995 commented 4 years ago

@zeidReg - No. You must import WebSocket from dart:io. Not from any other package

ziedReg commented 4 years ago

@kavinda1995 I don't really get it. I imported it from dart:io but when i press Ctrl + click on the class name i go to the file flutter/bin/cache/pkg/sky_engine/lib/_http/websocket.dart

kavinda1995 commented 4 years ago

@ziedReg please provide your code snippet here. So we can elaborate :smiley:

ziedReg commented 4 years ago

These are the imports

import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:web_socket_channel/html.dart';
import 'package:my_project/constants/variables.dart';
import 'package:web_socket_channel/io.dart';

this is the method where i connect to the socket

void fetchImages() {
    if (channel == null) {
      // copying your code
      WebSocket.connect("ws://"+settings.host+":"+settings.port.toString()).timeout(Duration(seconds: 5)).then((ws) {
          try {
            channel = new HtmlWebSocketChannel(ws); // there is an error at "ws"
          } catch (e) {
          print('Error happened when opening a new websocket connection. ${e.toString()}');
        }
      });

      // what i'm working with right now
      channel = HtmlWebSocketChannel.connect("ws://"+settings.host+":"+settings.port.toString());

    }
    channel.stream.listen((message) async{
      bytes = base64Decode(message);
      image = Image.memory(bytes);
      await precacheImage(image.image, context);
      setState(() {});
    });
  }

The error is : The argument type 'WebSocket (where WebSocket is defined in ~/flutter/bin/cache/pkg/sky_engine/lib/_http/websocket.dart)' can't be assigned to the parameter type 'WebSocket (where WebSocket is defined in ~/flutter/bin/cache/pkg/sky_engine/lib/html/html_dart2js.dart)'. (argument_type_not_assignable at [my_project] lib/views/control/control.dart:44)

I am using HtmlWebSocketChannel because i'm working on web but i tried IOWebSocketChannel and got the same error

kavinda1995 commented 4 years ago

Oh 😞. Couldn't see any wrong in this

theLee3 commented 4 years ago

@ziedReg I'm probably too late, but the issue here is that you are using HtmlWebSocketChannel (for web) while @kavinda1995 is using IOWebSocketChannel (for mobile). As noted, the WebSocket class in use is from the dart:io package, which cannot be used for web development. The two websocket implementations are not interchangeable.

steeling commented 3 years ago

So there really isn't a good way to do this, particularly since the dart:html package only offers a constructor that doesn't return a future

sm2017 commented 3 years ago

What i was doing is create a native socket and wrap it in IOWebSocketChannel

Like this

WebSocket.connect(_SERVER_ADDRESS)
        .timeout(Duration(seconds: 20))
        .then((ws) {
      try {
        _channel = new IOWebSocketChannel(ws);
      } catch (e) {
        print(
            'Error happened when opening a new websocket connection. ${e.toString()}');
      }

@kavinda1995 is your approach safe? Consider the following scenario:

1- By running WebSocket.connect a web socket connection will be established but due to network issues, it can be established after 30 seconds 2- timeout(Duration(seconds: 20)) will completeError the feature with TimeoutException after 20 seconds 3- We have a reconnect logic in our code in the catch and do same thing for connection 4- By running WebSocket.connect again, a new web socket connection will be established without timeout 5- after 30 seconds, the first connection is established, So we have 2 web socked connection, and there is no way to close the first

theLee3 commented 3 years ago

@sm2017 is correct. This can lead to resource leaks. The timeout passed to the WebSocket is for the communication Stream, not the establishment of the WebSocket.

It seems to me that a viable solution could be to pass a connectionTimeout parameter to the WebSocket constructor which would be passed on to the HttpClient when establishing a connection. This could also trickle up to IOWebSocketChannel.connect making establishing web sockets much easier/safer. Unless there is a reason unbeknown to me why this is should not be.

Currently, I believe the safest way to handle timeouts when establishing a web socket in Dart is to use an HttpClient and manually upgrade to a WebSocket. Then you can create the IOWebSocketChannel with the WebSocket.

    final r = Random();
    final key = base64.encode(List<int>.generate(8, (_) => r.nextInt(255)));

    final client = HttpClient();
    client
       .getUrl(Uri.parse(SERVER_ADDRESS))
       .timeout(Duration(seconds: 20))
       .then((request) {
           request.headers.add('Connection', 'upgrade');
           request.headers.add('Upgrade', 'websocket');
           request.headers.add('sec-websocket-version', '13');
           request.headers.add('sec-websocket-key', key);

           request.close().then((response) {
              response.detachSocket().then((socket) {
                  final webSocket = WebSocket.fromUpgradedSocket(socket, serverSide: false);
                  _channel = IOWebSocketChannel(webSocket);
               });
            });
         }).catchError((error) {
              // handle error
              return null;
         });
julienduchow commented 3 years ago

Thanks, works great! @theLee3 Any solution for Web though?

theLee3 commented 3 years ago

@julien100000 For the web, we can simply check the status of the WebSocket after the desired timeout period, and if the connection has not been established, close the socket and handle the timeout according to your needs.

    // WebSocket class from `html` library
    final webSocket = WebSocket(SERVER_ADDRESS);
    Future.delayed(Duration(seconds: 20), () {
      if (webSocket.readyState == WebSocket.CONNECTING) {
        webSocket.close();
        // handle the timeout here
      }
    });
    _channel = HtmlWebSocketChannel(webSocket);
ycherniavskyi commented 2 years ago

I think that I found not ideal but technically correct solution:

final httpClient = HttpClient();
httpClient.connectionTimeout = Duration(seconds: 20);
final webSocket = await WebSocket.connect(
  'some url',
  customClient: httpClient,
);
final channel = IOWebSocketChannel(webSocket);
theLee3 commented 2 years ago

@ycherniavskyi This is a good solution. I would also suggest setting httpClient.idleTimeout and/or webSocket.pingInterval according to your needs.

Tienisto commented 11 months ago

I think that I found not ideal but technically correct solution:

final httpClient = HttpClient();
httpClient.connectionTimeout = Duration(seconds: 20);
final webSocket = await WebSocket.connect(
  'some url',
  customClient: httpClient,
);
final channel = IOWebSocketChannel(webSocket);

This is a viable workaround, but dart:io is not allowed in web

ScorpiosCrux commented 2 months ago

Maybe something has changed since this was originally posted, but you can set a timeout like this:

WebSocketChannel webSocket = WebSocketChannel.connect(Uri.parse(url));

await webSocket.ready.timeout(Duration(seconds: 10));

It then throws a TimeoutException