一转眼来到国庆假期,核酸未停疫情还在,假期是个好好充电的日子,休息之余总结之前的工作度过一个充实的假期,但愿疫情快点过去,大家能回归到正常生活吧
上一篇写到了flutter的开发框架封装,其中用到了DioManager这个类,这个类主要是利用flutter框架提供的dio对网络层http请求做一个封装,另外由于业务需要,也封装了WebSocket请求,这里整理了一下具体的封装流程,欢迎一起交流讨论~
http请求封装
封装数据传输Bean基础类
这里以WanAndroid的Api为例
- data是泛型T,返回的数据内容
- errorCode是后端返回的int类型的业务状态码
- errorMsg是错误信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35class BaseResult<T> {
BaseResult({
this.data,
this.errorCode,
this.errorMsg,
});
factory BaseResult.fromJson(Map<String, dynamic> json) {
return BaseResult(
data: json['data'],
errorCode: json['errorCode'],
errorMsg: json['errorMsg']);
}
/**
* 不显示错误toast
*/
bool isSuccess() {
return errorCode == 0;
}
T? data;
int? errorCode;
String? errorMsg;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
if (data != null) {
map['data'] = data;
}
map['errorCode'] = errorCode;
map['errorMsg'] = errorMsg;
return map;
}
}单例模式
构造函数私有化,懒汉式构造单例1
2
3
4
5
6
7
8static DioManager? instance;
static DioManager? getInstance() {
instance ??= DioManager._internal();
return instance;
}
DioManager._internal();声明baseUrl和全局Header
1
2
3
4
5
6
7
8
9
10static String getAppUrl() {
return "https://www.wanandroid.com";
}
static Map<String, dynamic> baseHeaders = {
"X-SYSTEM": "Android",
"LANG": "zh_CN",
"x-app-token": "",
"X-VERSION": ""
};初始化DioManager
dio对象全局唯一,DioManager的构造函数中需要对options进行相关配置,全局header和baseUrl以及连接超时,接收超时,发送超时,添加日志拦截器,设置数据的返回格式为Json1
2
3
4
5
6
7
8
9
10
11
12
13
14Dio dio = Dio();
DioManager() {
dio.options.headers = baseHeaders;
dio.options.baseUrl = getAppUrl();
dio.options.connectTimeout = 15000;
dio.options.receiveTimeout = 15000;
dio.options.sendTimeout = 30000;
// 是否开启请求日志
if (Global.isDebug) {
dio.interceptors.addAll([LogInterceptor()]);
}
dio.options.responseType = ResponseType.json;
}封装get和post请求
- post请求有表单和body两种方式,isForm为true时为表单请求默认为false
- _request方法中统一处理,调用dio的request方法传path,data,queryParameters和options发起请求,根据post和get请求配置options的method,根据post表单和body两种请求方式配置contentType,如果是get请求携带的参数传queryParameters,post请求携带的参数传data
- dio的request需要try-catch捕获异常,如果捕获到异常且needToast为true时通过toast展示异常code
- 获取到data数据后,如果是string则直接反序列化,否则调用Bean基础类BaseResult的fromJson反序列化,反序列化操作需要try-catch捕获异常,如果成功直接返回结果,否则返回序列化失败,如果当前是错误状态码且needToast为true时通过toast展示toast
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56Future<BaseResult> getRequest(String url, {Map<String, dynamic>? params}) {
return _request(
url,
queryParameters: params,
options: Options(method: "GET"),
);
}
Future<BaseResult> postRequest(String url,
{dynamic params, bool isForm = false}) {
final contentType =
isForm ? Headers.formUrlEncodedContentType : Headers.jsonContentType;
final options = Options(
method: "POST",
extra: {"dio_content_type": contentType},
contentType: contentType);
return _request(url, data: params, options: options);
}
Future<BaseResult> _request(String path,
{data,
Map<String, dynamic>? queryParameters,
Options? options,
bool needToast = true}) async {
final Response response;
try {
response = await dio.request(path,
data: data, queryParameters: queryParameters, options: options);
} on DioError catch (error, stackTrace) {
var checkedError = _checkError(error);
final statusCode =
checkedError.response?.statusCode ?? ResultCode.NO_NETWORK;
if (needToast) {
showToast("network_error(${statusCode.toString()})");
}
if (Global.isDebug) Future.error(checkedError, stackTrace);
return BaseResult(errorCode: statusCode, errorMsg: error.message);
}
try {
//防止解析失败
var data = response.data;
if (data is String) {
data = json.decode(data);
}
var baseResult = BaseResult.fromJson(data);
if (!baseResult.isSuccess() && needToast) {
showToast(baseResult.errorMsg ?? "");
}
return baseResult;
} catch (ex, stackTrace) {
if (Global.isDebug) Future.error(ex, stackTrace);
return BaseResult(
errorCode: ResultCode.JSON_ERROR, errorMsg: ex.toString());
}
}错误码统一处理
如果返回的DioError的response为空或statusCode为空统一处理为没有网络,其余的错误类型转换为自定义的错误码便于混合开发时统计最后附上DioManager完整的源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42DioError _checkError(DioError error) {
// 如果response为空构造没有网络的response
// 如果statusCode为空直接赋值为没有网络
/// 统一认为是没有网络
Response errorResponse = error.response ??
Response(
statusCode: ResultCode.NO_NETWORK,
requestOptions: error.requestOptions);
errorResponse.statusCode ??= ResultCode.NO_NETWORK;
/// 自定义请求超时错误码
if (error.type == DioErrorType.connectTimeout) {
errorResponse.statusCode = ResultCode.CONNECT_TIMEOUT;
} else if (error.type == DioErrorType.receiveTimeout) {
errorResponse.statusCode = ResultCode.RECEIVE_TIMEOUT;
} else if (error.type == DioErrorType.sendTimeout) {
errorResponse.statusCode = ResultCode.SEND_TIMEOUT;
}
error.response = errorResponse;
return error;
}
class ResultCode {
/// 业务错误
static const BIZ_ERROR = -111;
/// json解析异常
static const JSON_ERROR = -100;
/// 没有网络
static const NO_NETWORK = -101;
///连接超时
static const CONNECT_TIMEOUT = -102;
/// 接收超时
static const RECEIVE_TIMEOUT = -103;
///写数据超时
static const SEND_TIMEOUT = -104;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135class DioManager {
static DioManager? instance;
factory DioManager() {
instance ??= DioManager();
return instance!;
}
static String getAppUrl() {
return "https://www.wanandroid.com";
}
static Map<String, dynamic> baseHeaders = {
"X-SYSTEM": "Android",
"LANG": "zh_CN",
"x-app-token": "",
"X-VERSION": ""
};
Dio dio = Dio();
DioManager._internal() {
dio.options.headers = baseHeaders;
dio.options.baseUrl = getAppUrl();
dio.options.connectTimeout = 15000;
dio.options.receiveTimeout = 15000;
dio.options.sendTimeout = 30000;
// 是否开启请求日志
if (Global.isDebug) {
dio.interceptors.addAll([LogInterceptor()]);
}
dio.options.responseType = ResponseType.json;
}
Future<BaseResult> getRequest(String url, {Map<String, dynamic>? params}) {
return _request(
url,
queryParameters: params,
options: Options(method: "GET"),
);
}
Future<BaseResult> postRequest(String url,
{dynamic params, bool isForm = false}) {
final contentType =
isForm ? Headers.formUrlEncodedContentType : Headers.jsonContentType;
final options = Options(
method: "POST",
extra: {"dio_content_type": contentType},
contentType: contentType);
return _request(url, data: params, options: options);
}
Future<BaseResult> _request(String path,
{data,
Map<String, dynamic>? queryParameters,
Options? options,
bool needToast = true}) async {
final Response response;
try {
response = await dio.request(path,
data: data, queryParameters: queryParameters, options: options);
} on DioError catch (error, stackTrace) {
var checkedError = _checkError(error);
final statusCode =
checkedError.response?.statusCode ?? ResultCode.NO_NETWORK;
if (needToast) {
showToast("network_error(${statusCode.toString()})");
}
if (Global.isDebug) Future.error(checkedError, stackTrace);
return BaseResult(errorCode: statusCode, errorMsg: error.message);
}
try {
//防止解析失败
var data = response.data;
if (data is String) {
data = json.decode(data);
}
var baseResult = BaseResult.fromJson(data);
if (!baseResult.isNotShowNetErrorToast() && needToast) {
showToast(baseResult.errorMsg ?? "");
}
return baseResult;
} catch (ex, stackTrace) {
if (Global.isDebug) Future.error(ex, stackTrace);
return BaseResult(
errorCode: ResultCode.JSON_ERROR, errorMsg: ex.toString());
}
}
DioError _checkError(DioError error) {
/// 统一认为是没有网络
// 如果response为空构造没有网络的response
// 如果statusCode为空直接赋值为没有网络
Response errorResponse = error.response ??
Response(
statusCode: ResultCode.NO_NETWORK,
requestOptions: error.requestOptions);
errorResponse.statusCode ??= ResultCode.NO_NETWORK;
/// 自定义请求超时错误码
if (error.type == DioErrorType.connectTimeout) {
errorResponse.statusCode = ResultCode.CONNECT_TIMEOUT;
} else if (error.type == DioErrorType.receiveTimeout) {
errorResponse.statusCode = ResultCode.RECEIVE_TIMEOUT;
} else if (error.type == DioErrorType.sendTimeout) {
errorResponse.statusCode = ResultCode.SEND_TIMEOUT;
}
error.response = errorResponse;
return error;
}
}
/// 跟宿主定义的http网络错误码保持一致
class ResultCode {
/// 业务错误
static const BIZ_ERROR = -111;
/// json解析异常
static const JSON_ERROR = -100;
/// 没有网络
static const NO_NETWORK = -101;
///连接超时
static const CONNECT_TIMEOUT = -102;
///It occurs when receiving timeout.
static const RECEIVE_TIMEOUT = -103;
///写数据超时
static const SEND_TIMEOUT = -104;
}WebSocket请求封装
封装数据传输Bean基础类
根据服务端返回的内容封装一个bean类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class WsBaseBean {
Decimal? changeRate;
Decimal? lastTradedPrice;
String? symbolCode;
Decimal? volValue;
WsBaseBean(
{this.changeRate, this.lastTradedPrice, this.symbolCode, this.volValue});
factory WsBaseBean.fromJson(Map<String, dynamic> json) {
return WsBaseBean(
changeRate: Decimal.tryParse("${json["changeRate"] ?? 0.0}"),
lastTradedPrice: Decimal.tryParse("${json["lastTradedPrice"] ?? 0.0}"),
symbolCode: json["symbolCode"],
volValue: Decimal.tryParse("${json["volValue"] ?? 0.0}"));
}
}单例模式
1
2
3
4
5
6
7
8
9
10
11
12/// 单例对象
static WebSocketUtility? _socket;
/// 内部构造方法,可避免外部暴露构造函数,进行实例化
WebSocketUtility._internal();
/// 获取单例内部方法
factory WebSocketUtility() {
// 只能有一个实例
_socket ??= WebSocketUtility._internal();
return _socket!;
}声明Websocket地址和连接状态
1
2
3
4
5
6
7const String _SOCKET_URL = 'ws://192.168.3.123:8181/test';
enum SocketStatus {
socketStatusConnected, // 已连接
socketStatusFailed, // 失败
socketStatusClosed, // 连接关闭
}声明变量
- 定义回调,接连错误,接收消息,开始连接
- 定义IOWebSocketChannel实现WebSocket通信
- 定义心跳定时器和心跳间隔,每隔固定时长发送心跳
- 定义重连定时器和当前重连次数和最大重连次数,未达到最大重连次数时每隔固定时长重新连接
- 定义当前socket的连接状态
1
2
3
4
5
6
7
8
9
10IOWebSocketChannel? _webSocket; // WebSocket
SocketStatus? _socketStatus; // socket状态
Timer? _heartBeat; // 心跳定时器
final int _heartTimes = 3000; // 心跳间隔(毫秒)
final int _reconnectMaxCount = 60; // 重连次数,默认60次
int _reconnectTimes = 0; // 重连计数器
Timer? _reconnectTimer; // 重连定时器
late Function onError; // 连接错误回调
late Function onOpen; // 开始连接回调
late Function onMessage; // 接收消息回调开始连接
- 初始化IOWebSocketChannel开始连接
- 设置_socketStatus连接状态
- 重置重连计数和定时器
- 调用onOpen回调
- 接收消息回调处理,收到消息调onMessage回调,连接错误调onError回调,连接推出时尝试重连
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25/// 开启WebSocket连接
void openSocket() {
closeSocket();
_webSocket = IOWebSocketChannel.connect(_SOCKET_URL);
print('WebSocket连接成功: $_SOCKET_URL');
// 连接成功,返回WebSocket实例
_socketStatus = SocketStatus.socketStatusConnected;
// 连接成功,重置重连计数器
_reconnectTimes = 0;
if (_reconnectTimer != null) {
_reconnectTimer?.cancel();
_reconnectTimer = null;
}
onOpen();
// 接收消息
_webSocket?.stream.listen((data) => onMessage(data), onError: (e) {
WebSocketChannelException ex = e;
_socketStatus = SocketStatus.socketStatusFailed;
onError(ex.message);
closeSocket();
}, onDone: () {
print('closed');
reconnect();
});
}销毁心跳
取消心跳定时器1
2
3
4
5
6void destroyHeartBeat() {
if (_heartBeat != null) {
_heartBeat?.cancel();
_heartBeat = null;
}
}初始化并开启心跳
如果心跳定时器已经初始化则销毁,重新初始化并开启心跳发送任务1
2
3
4
5
6void initHeartBeat() {
destroyHeartBeat();
_heartBeat = Timer.periodic(Duration(milliseconds: _heartTimes), (timer) {
sendMessage('{"module": "HEART_CHECK", "message": "请求心跳"}');
});
}发送WebSocket消息
如果当前的连接状态是已连接则发送消息,否则打印当前状态1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19/// 发送WebSocket消息
void sendMessage(message) {
if (_webSocket != null) {
switch (_socketStatus) {
case SocketStatus.socketStatusConnected:
print('发送中:' + message);
_webSocket?.sink.add(message);
break;
case SocketStatus.socketStatusClosed:
print('连接已关闭');
break;
case SocketStatus.socketStatusFailed:
print('发送失败');
break;
default:
break;
}
}
}重连机制
如果当前未达到最大连接次数则重连次数+1,开启重连定时器,执行WebSocket连接,否则如果重连定时器不为空则销毁最后附上WebSocketUtility完整的源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/// 重连机制
void reconnect() {
if (_reconnectTimes < _reconnectMaxCount) {
_reconnectTimes++;
_reconnectTimer =
Timer.periodic(Duration(milliseconds: _heartTimes), (timer) {
openSocket();
});
} else {
if (_reconnectTimer != null) {
print('重连次数超过最大次数');
_reconnectTimer?.cancel();
_reconnectTimer = null;
}
return;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136/// WebSocket地址
const String _SOCKET_URL = 'ws://192.168.3.123:8181/test';
/// WebSocket状态
enum SocketStatus {
socketStatusConnected, // 已连接
socketStatusFailed, // 失败
socketStatusClosed, // 连接关闭
}
class WebSocketUtility {
/// 单例对象
static WebSocketUtility? _socket;
/// 内部构造方法,可避免外部暴露构造函数,进行实例化
WebSocketUtility._internal();
/// 获取单例内部方法
factory WebSocketUtility() {
// 只能有一个实例
_socket ??= WebSocketUtility._internal();
return _socket!;
}
IOWebSocketChannel? _webSocket; // WebSocket
SocketStatus? _socketStatus; // socket状态
Timer? _heartBeat; // 心跳定时器
final int _heartTimes = 3000; // 心跳间隔(毫秒)
final int _reconnectMaxCount = 60; // 重连次数,默认60次
int _reconnectTimes = 0; // 重连计数器
Timer? _reconnectTimer; // 重连定时器
late Function onError; // 连接错误回调
late Function onOpen; // 连接开启回调
late Function onMessage; // 接收消息回调
/// 初始化WebSocket
void initWebSocket(
{required Function onOpen,
required Function onMessage,
required Function onError}) {
this.onOpen = onOpen;
this.onMessage = onMessage;
this.onError = onError;
openSocket();
}
/// 开启WebSocket连接
void openSocket() {
closeSocket();
_webSocket = IOWebSocketChannel.connect(_SOCKET_URL);
print('WebSocket连接成功: $_SOCKET_URL');
// 连接成功,返回WebSocket实例
_socketStatus = SocketStatus.socketStatusConnected;
// 连接成功,重置重连计数器
_reconnectTimes = 0;
if (_reconnectTimer != null) {
_reconnectTimer?.cancel();
_reconnectTimer = null;
}
onOpen();
// 接收消息
_webSocket?.stream.listen((data) => onMessage(data), onError: (e) {
WebSocketChannelException ex = e;
_socketStatus = SocketStatus.socketStatusFailed;
onError(ex.message);
closeSocket();
}, onDone: () {
print('closed');
reconnect();
});
}
/// 初始化心跳
void initHeartBeat() {
destroyHeartBeat();
_heartBeat = Timer.periodic(Duration(milliseconds: _heartTimes), (timer) {
sendMessage('{"module": "HEART_CHECK", "message": "请求心跳"}');
});
}
/// 销毁心跳
void destroyHeartBeat() {
if (_heartBeat != null) {
_heartBeat?.cancel();
_heartBeat = null;
}
}
/// 关闭WebSocket
void closeSocket() {
if (_webSocket != null) {
print('WebSocket连接关闭');
_webSocket?.sink.close();
destroyHeartBeat();
_socketStatus = SocketStatus.socketStatusClosed;
}
}
/// 发送WebSocket消息
void sendMessage(message) {
if (_webSocket != null) {
switch (_socketStatus) {
case SocketStatus.socketStatusConnected:
print('发送中:' + message);
_webSocket?.sink.add(message);
break;
case SocketStatus.socketStatusClosed:
print('连接已关闭');
break;
case SocketStatus.socketStatusFailed:
print('发送失败');
break;
default:
break;
}
}
}
/// 重连机制
void reconnect() {
if (_reconnectTimes < _reconnectMaxCount) {
_reconnectTimes++;
_reconnectTimer =
Timer.periodic(Duration(milliseconds: _heartTimes), (timer) {
openSocket();
});
} else {
if (_reconnectTimer != null) {
print('重连次数超过最大次数');
_reconnectTimer?.cancel();
_reconnectTimer = null;
}
return;
}
}
}在Widget中使用
在initState中初始化WebSocket并在连接后开启心跳发送任务与服务端保持通信连接,在消息接收中反序列化实时刷新页面数据看下flutter客户端的控制台输出效果1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44class SocketTestWidget extends StatefulWidget {
const SocketTestWidget({Key? key}) : super(key: key);
State<SocketTestWidget> createState() => _SocketTestWidgetState();
}
class _SocketTestWidgetState extends State<SocketTestWidget> {
dynamic data;
void initState() {
super.initState();
WebSocketUtility().initWebSocket(onOpen: () {
WebSocketUtility().initHeartBeat();
}, onMessage: (data) {
WsBaseBean wsBaseBean = WsBaseBean.fromJson(jsonDecode(data));
setState(() {
this.data = wsBaseBean.changeRate;
});
}, onError: (e) {
print("WebSocketUtility" + e);
});
}
void dispose() {
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("SocketTest"),
),
body: Center(
child: ListView(
children: [Text('收到数据: ${data}')],
),
),
);
}
}
最后我们可以用python写个简单的服务端脚本测试一下
1 | import asyncio |
看下服务端的控制台输出效果