【问题标题】:gRPC in Flutter crash when no internet没有互联网时 Flutter 中的 gRPC 崩溃
【发布时间】:2019-01-09 07:57:40
【问题描述】:

我正在使用 gRPC 开发 Flutter 应用程序,一切正常,直到我决定看看如果没有互联网连接会发生什么。

执行此操作并发出请求后,我收到以下错误:

E/flutter (26480): gRPC Error (14, Error making call: Bad state: http/2 连接不再活跃,因此不能用于创建新流。)

问题是即使重新启用连接后,错误仍然出现。
我必须重新创建 clientChannel 吗?

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

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

我只想抛出一些错误,但一旦互联网连接恢复正常工作。

【问题讨论】:

    标签: networking dart flutter grpc


    【解决方案1】:

    根据@Ishaan 的建议,我使用 Connectivity 包创建了一个客户端,该客户端在 Internet 备份时重新连接。到目前为止,它似乎正在工作。

    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;
    }
    

    【讨论】:

    • 我很高兴它为你解决了!也许为它放一个 pub 包? ;)
    • 不能这样做,因为这个类需要扩展你生成的Client类:(
    • 很好的解决方案,一开始我也尝试使用 super.response.asBroadcastStream() 但出现相同的错误:( 我尝试在一个地方处理 StatusCode.unauthenticated 以便能够刷新每次我收到此错误时的令牌。如果您在没有 BroadcaseCall 的情况下成功管理此问题,请更新:)
    • 连接包不检查是否有互联网连接,它使用MethodChannel来确定手机使用的连接类型,如WifiCellular data。如果手机通过路由器连接到互联网并且路由器无法访问互联网(ISP因为账单而切断了您),连接包会说您已连接到互联网,因为您仍在使用@987654325 @你手机上的服务,而实际上你不能在路由器之外发送任何请求。
    【解决方案2】:

    我猜你是少数尝试它的人之一。

    GRPC 连接需要一些时间来创建新连接,不仅在 dart 中,而且在所有其他语言中。如果需要,您可以在错误代码 14 上放置一个 catch 侦听器并手动终止连接并重新连接。还有 idleTimeout 频道选项可能对你有帮助,在 grpc-dart 中默认为 5 分钟

    修复了意外崩溃问题https://github.com/grpc/grpc-dart/issues/131,因此请尝试更新您的依赖项(grpc-dart)以防止崩溃,但网络重新连接的问题可能仍然存在。

    在此修复后,崩溃已经停止,但过时的连接问题对我来说仍然存在。我已经使用诸如“无法连接到服务器,请在几分钟后重试”之类的语句显示小吃吧。

    【讨论】:

    • 什么时候恢复连接?
    • 我会使用这个:pub.dartlang.org/packages/connectivity 来监听网络可用性,然后尝试重新连接。您可以设计一个小型库,可以自动为 GRPC 执行此操作。因此,当网络不可用时,只需显示网络不可用或类似内容的小吃吧。当它存在时,您将始终拥有有效的网络连接。我还没有这样做,它在我的管道中,一旦我完成它会发布一些示例代码! :)
    • @ishaan 嗨,您使用 Flutter/dart 重新连接 GRPC 成功了吗?
    【解决方案3】:

    直到今天我还没有使用 gRPC。

    由于我花时间尝试模拟此错误,因此我将在此处发布我的答案,但我所有的英特尔都由 @ishann 的答案领导,我已经投票了,这应该是接受了一个。

    我刚刚试过dart hello world example

    server 在我的机器上运行,client 作为 Flutter 应用程序运行。

    当我不运行服务器时,我得到了错误

    gRPC Error (14, Error connecting: SocketException:

    但是一旦服务器启动,一切都开始按预期工作,但后来我意识到我每次都在重新创建频道,所以这不是 OP 场景。

    这是我的第一个 Flutter 代码:

    void _foo() async {
      final channel = new ClientChannel('192.168.xxx.xxx',
          port: 50051,
          options: const ChannelOptions(
              credentials: const ChannelCredentials.insecure()));
      final stub = new GreeterClient(channel);
    
      final name = 'world';
    
      var _waitHelloMessage = true;
      while (_waitHelloMessage) {
        try {
          final response = await stub.sayHello(new HelloRequest()..name = name);
          print('Greeter client received: ${response.message}');
          _waitHelloMessage = false;
        } catch (e) {
          print('Caught error: $e');
          sleep(Duration(seconds: 1));
        }
      }
      print('exiting');
      await channel.shutdown();
    }
    

    如果我将设备置于 airplain 模式,然后再切换回正常的 wifi/lte 连接,则行为相同。

    使用这个其他游乐场项目,我已经复制了任一

    Caught error: gRPC Error (14, Error making call: Bad state: The http/2 connection is no longer active and can therefore not be used to make new streams.)
    

    如果不重新创建频道,您就无法从中提出,并且

    Caught error: gRPC Error (14, Error connecting: SocketException: OS Error: Connection refused, errno = 111, address = 192.168.1.58, port = 38120)
    

    (例如关闭服务器),您可以从中重新启动,而无需重新创建频道。

    前一个错误码不太容易得到,因为似乎wifi和lte连接之间的通道节流。

    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.
        );
      }
    }
    

    这是我的flutter doctor -v,如果有帮助的话:

    $ 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!
    

    【讨论】:

    • 但是还是没有解决自动恢复连接的问题吧?
    • @MarcinSzałek 是的,对!我在此确认,当您收到error 14 with http/2 connection is no longer active 时,您必须重新创建频道。在这种实际情况下,您无法自动重新连接。使用socket error,您可以自动重新连接。
    猜你喜欢
    • 2020-06-07
    • 1970-01-01
    • 2016-11-30
    • 2016-06-09
    • 1970-01-01
    • 1970-01-01
    • 2016-06-08
    • 1970-01-01
    相关资源
    最近更新 更多