2023-08-10 19:08:46 +08:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:convert';
|
|
|
|
|
2023-10-01 18:40:14 +08:00
|
|
|
import 'package:flutter/foundation.dart';
|
2023-08-10 19:08:46 +08:00
|
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
2023-09-09 16:48:47 +08:00
|
|
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
|
|
|
import 'package:web_socket_channel/status.dart' as status;
|
2023-08-15 10:53:30 +08:00
|
|
|
|
2024-04-09 17:23:28 +08:00
|
|
|
import '/database/box_type.dart';
|
|
|
|
import '/utils/ws_receive_callback.dart';
|
2023-08-10 19:08:46 +08:00
|
|
|
|
2023-08-27 19:14:37 +08:00
|
|
|
enum SocketStatus {
|
|
|
|
connected,
|
2023-09-17 10:27:00 +08:00
|
|
|
reconnecting,
|
2023-08-27 19:14:37 +08:00
|
|
|
error,
|
2023-09-17 10:27:00 +08:00
|
|
|
closed,
|
2023-08-27 19:14:37 +08:00
|
|
|
}
|
|
|
|
|
2023-10-01 18:40:14 +08:00
|
|
|
// const chunkSize = 1024 * 1024;
|
|
|
|
|
2023-08-10 19:08:46 +08:00
|
|
|
class WebSocketManager extends ChangeNotifier {
|
|
|
|
late Uri wsUrl;
|
|
|
|
late WebSocketChannel channel;
|
|
|
|
String id = '';
|
2023-08-27 19:14:37 +08:00
|
|
|
SocketStatus socketStatus = SocketStatus.closed;
|
2023-08-10 19:08:46 +08:00
|
|
|
Timer? heartBeatTimer;
|
|
|
|
Timer? serverTimer;
|
|
|
|
Timer? reconnectTimer;
|
2023-09-20 20:21:27 +08:00
|
|
|
int reconnectCount = 5;
|
2023-08-10 19:08:46 +08:00
|
|
|
int reconnectTimes = 0;
|
2024-03-17 20:49:51 +08:00
|
|
|
Duration heartBeatTimeout = const Duration(seconds: 30);
|
|
|
|
Duration reconnectTimeout = const Duration(seconds: 5);
|
2023-10-01 18:40:14 +08:00
|
|
|
Map<String, Timer> sendImageTimer = {};
|
2023-08-10 19:08:46 +08:00
|
|
|
|
2023-09-16 11:33:57 +08:00
|
|
|
void connect(String userId, bool isReconnect) {
|
2023-08-10 19:08:46 +08:00
|
|
|
id = userId;
|
2023-09-16 11:33:57 +08:00
|
|
|
wsUrl = Uri.parse('ws://10.0.2.2:8000/ws/$id?is_reconnect=$isReconnect');
|
2023-09-20 20:21:27 +08:00
|
|
|
if (isReconnect) {
|
|
|
|
socketStatus = SocketStatus.reconnecting;
|
|
|
|
notifyListeners();
|
|
|
|
}
|
2024-03-17 20:49:51 +08:00
|
|
|
// This doesn't block the program whether it connect the server or not
|
|
|
|
// So heartBeat will be execute straightly
|
2023-08-10 19:08:46 +08:00
|
|
|
channel = WebSocketChannel.connect(wsUrl);
|
2023-09-16 11:33:57 +08:00
|
|
|
heartBeatInspect();
|
2023-08-10 19:08:46 +08:00
|
|
|
|
|
|
|
channel.stream.listen(onData, onError: onError, onDone: onDone);
|
|
|
|
}
|
|
|
|
|
2023-08-27 19:14:37 +08:00
|
|
|
void disconnect() {
|
|
|
|
channel.sink.close();
|
|
|
|
wsUrl = Uri();
|
|
|
|
id = '';
|
|
|
|
socketStatus = SocketStatus.closed;
|
2023-09-17 10:27:00 +08:00
|
|
|
notifyListeners();
|
2023-08-27 19:14:37 +08:00
|
|
|
heartBeatTimer?.cancel();
|
|
|
|
serverTimer?.cancel();
|
|
|
|
reconnectTimer?.cancel();
|
|
|
|
heartBeatTimer = null;
|
|
|
|
serverTimer = null;
|
|
|
|
reconnectTimer = null;
|
|
|
|
reconnectTimes = 0;
|
|
|
|
}
|
|
|
|
|
2023-10-01 18:40:14 +08:00
|
|
|
void onData(jsonData) async {
|
2023-09-17 10:27:00 +08:00
|
|
|
// If socket can receive msg, that means connection is estabilished
|
|
|
|
socketStatus = SocketStatus.connected;
|
|
|
|
notifyListeners();
|
|
|
|
print('websocket connected <$channel>');
|
|
|
|
if (reconnectTimer != null) {
|
|
|
|
reconnectTimer!.cancel();
|
|
|
|
reconnectTimer = null;
|
|
|
|
reconnectTimes = 0;
|
|
|
|
}
|
|
|
|
|
2023-08-10 19:08:46 +08:00
|
|
|
heartBeatInspect();
|
2023-09-17 10:27:00 +08:00
|
|
|
|
2023-10-01 18:40:14 +08:00
|
|
|
// Map<String, dynamic> data = json.decode(jsonData);
|
|
|
|
Map<String, dynamic> data =
|
|
|
|
await compute((message) => json.decode(message), jsonData);
|
2023-08-10 19:08:46 +08:00
|
|
|
switch (data['event']) {
|
2023-08-23 16:30:41 +08:00
|
|
|
case 'friend-chat-msg':
|
2023-10-01 18:40:14 +08:00
|
|
|
await receiveFriendMsg(data, true);
|
2023-08-11 23:02:31 +08:00
|
|
|
case 'apply-friend':
|
|
|
|
receiveApplyFriend(data);
|
|
|
|
case 'friend-added':
|
|
|
|
receiveFriendAdded(data);
|
|
|
|
case 'friend-deleted':
|
|
|
|
receiveFriendDeleted(data);
|
2023-09-16 11:33:57 +08:00
|
|
|
case 'chat-image':
|
2023-08-23 16:30:41 +08:00
|
|
|
receiveChatImages(data);
|
|
|
|
case 'group-chat-creation':
|
|
|
|
receiveGroupChatCreation(data);
|
|
|
|
case 'group-chat-msg':
|
2023-09-17 10:27:00 +08:00
|
|
|
receiveGroupChatMsg(data, true);
|
2023-10-01 18:40:14 +08:00
|
|
|
case 'pull-chat-image':
|
|
|
|
receivePullChatImage(data);
|
|
|
|
case 'chat-image-send-ok':
|
|
|
|
receiveCheckChatImage(data);
|
2023-08-10 19:08:46 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-17 10:27:00 +08:00
|
|
|
// This will be trigger while server or client close the connection
|
|
|
|
// for example server is restarting
|
2023-08-10 19:08:46 +08:00
|
|
|
void onDone() {
|
|
|
|
print('websocket disconnected <$channel>');
|
2023-09-20 20:21:27 +08:00
|
|
|
print('22222222222222222222');
|
2023-08-27 19:14:37 +08:00
|
|
|
if (socketStatus == SocketStatus.closed) {
|
2023-09-17 10:27:00 +08:00
|
|
|
// Client close the connection
|
2023-08-27 19:14:37 +08:00
|
|
|
return;
|
|
|
|
}
|
2023-09-17 10:27:00 +08:00
|
|
|
if (socketStatus == SocketStatus.connected) {
|
|
|
|
// Server close the connection
|
|
|
|
socketStatus = SocketStatus.reconnecting;
|
|
|
|
notifyListeners();
|
|
|
|
print(111111111111111);
|
|
|
|
reconnectTimes++;
|
|
|
|
reconnect();
|
|
|
|
}
|
|
|
|
if (reconnectTimes >= reconnectCount) {
|
|
|
|
socketStatus = SocketStatus.error;
|
2023-09-20 20:21:27 +08:00
|
|
|
reconnectTimes = 0;
|
|
|
|
} else {
|
|
|
|
socketStatus = SocketStatus.reconnecting;
|
|
|
|
print('3333333333333333333');
|
|
|
|
notifyListeners();
|
2023-09-17 10:27:00 +08:00
|
|
|
}
|
2023-08-10 19:08:46 +08:00
|
|
|
}
|
|
|
|
|
2023-09-17 10:27:00 +08:00
|
|
|
// This will be trigger while server exactly shutdown
|
|
|
|
void onError(Object error, StackTrace st) {
|
|
|
|
print('Websocket connect occurs error: <$error>');
|
|
|
|
// print(st);
|
|
|
|
if (reconnectTimes >= reconnectCount) {
|
|
|
|
socketStatus = SocketStatus.error;
|
2023-09-20 20:21:27 +08:00
|
|
|
notifyListeners();
|
|
|
|
channel.sink.close();
|
2023-09-17 10:27:00 +08:00
|
|
|
if (heartBeatTimer != null) {
|
|
|
|
heartBeatTimer!.cancel();
|
|
|
|
heartBeatTimer = null;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
print('${reconnectTimes}th reconnection');
|
|
|
|
reconnect();
|
|
|
|
}
|
|
|
|
}
|
2023-08-10 19:08:46 +08:00
|
|
|
|
|
|
|
void heartBeatInspect() {
|
2023-09-17 10:27:00 +08:00
|
|
|
print('start heartbeat inspect......');
|
|
|
|
|
2023-08-10 19:08:46 +08:00
|
|
|
if (heartBeatTimer != null) {
|
|
|
|
heartBeatTimer!.cancel();
|
|
|
|
heartBeatTimer = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (serverTimer != null) {
|
|
|
|
serverTimer!.cancel();
|
|
|
|
serverTimer = null;
|
|
|
|
}
|
2023-09-17 10:27:00 +08:00
|
|
|
|
|
|
|
heartBeatTimer = Timer(heartBeatTimeout, () {
|
2023-08-10 19:08:46 +08:00
|
|
|
channel.sink.add(json.encode({'event': 'ping'}));
|
2023-09-17 10:27:00 +08:00
|
|
|
serverTimer = Timer(heartBeatTimeout, () {
|
|
|
|
// This will trigger the onDone callback
|
2023-08-10 19:08:46 +08:00
|
|
|
channel.sink.close(status.internalServerError);
|
2023-09-17 10:27:00 +08:00
|
|
|
socketStatus = SocketStatus.reconnecting;
|
|
|
|
notifyListeners();
|
2023-08-10 19:08:46 +08:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void reconnect() {
|
|
|
|
if (heartBeatTimer != null) {
|
|
|
|
heartBeatTimer!.cancel();
|
|
|
|
heartBeatTimer = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (serverTimer != null) {
|
|
|
|
serverTimer!.cancel();
|
|
|
|
serverTimer = null;
|
|
|
|
}
|
|
|
|
|
2023-09-17 10:27:00 +08:00
|
|
|
if (reconnectTimer != null) {
|
|
|
|
reconnectTimer!.cancel();
|
|
|
|
reconnectTimer = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
reconnectTimer = Timer(reconnectTimeout, () {
|
2023-08-10 19:08:46 +08:00
|
|
|
if (reconnectTimes < reconnectCount) {
|
2023-09-17 10:27:00 +08:00
|
|
|
print('websocket reconnecting......');
|
2023-08-10 19:08:46 +08:00
|
|
|
reconnectTimes++;
|
2023-09-17 10:27:00 +08:00
|
|
|
connect(id, true);
|
2023-08-10 19:08:46 +08:00
|
|
|
} else {
|
|
|
|
print('reconnection times exceed the max times......');
|
|
|
|
// If it is still disconnection after reconnect 30 times, set the socket
|
|
|
|
// status to error, means the network is bad, and stop reconnecting.
|
|
|
|
socketStatus = SocketStatus.error;
|
2023-09-17 10:27:00 +08:00
|
|
|
notifyListeners();
|
2023-08-10 19:08:46 +08:00
|
|
|
channel.sink.close();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2023-09-09 16:48:47 +08:00
|
|
|
|
2023-10-01 18:40:14 +08:00
|
|
|
void addSendImageTimer(String filename, int totalChunkNum) {
|
|
|
|
if (sendImageTimer.containsKey(filename)) {
|
|
|
|
sendImageTimer[filename]!.cancel();
|
|
|
|
sendImageTimer.remove(filename);
|
2023-09-09 16:48:47 +08:00
|
|
|
}
|
2023-08-11 23:02:31 +08:00
|
|
|
|
2023-10-01 18:40:14 +08:00
|
|
|
sendImageTimer[filename] = Timer(
|
|
|
|
heartBeatTimeout,
|
|
|
|
() async {
|
|
|
|
late Box<AttachmentProgress> attachmentLoadingBox;
|
|
|
|
try {
|
|
|
|
attachmentLoadingBox =
|
|
|
|
Hive.box<AttachmentProgress>('attachment_receive');
|
|
|
|
} catch (_) {
|
|
|
|
attachmentLoadingBox =
|
|
|
|
await Hive.openBox<AttachmentProgress>('attachment_receive');
|
|
|
|
}
|
|
|
|
|
|
|
|
AttachmentProgress? at = attachmentLoadingBox.get(filename);
|
|
|
|
|
|
|
|
if (at == null) {
|
|
|
|
attachmentLoadingBox.put(
|
|
|
|
filename,
|
|
|
|
AttachmentProgress(
|
|
|
|
0,
|
|
|
|
totalChunkNum,
|
|
|
|
0,
|
|
|
|
false,
|
|
|
|
true,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
at.isPause = true;
|
|
|
|
attachmentLoadingBox.put(filename, at);
|
|
|
|
}
|
|
|
|
},
|
2023-08-23 16:30:41 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-10-01 18:40:14 +08:00
|
|
|
void removeSendImageTimer(String filename) {
|
|
|
|
if (sendImageTimer.containsKey(filename)) {
|
|
|
|
sendImageTimer[filename]!.cancel();
|
2023-09-09 16:48:47 +08:00
|
|
|
}
|
2023-10-01 18:40:14 +08:00
|
|
|
sendImageTimer.remove(filename);
|
2023-09-09 16:48:47 +08:00
|
|
|
}
|
|
|
|
}
|