背景描述
Flutter 混合项目中,一个 Flutter 页面的下拉刷新,当出现网络连接错误的时候,再怎么刷新,也连不上了。
由于服务器在海外,所以怀疑网络不稳定,这种情况也正常,但诡异的是:
- 出现问题之后,即使重新开启科学上网,也无法连接上
- 退出页面,重新进入的时候,又可以连接了。
- 并且,当退出页面的时候,Native 部分的网络也正常。
使用了 Dio 作为网络库,并且搭配了 http2 adapter
分析1:定位问题
观察日志,当出现该问题时,出现以下异常
HandshakeException: Connection terminated during handshake
首先,在 QA 同学的协助下,找到了可以复现的方式:在非科学上网的情况下,反复进出页面,不断刷新,会复现该问题。
尝试使用Flutter 单独项目跑起来,反复进入页面,发现无法复现。
比较这两者的不同:
正式项目里,反复重进页面,相当于 Flutter 模块不断重启,每次使用的 Dio 都是新的
而 Flutter 测试项目里,反复进页面,Dio 是复用的
尝试将这里改成每次创建 Dio 对象,可以出现连接中断问题,并且再次刷新,就又能连上了。
分析 2:尝试恢复
看起来是 Dio 对象的问题?一旦出现这个错误,将无法再使用?
在最外侧的 try-catch 中,尝试捕获该问题,并且重建 Dio 对象,看起来下次刷新时就可以修复了。
Future<T?> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Map<String, dynamic>? headers,
}) async {
try {
// RequestOptions options = RequestOptions();
Options options = Options();
if (headers != null) {
options.headers = headers;
}
Response<T> response = await client.dio.get<T>(
path,
queryParameters: queryParameters,
options: options,
);
return response.data;
} on DioError catch (e) {
_handleLogout(e);
if (e.type == DioErrorType.other && e.error is HandshakeException) {
// 尝试重启
client = MyNetworkClient(baseUrl: client.dio.options.baseUrl);
}
throw MyNetworkError(e);
}
}
但是为什么 Dio 对象就无法使用了呢?
此外,上述修复代码无法作用于共享了同样 client 对象的其他地方(client 是对 dio 的封装),需要搭配更多的修复代码
于是继续向下探究,是否有更合适的修复点,以及原因到底是什么
分析 3:继续追踪流程
找到抛出错误的地方:
@override
Future<ClientTransportConnection> getConnection(
RequestOptions options) async {
if (_closed) {
throw Exception(
"Can't establish connection after [ConnectionManager] closed!");
}
var uri = options.uri;
var domain = '${uri.host}:${uri.port}';
var transportState = _transportsMap[domain];
if (transportState == null) {
var _initFuture = _connectFutures[domain];
if (_initFuture == null) {
_connectFutures[domain] = _initFuture = _connect(options);
}
// ↓↓↓↓↓ 这里 await initFuture 之后就会抛出异常 ↓↓↓↓↓
transportState = await _initFuture;
if (_forceClosed) {
transportState.dispose();
} else {
_transportsMap[domain] = transportState;
var _ = _connectFutures.remove(domain);
}
} else {
// Check whether the connection is terminated, if it is, reconnecting.
if (!transportState.transport.isOpen) {
transportState.dispose();
_transportsMap[domain] = transportState = await _connect(options);
}
}
return transportState.activeTransport;
}
观察 initFuture 是啥
Future<_ClientTransportConnectionState> _connect(
RequestOptions options) async {
var uri = options.uri;
var domain = '${uri.host}:${uri.port}';
var clientConfig = ClientSetting();
if (onClientCreate != null) {
onClientCreate!(uri, clientConfig);
}
late SecureSocket socket;
try {
// Create socket
// ↓↓↓↓↓ 从 connect 里抛出该异常 ↓↓↓↓↓
socket = await SecureSocket.connect(
uri.host,
uri.port,
timeout: options.connectTimeout > 0
? Duration(milliseconds: options.connectTimeout)
: null,
context: clientConfig.context,
onBadCertificate: clientConfig.onBadCertificate,
supportedProtocols: ['h2'],
);
} on SocketException catch (e) {
if (e.osError == null) {
if (e.message.contains('timed out')) {
throw DioError(
requestOptions: options,
error: 'Connecting timed out [${options.connectTimeout}ms]',
type: DioErrorType.connectTimeout,
);
}
}
rethrow;
}
// Config a ClientTransportConnection and save it
var transport = ClientTransportConnection.viaSocket(socket);
var _transportState = _ClientTransportConnectionState(transport);
transport.onActiveStateChanged = (bool isActive) {
_transportState.isActive = isActive;
if (!isActive) {
_transportState.latestIdleTimeStamp =
DateTime.now().millisecondsSinceEpoch;
}
};
//
_transportState.delayClose(
_closed ? 50 : _idleTimeout,
() {
_transportsMap.remove(domain);
_transportState.transport.finish();
},
);
return _transportState;
}
当 connect 发生错误后,_connect 返回的 future,被保存在 _connectFutures 中,下次如果继续拿出来 await,会直接返回 exception
所以,需要在 SecureSocket.connect 对应的try-catch 中,捕获错误,并且执行 _connectFutures.remove(domain);
并且!
HandshakeException 不属于 SocketException,需要增加一个捕获的错误类型
分析 4:Future 的内部实现
之前没有重复使用过 Future,也就没有注意到这种写法的问题,
进一步往后分析,写一段简单的测试代码,如果一个 Future 执行过,并抛出了异常,那么去 await 他,之后会一直抛出异常
class Debug {
Future<void> run() async {
// await test1();
// await test2();
Future task = test3();
try {
await task;
} catch (e) {
print(e.toString());
}
for (int i = 0; i < 10; i++) {
print("i: $i");
try {
await task;
} catch (e) {
print(e.toString());
}
}
}
Future<void> test3() async {
var rnd = Random().nextInt(100);
print(rnd);
if (rnd > 50) {
throw Exception("error!!!!!");
}
}
}
参考 Future 原理和使用的一些分析文章:
zhuanlan.zhihu.com/p/450577968
Future 本身是有状态的,如果是 complete 状态(_stateValue 或者 _stateError),将会 cloneResult 并且直接进行下一步