Giter Site home page Giter Site logo

felangel / web_socket_client Goto Github PK

View Code? Open in Web Editor NEW
135.0 6.0 28.0 41 KB

A simple WebSocket client for Dart which includes automatic reconnection logic.

Home Page: https://pub.dev/packages/web_socket_client

License: MIT License

Dart 100.00%
dart dart-package web-socket websocket websocket-client

web_socket_client's Introduction

WebSocket Client

build coverage pub package style: very good analysis License: MIT

A simple WebSocket client for Dart which includes automatic reconnection logic.

Quick Start ๐Ÿš€

// Create a WebSocket client.
final socket = WebSocket(Uri.parse('ws://localhost:8080'));

// Listen to messages from the server.
socket.messages.listen((message) {
  // Handle incoming messages.
});

// Send a message to the server.
socket.send('ping');

// Close the connection.
socket.close();

Establishing a Connection ๐Ÿ”Œ

The WebSocket client will attempt to establish a connection immediately upon initialization. By default, a timeout will occur if establishing a connection exceeds 60 seconds but a custom timeout duration can be provided:

final uri = Uri.parse('ws://localhost:8080');

// Trigger a timeout if establishing a connection exceeds 10s.
final timeout = Duration(seconds: 10);
final socket = WebSocket(uri, timeout: timeout);

Reconnecting ๐Ÿ”„

If the WebSocket client is not able to establish a connection, it will automatically attempt to reconnect using the provided Backoff strategy. By default, a BinaryExponentialBackoff is used but a custom backoff can be provided.

There are three built-in backoff strategies but a custom backoff strategy can be written by implementing the Backoff interface.

ConstantBackoff

This backoff strategy will make the WebSocket client wait a constant amount of time between reconnection attempts.

// Wait a constant 1s between reconnection attempts.
// [1, 1, 1, ...]
const backoff = ConstantBackoff(Duration(seconds: 1));
final socket = WebSocket(uri, backoff: backoff);

LinearBackoff

This backoff strategy will make the WebSocket client wait a linearly increasing amount of time until an optional maximum duration is reached.

// Initially wait 0s and increase the wait time by 1s until a maximum of 5s is reached.
// [0, 1, 2, 3, 4, 5, 5, 5, ...]
const backoff = LinearBackoff(
  initial: Duration(seconds: 0),
  increment: Duration(seconds: 1),
  maximum: Duration(seconds: 5),
);
final socket = WebSocket(uri, backoff: backoff);

BinaryExponentialBackoff

This backoff strategy will make the WebSocket client wait an exponentially increasing amount of time until a maximum step is reached.

// Initially wait 1s and double the wait time until a maximum step of of 3 is reached.
// [1, 2, 4, 4, 4, ...]
const backoff = BinaryExponentialBackoff(
  initial: Duration(seconds: 1),
  maximumStep: 3
);
final socket = WebSocket(uri, backoff: backoff);

Monitoring the Connection โšก๏ธ

The WebSocket client exposes a connection object which can be used to query the connection state at any given time as well as listen to real-time changes in the connection state.

final uri = Uri.parse('ws://localhost:8080');
final socket = WebSocket(uri);

// Listen to changes in the connection state.
socket.connection.listen((state) {
  // Handle changes in the connection state.
});

// Query the current connection state.
final connectionState = socket.connection.state;

The connection state can be one of the following:

  • connecting: the connection has not yet been established.
  • connected: the connection is established and communication is possible.
  • reconnecting: the connection was lost and is in the process of being re-established.
  • reconnected: the connection was lost and has been re-established.
  • disconnecting: the connection is going through the closing handshake or the close method has been invoked.
  • disconnected: the WebSocket connection has been closed or could not be established.

* The disconnected connection state contains nullable fields for the close code, close reason, error, and stack trace.

Sending Messages ๐Ÿ“ค

Once a WebSocket connection has been established, messages can be sent to the server via send:

final socket = WebSocket(Uri.parse('ws://localhost:8080'));

// Wait until a connection has been established.
await socket.connection.firstWhere((state) => state is Connected);

// Send a message to the server.
socket.send('ping');

Receiving Messages ๐Ÿ“ฅ

Listen for incoming messages from the server via the messages stream:

final socket = WebSocket(Uri.parse('ws://localhost:8080'));

// Listen for incoming messages.
socket.messages.listen((message) {
  // Handle the incoming message.
});

Protobuf ๐Ÿ’ฌ

If you're using web_socket_client on the web with Protobuf, you might want to use binaryType when initializing the WebSocket class. binaryType is only applicable on the web and is not used on desktop or mobile platforms.

final socket = WebSocket(Uri.parse('ws://localhost:8080'), binaryType: 'arraybuffer');

See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/binaryType for more info.

Closing the Connection ๐Ÿšซ

Once a WebSocket connection is established, it will automatically attempt to reconnect if the connection is disrupted. Calling close() will update the connection state to disconnecting, perform the closing handshake, and set the state to disconnected. At this point, the WebSocket client will not attempt to reconnect and a new WebSocket client instance will need to be created in order to establish a new connection.

final socket = WebSocket(Uri.parse('ws://localhost:8080'));

// Later, close the connection with an optional code and reason.
socket.close(1000, 'CLOSE_NORMAL');

web_socket_client's People

Contributors

dependabot[bot] avatar erjanmx avatar felangel avatar lwj1994 avatar maximilianflechtner avatar mytja avatar postflow avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

web_socket_client's Issues

refactor: Why is ConnectionState an abstact class and not an enum?

Description

I was checking the documentation regarding the connection state (and I expected a stream btw). I noticed that the ConnectionState is an abstract class. It seems that there is a special handling for Disconnected. I think that should be some kind of error stream instead of part of an state.

Requirements

  • Think about if a migration to an enum would be better
  • Stream the connection state?
  • Provide an error callback for the disconnected state error

feat: public connect fuction

Description

Don't authomatically connect when websocket class is created, but instead expose the now private _connect function.

Additional Context

I have a usecase where I don't want the socket to connect the second it is created.

fix: Websocket not updating state connection

Description

Hi, didn't know how to classify this, maybe is a bug, maybe a question or maybe a documentation problem.

I have an app that needs a behavior like Whatsapp or any messenger app, that when you open the app it should always update the screen if there is new data. So when using this websocket all is good while I am on the app. But when I go to another app or block the device for a long time, websocket of course loose connection maybe because of how Android or iOS handles background calls. So there are some times that websocket is not able to reconnect, I mean is not even noticing it (using the listener for state connection). Idk if this is a expected behavior when you leave the app? How I can handle this? I am helping me with WidgetsBindingObserver, but as I don't know if the websocket looses connection I don't have a way to know if a call to remote data source is needed. Another question that I couldn't find is the amount of time that strategies tries to reconnect, is infinite? I am using the default one.

This is happening only on a physical device, I am not able to reproduce it on emulator. No matter if I lock the device for hours, websocket state connection works great. But there is a second problem related: disabling wifi and enabling, happens in both, physical and emulator devices, the listener is not fired.

Expected Behavior
listener should always communicate the state of the connection? How to handle when you go out of the app, and come back, listener should reconnect?
If you disable internet connection like Wifi, and re enable it, webscoket should reconnect? Or at least communicate it on the listener?

feat: a way to know if WS is going to retry connecting

Description

Right now we get Disconnected from the .connection stream of WS.
But there is no way if the WS will try to reconnect afterwards or if it exhausted retrying.

Additional Context

I am creating a client API. While using a await for in ws.connection, I want to return if we are successfully connected or not.
If we get Connected I return true.
But if i get the first Disconnected, I should not return false since maybe the WS under the hood will try to connect again.

You can expose a bool getter called willRetry which can solve this ambiguity.

Or if there is something I am missing please do tell :)

      await for (ConnectionState event in _ws!.connection) {
        if (event is Connected) {
          return true;
        }
        if (event is Disconnected) {  // (&& _ws!.willRetry == false)
          return false; //maybe it will try connecting again?
        }
      }

fix: Cannot add new events after calling close

Description

Open a socket, listen to messages, run socket.close();

Steps To Reproduce

  1. Open a socket
  2. Await the Connected state
  3. Listen to messages (2-3 msgs / sec) and print(event)
  4. Close socket

Expected Behavior

Socket closes peacefully.

Screenshots

I/flutter (27831): Bad state: Cannot add new events after calling close
I/flutter (27831): #0 _BroadcastStreamController.add (dart:async/broadcast_stream_controller.dart:243:24)
I/flutter (27831): #1 _RootZone.runUnaryGuarded (dart:async/zone.dart:1586:10)
I/flutter (27831): #2 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
I/flutter (27831): #3 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7)
I/flutter (27831): #4 _ForwardingStreamSubscription._add (dart:async/stream_pipe.dart:123:11)
I/flutter (27831): #5 _HandleErrorStream._handleData (dart:async/stream_pipe.dart:253:10)
I/flutter (27831): #6 _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:153:13)
I/flutter (27831): #7 _RootZone.runUnaryGuarded (dart:async/zone.dart:1586:10)
I/flutter (27831): #8 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
I/flutter (27831): #9 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7)
I/flutter (27831): #10 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:774:19)
I/flutter (27831): #11 _StreamController._add (dart:async/stream_controller.dart:648:7)
I/flutter (27831): #12 _StreamController.add (dart:async/stream_controller.dart:596:

Additional Context

Add any other context about the problem here.

fix: timeout does not have any effect

Description

I am not sure if I understand the function of timeout yet so if this is not a bug accept my apologies.
I think timeout means, if client can not connect to the WS server for timeout duration, the client will not keep on trying to connect anymore. It will give up!

Steps To Reproduce

When I don't supply a timeout in the WS, it should use the default timer which is 60 seconds.

/// The default connection timeout duration.
const _defaultTimeout = Duration(seconds: 60);

I shut down my server while client is connected
Client connection becomes Reconnecting
I wait for 4 minutes and run the server again
Client connection becomes Reconnected

Expected Behavior

Expected behavior would be after the timeout, client should be Disconnected and even when the server is back up again, it should not try to connect to it.

Additional Context

In web_socket.dart the onTimeout callback is not used. So I don't know if the timeout has any effect or not.

      final ws = await connect(
        _uri.toString(),
        protocols: _protocols,
        pingInterval: _pingInterval,
      ).timeout(_timeout);  //there is an optional onTimeout

Future<Stream<dynamic>> timeout(Duration timeLimit, {FutureOr<Stream<dynamic>> Function()? onTimeout})

Lastly, when timeout occurs, will the client get a Disconnect with a timeout reason and code?

EDIT: I realized that you are using try catch to catch timeout cases. And you are calling reconnect over and over again.
If this is the expected behavior then accepting a numberOfReconnects int might be very useful imo.

feat: Add an asynchronous `close()` method

Description

I want to close the connection immediately upon receiving a specific notification from the server within the socket.messages.listen() method, like this:

socket.messages.listen((message) {
  print('onMessage');
  if (message is String && message == 'shut down connection') {
    socket.close();
  }
});

However, if the server sends another message immediately while the connection is still in the process of closing, it will cause a problem: a new message is received before the connection closed completely, causing a duplicate call to the close() method. It will throw an exception:

Unhandled exception:
Bad state: Cannot add new events after calling close
#0      _BroadcastStreamController.add (dart:async/broadcast_stream_controller.dart:243:24)
#1      ConnectionController.add (package:web_socket_client/src/connection.dart:50:17)
#2      WebSocket.close (package:web_socket_client/src/web_socket.dart:156:27)
<asynchronous suspension>

Requirements

Currently, the close() method in web_socket.dart is synchronous. Could you add an asynchronous version to avoid this situation?

application doesn't work properly in production env

I have application which is running in andorid (flutter). I have server part in .Net. Everything is working fine and I want to implement simple websocket so I use web_socket_client like below:

Future<void> main() async {
  final appVersion = await getVersion();
  final backoff = LinearBackoff(
    initial: const Duration(seconds: 0),
    increment: const Duration(seconds: 1),
    maximum: const Duration(seconds: 5),
  );
  final socket = WebSocket(
    Uri.parse('ws://my_api_address/ws'),
    backoff: backoff,
  );

  socket.connection.listen((event) {
    if (event is Connected ||
        event is Reconnecting ||
        event is Reconnected ||
        event is Connecting) {
      runApp(
        StreamBuilder(
          stream: socket.messages,
          builder: (context, snapshot) {
            if (snapshot.hasData && snapshot.data != appVersion) {
              return RequiredVersionAlert(
                  appVersion: appVersion, requiredVersion: snapshot.data);
            }
            return const App();
          },
        ),
      );
    }
  });
}

There are two streams, first one for connection with server status, second one for message from socket. In the local env everything is forking fine, I didn't spot any issues, but if I try to run in production env (.net app is deploing via IIS windows 10) there is something strange, the client (flutter) performance is very bad, application is stuttering and reacting very slow.
There is my code for websocket in server:

public class WebSocketController : ControllerBase
{
    private const string version = "3.0.0";
    [HttpGet("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }

    private static async Task Echo(WebSocket webSocket)
    {
        while (webSocket.State == WebSocketState.Open)
        {
            var versionBytes = Encoding.UTF8.GetBytes(version);
            await webSocket.SendAsync(new ArraySegment<byte>(versionBytes), WebSocketMessageType.Text, true, CancellationToken.None);
        }

        await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
    }
}

The websocket goal is simple, it's just sending application version string. Maybe there is some problem with server settings (firewall etc.) or LinearBackoff settings?

Expose a method to update connection uri in case of passing token parameter

URI is init each time Websocket instance is create, and reuse it for every reconnect attempt. That is sometime a problem.
If I pass a jwt token to the uri then if the token is expired I always have to call the method close and create new WebSocket instance, update the uri with new token.
It is convenient if the uri value can be expose and modify outside.

feat: a function to generate URI before every connecting

Description

I want to add the user authentication token as query param to URI, but the token has short lifetime, while trying reconnect after token expires, there is no way to update the token, it will stuck looping.

WebSocket(
  uri: () => "wss://localhost/?_token=xxxxx", // we can use the callback to retrieve valid token before connects.
);

Requirements

  • A function to generate URI before every connect happens.

Additional Context

No.

fix: headers not included in request on web platform

Description

When specifying headers in the connect request, they are not included in the server request when the platform is web.

(It works on linux)

Steps To Reproduce

v0.1.1

import 'package:web_socket_client/web_socket_client.dart';

WebSocket connect(url) {
  return WebSocket(url, headers: {"test": "header"});
}

Expected Behavior

Headers to be included in the network request.

Screenshots

It works on linux:

Screenshot from 2024-04-27 07-12-56

but not on web (chrome):

Screenshot from 2024-04-27 07-25-31

Safari: 'null' is not a valid value for binaryType; binaryType remains unchanged.

Description

'null' is not a valid value for binaryType; binaryType remains unchanged.

This causes frequent reconnects and messages aren't coming through the messages stream. Seems to only happen in Safari from what I can tell so far.

Steps To Reproduce

name: socket_test
description: A new Flutter project.
publish_to: 'none'
version: 0.1.0

environment:
  sdk: '>=3.1.5 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  web_socket_client: ^0.1.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:web_socket_client/web_socket_client.dart' as ws;

void main() {
  runApp(const MainApp());
}

class MainApp extends StatefulWidget {
  const MainApp({super.key});

  @override
  State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> {
  ws.WebSocket? _socket;
  StreamSubscription? _socketMessageSub;
  StreamSubscription? _socketConnectionSub;

  @override
  void initState() {
    startSocket();
    super.initState();
  }

  @override
  void dispose() {
    _socketConnectionSub?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(
          child: Text('Hello World!'),
        ),
      ),
    );
  }

  void startSocket() async {
    print('startSocket()');

    _socket = ws.WebSocket(
      Uri.parse(
        'wss://socketsbay.com/wss/v2/1/demo/',
      ),
    );
    _listenToSocketConnection();
  }

  void _listenToSocketConnection() {
    print('_listenToSocketConnection()');

    _socketConnectionSub = _socket?.connection.listen(
      _onSocketConnectionChanged,
    );
  }

  void _listenToSocketMessages() async {
    print('_listenToSocketMessages()');

    await _socketMessageSub?.cancel();

    _socketMessageSub = _socket?.messages.listen(
      _onSocketMessageReceived,
    );
  }

  void _onSocketConnectionChanged(ws.ConnectionState cs) {
    print('_onSocketConnectionChanged() - $cs');

    if (cs is ws.Connected || cs is ws.Reconnected) {
      _listenToSocketMessages();
    }
  }

  void _onSocketMessageReceived(dynamic data) {
    print('_onSocketMessageReceived() - $data');
  }
}

Screenshots

Screenshot 2024-02-05 at 9 09 00 AM

Error: When the server sends a message or receives a message, the connection is being disrupted.

I am trying to connect a Flutter application to a server that I developed in .NET Core. After establishing the connection, the application disconnects when it receives a message. I have been unable to find a solution. When I connect to the server using Postman, Hercules, or other client applications, there are no issues. However, I cannot establish a connection with this package.

There may be a mismatch during the handshake process. It is possible that the header is not coming correctly due to the parsing structure ("\r\n").
Error:
HttpException (HttpException: Invalid request method, uri = http://localhost:8080)
Below is a simple example of the server application and a code snippet for a basic client in Flutter:

Server code:

using XiaoFeng.Net;

namespace AppserverTest
{
    internal class Program
    {
        static NetServer<ServerSession> server8080 = new NetServer<ServerSession>(8080);

        static void Main(string[] args)
        {
            server8080.OnStart += Server8080_OnStart;
            server8080.OnStop += Server8080_OnStop;
            server8080.OnDisconnected += Server8080_OnDisconnected;
            server8080.OnNewConnection += Server8080_OnNewConnection;
            server8080.OnMessage += Server8080_OnMessage;

            server8080.Start();
            Console.ReadLine();
        }

        private static void Server8080_OnStop(ISocket socket, EventArgs e)
        {
            Console.WriteLine("8080 stopped");
        }

        private static void Server8080_OnStart(ISocket socket, EventArgs e)
        {
            Console.WriteLine("8080 started");
        }

        private static void Server8080_OnMessage(INetSession session, string message, EventArgs e)
        {
            Console.WriteLine(message);
            session.Send("Hello");
        }

        private static void Server8080_OnNewConnection(INetSession session, EventArgs e)
        {
            Console.WriteLine("connected: " + session.Headers);
            session.Send("hello");
        }

        private static void Server8080_OnDisconnected(INetSession session, EventArgs e)
        {
            Console.WriteLine("8080 disconnected");
        }
    }
}

Client:

import 'package:web_socket_client/web_socket_client.dart';

void main() async {
  final uri = Uri.parse('ws://127.0.0.1:8080');
  const backoff = ConstantBackoff(Duration(seconds: 1));
  final socket = WebSocket(uri, backoff: backoff);

  socket.connection.listen((state) => print('state: "$state"'));

  socket.messages.listen((message) {
    print('message: "$message"');
    socket.send("hi server");
  });
}

Please note that the provided code snippets are for reference purposes, and the issue you are facing may require further analysis and debugging.

fix: web package dependency

Description

Since v0.1.1 package web_socket_client is not compatible with package_info_plus anymore.

Is it possible to bump the dependency on package web higher as this might resolve this issue?

Dependency resolution output


Resolving dependencies... 
Because web_socket_client >=0.1.1 depends on web ^0.4.0 and package_info_plus 6.0.0 depends on web >=0.5.0 <=0.6.0,
  web_socket_client >=0.1.1 is incompatible with package_info_plus 6.0.0.
And because no versions of package_info_plus match >6.0.0 <7.0.0, web_socket_client >=0.1.1 is incompatible with
  package_info_plus ^6.0.0.
So, because buzz depends on both package_info_plus ^6.0.0 and web_socket_client ^0.1.1, version solving failed.


You can try one of the following suggestions to make the pubspec resolve:
* Consider downgrading your constraint on web_socket_client: flutter pub add web_socket_client:^0.1.0
* Consider downgrading your constraint on package_info_plus: flutter pub add package_info_plus:^5.0.1

feat: reconnect on network change or app wake

I have used this package to replace some custom reconnect code because I think it does a better job than we were doing. However one issue with the this package is that when the network switches, from Wifi to Cellular for example, there is no reconnection attempt made because often the WebSocket class doesn't register it as a dropped connection. My fix was to use the connectivity_plus package to look for changes to the network, ignoring changes to and from no connection since this package already handles disconnects, to create a reliable long running websocket connection.

There are also issues where a websocket connection will stop responding after the app has gone into the background but hasn't been unloaded from memory. In this case we need to check if the app has resumed and assume that after a certain amount of time it has been paused that we should force a reconnect.

It would be great if this client could handle those cases, because maintaining a reliable websocket connection in flutter isn't easy.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.