5
votes

gRPC dans Flutter se bloque en l'absence d'Internet

Je développe une application Flutter utilisant gRPC et tout fonctionnait correctement jusqu'à ce que je décide de voir ce qui se passe s'il n'y a pas de connexion Internet.

Après avoir fait cela et fait une demande, j'obtiens l'erreur suivante:

E / flutter (26480): Erreur gRPC (14, Erreur lors de l'appel: Mauvais état: La connexion http / 2 n'est plus active et ne peut donc pas être utilisée pour créer de nouveaux flux.)

Le problème est que même après avoir réactivé la connexion, l'erreur persiste.
Dois-je recréer le clientChannel?

const String serverUrl = 'theaddress.com';
const int serverPort = 50051;

final ClientChannel defaultClientChannel = ClientChannel(
  serverUrl,
  port: serverPort,
  options: const ChannelOptions(
    credentials: const ChannelCredentials.insecure(),
  ),
);

Je voudrais simplement lancer quelques erreurs, mais cela fonctionne correctement une fois la connexion Internet rétablie.


0 commentaires

3 Réponses :


3
votes

Je suppose que vous êtes l'une des rares personnes à essayer.

Les connexions GRPC prennent un peu de temps pour créer une nouvelle connexion, pas seulement dans Dart, mais dans tous les autres langages. Si vous le souhaitez, vous pouvez mettre un écouteur catch sur le code d'erreur 14 et arrêter manuellement la connexion et vous reconnecter. Il y a aussi l'option de canal idleTimeout qui pourrait vous être utile, la valeur par défaut est de 5 minutes dans grpc-dart

Il y a eu un correctif pour le problème de plantage non résolu https://github.com/ grpc / grpc-dart / issues / 131 , alors essayez de mettre à jour vos dépendances ( grpc-dart ) ce qui évitera le crash, mais le problème de la reconnexion sur le réseau pourrait persister.

Après cette correction, les plantages se sont arrêtés, mais le problème de connexion obsolète persiste également pour moi. J'ai eu recours à l'affichage de barres-collations avec des phrases telles que "Connexion aux serveurs impossible, veuillez réessayer dans quelques minutes".


3 commentaires

Et quand rétablissez-vous la connexion?


J'utiliserais ceci: pub.dartlang.org/packages/connectivity pour écouter la disponibilité du réseau puis essayez de vous reconnecter. Vous pouvez concevoir une petite bibliothèque qui peut le faire automatiquement pour GRPC. Ainsi, lorsque le réseau n'est pas disponible, montrez simplement à un snack-bar que le réseau n'est pas disponible ou quelque chose de similaire. Et quand c'est le cas, vous aurez toujours une connexion réseau valide. Je n'ai pas encore fait cela, c'est dans mon pipeline, je posterai un exemple de code une fois que j'en aurai terminé! :)


@ishaan salut, avez-vous réussi à vous reconnecter avec GRPC en utilisant Flutter / Dart?



1
votes

Je n'utilise pas gRPC jusqu'à aujourd'hui.

Depuis que j'ai pris le temps d'essayer de simuler cette erreur, je posterai ici ma réponse, mais toutes mes informations ont été dirigées par @ishann réponse que j'ai votée et qui devrait être la j'en ai accepté un.

Je viens d'essayer fléchette bonjour l'exemple du monde .

J'ai le serveur en cours d'exécution sur ma machine et le client en tant qu'application Flutter.

Lorsque je n'exécute pas le serveur, j'obtiens l'erreur

Erreur gRPC (14, Erreur de connexion: SocketException:

entrer l'image description here

Mais dès que le serveur démarre, tout commence à fonctionner comme prévu, mais j'ai réalisé que je recréais le canal à chaque fois, donc ce n'est pas l'OP scénario.

C'est mon premier code Flutter:

$ flutter doctor -v
[✓] Flutter (Channel beta, v1.0.0, on Mac OS X 10.14.1 18B75, locale en-IT)
    • Flutter version 1.0.0 at /Users/shadowsheep/flutter/flutter
    • Framework revision 5391447fae (6 weeks ago), 2018-11-29 19:41:26 -0800
    • Engine revision 7375a0f414
    • Dart version 2.1.0 (build 2.1.0-dev.9.4 f9ebf21297)

[✓] Android toolchain - develop for Android devices (Android SDK 28.0.3)
    • Android SDK at /Users/shadowsheep/Library/Android/sdk
    • Android NDK location not configured (optional; useful for native profiling support)
    • Platform android-28, build-tools 28.0.3
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1248-b01)
    • All Android licenses accepted.

[✓] iOS toolchain - develop for iOS devices (Xcode 10.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 10.1, Build version 10B61
    • ios-deploy 1.9.4
    • CocoaPods version 1.5.3

[✓] Android Studio (version 3.3)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin version 31.3.3
    • Dart plugin version 182.5124
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1248-b01)

[✓] VS Code (version 1.30.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 2.21.1

[✓] Connected device (1 available)
    [...]

• No issues found!

Même comportement si je mets un appareil en mode avion et de revenir à une connexion wifi / lte normale.

Avec cet autre projet de terrain de jeu, j'ai reproduit soit

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_test_grpc/grpc/generated/helloworld.pbgrpc.dart';
import 'package:grpc/grpc.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  ClientChannel _channel;

  @override
  void dispose() {
    _shutdown();
    super.dispose();
  }

  void _shutdown() async {
    if (null != _channel) {
      print('shutting down...');
      await _channel.shutdown();
      print('shut down');
      _channel = null;
    } else {
      print ('connect first');
    }
  }

  void _connect() {
    print('connecting...');
    _channel = new ClientChannel('192.168.xxx.xxx',
        port: 50051,
        options: const ChannelOptions(
            credentials: const ChannelCredentials.insecure()));
    print('connected');
  }

  void _sayHello() async {
    if (_channel != null) {
      final stub = new GreeterClient(_channel);

      final name = 'world';

      try {
        final response = await stub.sayHello(new HelloRequest()..name = name);
        print('Greeter client received: ${response.message}');
      } catch (e) {
        print('Caught error: $e');
        //sleep(Duration(seconds: 2));
      }

      //print('exiting');
      //await channel.shutdown();
    } else {
      print('connect first!');
    }
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: Padding(
        padding: const EdgeInsets.only(left: 36.0),
        child: Row(
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: FloatingActionButton(
                onPressed: _connect,
                tooltip: 'Increment',
                child: Icon(Icons.wifi),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: FloatingActionButton(
                onPressed: _sayHello,
                tooltip: 'Increment',
                child: Icon(Icons.send),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: FloatingActionButton(
                onPressed: _shutdown,
                tooltip: 'Increment',
                child: Icon(Icons.close),
              ),
            ),
          ],
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}


2 commentaires

Mais cela ne résout toujours pas le problème de la restauration automatique de la connexion, non?


@ MarcinSzałek ouais, c'est vrai! Je confirme ici que lorsque vous obtenez l'erreur 14 avec la connexion http / 2 n'est plus active , vous devez recréer le canal. Dans ce scénario réel, vous ne pouvez pas vous reconnecter automatiquement. Avec l ' socket error à la place, vous pouvez vous reconnecter automatiquement.



5
votes

Sur la base de la suggestion de @ Ishaan, j'ai utilisé le package de connectivité pour créer un client qui se reconnecte lorsque Internet est rétabli. Jusqu'à présent, cela semble fonctionner.

import 'dart:async';

import 'package:connectivity/connectivity.dart';
import 'package:flutter_worker_app/generated/api.pbgrpc.dart';
import 'package:grpc/grpc.dart';
import 'package:rxdart/rxdart.dart';

class ConnectiveClient extends ApiClient {

  final CallOptions _options;
  final Connectivity _connectivity;
  ClientChannel _channel;
  bool hasRecentlyFailed = false;


  ConnectiveClient(this._connectivity, this._channel, {CallOptions options})
      : _options = options ?? CallOptions(),
        super(_channel) {
    //TODO: Cancel connectivity subscription
    _connectivity.onConnectivityChanged.listen((result) {
      if (hasRecentlyFailed && result != ConnectivityResult.none) {
        _restoreChannel();
      }
    });
  }

  ///Create new channel from original channel
  _restoreChannel() {
    _channel = ClientChannel(_channel.host,
        port: _channel.port, options: _channel.options);
    hasRecentlyFailed = false;
  }

  @override
  ClientCall<Q, R> $createCall<Q, R>(
      ClientMethod<Q, R> method, Stream<Q> requests,
      {CallOptions options}) {
    //create call
    BroadcastCall<Q, R> call = createChannelCall(
      method,
      requests,
      _options.mergedWith(options),
    );
    //listen if there was an error
    call.response.listen((_) {}, onError: (Object error) async {
      //Cannot connect - we assume it's internet problem
      if (error is GrpcError && error.code == StatusCode.unavailable) {
        //check connection
        _connectivity.checkConnectivity().then((result) {
          if (result != ConnectivityResult.none) {
            _restoreChannel();
          }
        });
        hasRecentlyFailed = true;
      }
    });
    //return original call
    return call;
  }

  /// Initiates a new RPC on this connection.
  /// This is copy of [ClientChannel.createCall]
  /// The only difference is that it creates [BroadcastCall] instead of [ClientCall]
  ClientCall<Q, R> createChannelCall<Q, R>(
      ClientMethod<Q, R> method, Stream<Q> requests, CallOptions options) {
    final call = new BroadcastCall(method, requests, options);
    _channel.getConnection().then((connection) {
      if (call.isCancelled) return;
      connection.dispatchCall(call);
    }, onError: call.onConnectionError);
    return call;
  }
}

///A ClientCall that can be listened multiple times
class BroadcastCall<Q, R> extends ClientCall<Q, R> {
  ///I wanted to use super.response.asBroadcastStream(), but it didn't work.
  ///I don't know why...
  BehaviorSubject<R> subject = BehaviorSubject<R>();

  BroadcastCall(
      ClientMethod<Q, R> method, Stream<Q> requests, CallOptions options)
      : super(method, requests, options) {
    super.response.listen(
          (data) => subject.add(data),
          onError: (error) => subject.addError(error),
          onDone: () => subject.close(),
        );
  }

  @override
  Stream<R> get response => subject.stream;
}


3 commentaires

Je suis content que cela ait fonctionné pour vous! Peut-être déposer un paquet de pub pour cela? ;)


Impossible car cette classe doit étendre votre classe Client générée :(


Excellente solution, au début, j'ai essayé d'utiliser super.response.asBroadcastStream () également mais avec la même erreur: (J'essaie de gérer StatusCode.unauthenticated en un seul endroit pour pouvoir actualiser le jeton à chaque fois que je reçois cette erreur. Si vous avez réussi à gérer cela sans BroadcaseCall, veuillez mettre à jour :)