diff --git a/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/.gitignore b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/.gitignore new file mode 100644 index 00000000..24476c5d --- /dev/null +++ b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/chat_controller.dart b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/chat_controller.dart new file mode 100644 index 00000000..071ddf7d --- /dev/null +++ b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/chat_controller.dart @@ -0,0 +1,201 @@ +import 'dart:async'; + +import 'package:get/get.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/client_core_sdk.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/conf/config_entity.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/auto_relogin_daemon.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/keep_alive_daemon.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/local_data_sender.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/qos_4_receive_daemon.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/qos_4_send_daemon.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/event/chat_base_event.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/event/chat_message_event.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/event/message_qoS_event.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/Ext.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/log.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/c/p_login_info.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/protocol.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/s/p_kickout_info.dart'; +import 'package:oktoast/oktoast.dart'; + +import 'test_home_page.dart'; + +class ChatBaseEventImpl extends ChatBaseEvent { + final _tag = "登录"; + + late ChatController _chat; + + void setChatController(ChatController chatController) { + _chat = chatController; + } + + void _refreshState() { + _chat.refreshState(); + } + + + @override + void onLinkClose(int errorCode) { + _refreshState(); + final info = + "[error] $_tag ${nowHmsStrWithMark()} 与服务端的通信断开的回调事件通知:$errorCode"; + Log.error(info, _tag); + _chat.msgList.add(info); + } + + @override + void onLoginResponse(int errorCode) { + _refreshState(); + final info = "$_tag ${nowHmsStrWithMark()} 结果:$errorCode"; + Log.info(info, _tag); + _chat.msgList.add(info); + ClientCoreSDK.getInstance().setLoginHasInit(errorCode == 0); + if (errorCode == 0) { + Get.to(const TestHomePage()); + } else { + showToast("登录失败:$errorCode"); + } + } + + @override + void onKickOut(PKickoutInfo kickOutInfo) { + _refreshState(); + final info = "[error] $_tag ${nowHmsStrWithMark()} 被踢:$kickOutInfo"; + Log.error(info, _tag); + _chat.msgList.add(info); + } + +} + +class ChatMessageEventImpl extends ChatMessageEvent { + final _tag = "消息"; + + late ChatController _chat; + + void setChatController(ChatController chatController) { + _chat = chatController; + } + + void _refreshMsg() {} + + @override + void onErrorResponse(int errorCode, String errorMsg) { + final info = + "$_tag ${nowHmsStrWithMark()} 【DEBUG_UI】收到服务端错误消息,errorCode=$errorCode errorMsg:$errorMsg"; + Log.warn(info, _tag); + _chat.msgList.add(info); + } + + @override + void onReceiveMessage(String fingerPrintOfProtocol, String userid, + String dataContent, int typeu) { + final info = + "$_tag ${nowHmsStrWithMark()} 【DEBUG_UI】[typeu=$typeu]收到来自用户 $userid 的消息: $dataContent"; + Log.info(info, _tag); + _chat.msgList.add(info); + _chat.newestReceiveMsg.value = info; + } +} + +class MessageQoSEventImpl extends MessageQoSEvent { + final _tag = "消息QoS"; + + late ChatController _chat; + + void setChatController(ChatController chatController) { + _chat = chatController; + } + + void _refreshState() {} + + @override + void messagesBeReceived(String theFingerPrint) { + final info = + "$_tag ${nowHmsStrWithMark()} 【DEBUG_UI】收到对方已收到消息事件的通知,fp=$theFingerPrint"; + Log.info(info, _tag); + _chat.msgList.add(info); + } + + @override + void messagesLost(List lostMessages) { + final info = + "$_tag ${nowHmsStrWithMark()} 【DEBUG_UI】收到系统的未实时送达事件通知,当前共有 ${lostMessages.length} 个包QoS保证机制结束,判定为【无法实时送达】!"; + Log.warn(info, _tag); + _chat.msgList.add(info); + } +} + +class ChatController extends GetxController { + final sdk = ClientCoreSDK.getInstance(); + + final linkState = false.obs; + + /// 重连 + final reLinkState = false.obs; + + /// 心跳 + final heartbeatState = false.obs; + + /// 送达(发) + final qosSendState = false.obs; + + /// 送达(收) + final qosReceiveState = false.obs; + + final msgList = RxList(); + + final newestReceiveMsg = "".obs; + + Timer? _periodicSecTimer; + + @override + void onInit() { + super.onInit(); + _initSdk(); + _periodicSecTimer = Timer.periodic(const Duration(seconds: 3), (timer) { + refreshState(); + }); + } + + @override + void dispose() { + super.dispose(); + _periodicSecTimer?.cancel(); + sdk.release(); + } + + void _initSdk() { + msgList.clear(); + sdk + ..init() + ..setChatBaseEvent(ChatBaseEventImpl()..setChatController(this)) + ..setChatMessageEvent(ChatMessageEventImpl()..setChatController(this)) + ..setMessageQoSEvent(MessageQoSEventImpl()..setChatController(this)); + } + + void refreshState() { + linkState.value = sdk.isConnectedToServer(); + reLinkState.value = AutoReLoginDaemon.getInstance().isAutoReLoginRunning(); + heartbeatState.value = KeepAliveDaemon.getInstance().isKeepAliveRunning(); + qosSendState.value = QoS4SendDaemon.getInstance().isRunning(); + qosReceiveState.value = QoS4ReceiveDaemon.getInstance().isRunning(); + } + + void doLogin(String ip, int port, String name, String pwd) { + _initSdk(); + + ConfigEntity.serverIP = ip; + ConfigEntity.serverPort = port; + + LocalDataSender.getInstance().sendLogin(PLoginInfo(name, loginToken: pwd)); + } + + void logout() { + var i = LocalDataSender.getInstance().sendLogout(); + if (i == 0) { + showToast("退出登录成功"); + sdk.release(); + } + refreshState(); + } +} diff --git a/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/loading_button.dart b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/loading_button.dart new file mode 100644 index 00000000..dc688bbd --- /dev/null +++ b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/loading_button.dart @@ -0,0 +1,92 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +/// 有加载动画的按钮 +class LoadingButton extends StatefulWidget { + final bool loading; + final String title; + final String loadingText; + final TextStyle? titleStyle; + final Widget? indicator; + final Color? loadingColor; + final Color? color; + final double radius; + final double width; + final double height; + final VoidCallback? onTap; + + const LoadingButton({ + super.key, + this.loading = false, + required this.title, + this.loadingText = '加载中', + this.titleStyle, + this.loadingColor, + this.color, + this.radius = 0, + this.height = 30, + this.width = 100, + this.indicator, + this.onTap, + }); + + @override + State createState() => _LoadingButtonState(); +} + +class _LoadingButtonState extends State { + @override + void initState() { + super.initState(); + } + + /// 标题 + Text _titleWidget() => Text( + widget.loading ? widget.loadingText : widget.title, + style: widget.titleStyle ?? + const TextStyle( + fontSize: 16, + color: Colors.white, + ), + overflow: TextOverflow.ellipsis, + ); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + if (!widget.loading && widget.onTap != null) { + widget.onTap!(); + } + }, + child: Container( + decoration: BoxDecoration( + color: widget.color ?? Colors.blue, + borderRadius: BorderRadius.all(Radius.circular(widget.radius)), + ), + child: TextButton( + onPressed: () { + if (!widget.loading && widget.onTap != null) { + widget.onTap!(); + } + }, + child: widget.loading + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + widget.indicator ?? + CupertinoActivityIndicator( + animating: widget.loading, + color: widget.loadingColor ?? Colors.white, + ), + const SizedBox(width: 8), + _titleWidget(), + ], + ) + : _titleWidget(), + ), + ), + ); + } +} diff --git a/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/log.dart b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/log.dart new file mode 100644 index 00000000..c96aa02b --- /dev/null +++ b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/log.dart @@ -0,0 +1,43 @@ +import 'package:flutter/foundation.dart'; +import 'package:logger/logger.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/ext.dart'; + +class Log { + static Function? cacheLogInfo; + + static final Logger _logger = Logger( + printer: PrefixPrinter(PrettyPrinter()), + ); + + static void i(dynamic message, String? tag) { + if (kDebugMode) { + var info = "$tag ${nowHmsStrWithMark()}--> $message"; + print(info); + cacheLogInfo?.call(info); + } + } + + static void d(dynamic message, String? tag) { + if (kDebugMode) { + _logger.d("$tag --> $message", time: DateTime.now()); + } + } + + static void w(dynamic message, String? tag) { + if (kDebugMode) { + _logger.w("$tag --> $message", time: DateTime.now()); + } + } + + static void e(dynamic message, String? tag) { + if (kDebugMode) { + _logger.e("$tag --> $message", time: DateTime.now()); + } + } + + static void f(dynamic message, String? tag) { + if (kDebugMode) { + _logger.f("$tag --> $message", time: DateTime.now()); + } + } +} diff --git a/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/main.dart b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/main.dart new file mode 100644 index 00000000..5bad9ea3 --- /dev/null +++ b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/main.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:mobile_im_sdk_flutter_tcp_example/test_login_page.dart'; +import 'package:oktoast/oktoast.dart'; + +void main() async { + // 初始化持久化 + await GetStorage.init(); + + runApp(const TestApp()); +} + +class TestApp extends StatelessWidget { + const TestApp({super.key}); + + @override + Widget build(BuildContext context) { + return OKToast( + radius: 15, + textPadding: + const EdgeInsets.only(left: 15, right: 15, top: 6, bottom: 6), + position: ToastPosition.bottom, + backgroundColor: Colors.black.withOpacity(0.7), + child: GetMaterialApp( + title: 'Chat Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + debugShowCheckedModeBanner: true, + home: const TestLoginPage(), + ), + ); + } +} diff --git a/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/test_home_page.dart b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/test_home_page.dart new file mode 100644 index 00000000..016eb92f --- /dev/null +++ b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/test_home_page.dart @@ -0,0 +1,335 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/local_data_sender.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/Ext.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/log.dart'; +import 'package:oktoast/oktoast.dart'; + +import 'chat_controller.dart'; +import 'loading_button.dart'; + +class TestHomeController extends GetxController { + final accountController = TextEditingController(); + final msgController = TextEditingController(); + bool loading = false; + + final ChatController chatController = Get.find(); + + final storage = GetStorage(); + + @override + void onInit() { + super.onInit(); + String name = storage.read("rec_name") ?? ""; + String info = storage.read("msg") ?? "{消息[doge]} "; + accountController.text = name; + msgController.text = info; + } + + void sendMsg() { + var info = msgController.text ?? ""; + var name = accountController.text ?? ""; + if (info.isEmpty || name.isEmpty) { + showToast("消息 和 用户名 不可为空"); + return; + } + + storage.write("rec_name", name); + storage.write("msg", info); + + var finalMsg = nowHmsStrWithMark() + info; + + chatController.msgList.add(finalMsg); + + LocalDataSender.getInstance().sendCommonData(finalMsg, name); + } +} + +class TestHomePage extends StatefulWidget { + const TestHomePage({super.key}); + + @override + State createState() => _TestHomePageState(); +} + +class _TestHomePageState extends State + with WidgetsBindingObserver, RouteAware { + bool _inCurrentPage = true; + + /// 由子页面 返回时刷新 + @override + void didPopNext() { + _inCurrentPage = true; + super.didPopNext(); + } + + /// 进入子页面 + @override + void didPushNext() { + _inCurrentPage = false; + super.didPushNext(); + } + + @override + void initState() { + super.initState(); + Log.cacheLogInfo = (str) { + var finalStr = str.toString(); + var flag = finalStr.contains("[error]") || finalStr.contains("[warning]"); + var notMsg = !finalStr.contains("[msg]"); + if (flag && notMsg) { + chatController.msgList.add(finalStr); + } + }; + + WidgetsBinding.instance.addObserver(this); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + // App 的生命周期回调 + switch (state) { + case AppLifecycleState.inactive: + print('app -> inactive'); + break; + case AppLifecycleState.resumed: + print('app -> resumed 恢复'); + // 连接 + break; + case AppLifecycleState.paused: + print('app -> paused 挂起'); + break; + case AppLifecycleState.detached: + print('app -> detached 已经退出(进程已杀死)'); + // 关闭连接 + break; + } + } + + TestHomeController controller = Get.find(); + ChatController chatController = Get.find(); + + ScrollController scrollController = ScrollController(); + + Color getBgColor() => Color.fromARGB(Random().nextInt(125), + Random().nextInt(255), Random().nextInt(255), Random().nextInt(255)); + + late Color bgColor = Colors.lime; + + final autoScroll = false.obs; + + @override + Widget build(BuildContext context) { + chatController.msgList.listen((p0) { + if (!autoScroll.value) return; + + /// 延迟 300 毫秒,再进行滑动 + Future.delayed(const Duration(milliseconds: 300), () { + scrollController.jumpTo(scrollController.position.maxScrollExtent); + }); + }); + + return Scaffold( + appBar: AppBar( + title: const Text('主页'), + ), + body: SizedBox.expand( + child: Padding( + padding: const EdgeInsets.only(top: 20, left: 15, right: 15), + child: Column( + children: [ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + Obx(() => Text( + "网络 ${chatController.linkState.value ? "连接" : "断开"}", + style: TextStyle( + fontSize: 8, + color: chatController.linkState.value + ? Colors.green + : Colors.red), + )), + const SizedBox(width: 10), + Obx(() => Text( + "重连 ${chatController.reLinkState.value ? "连接" : "断开"}", + style: TextStyle( + fontSize: 8, + color: chatController.reLinkState.value + ? Colors.green + : Colors.red), + )), + const SizedBox(width: 10), + Obx(() => Text( + "心跳 ${chatController.heartbeatState.value ? "连接" : "断开"}", + style: TextStyle( + fontSize: 8, + color: chatController.heartbeatState.value + ? Colors.green + : Colors.red), + )), + const SizedBox(width: 10), + Obx(() => Text( + "QoS 发送 ${chatController.qosSendState.value ? "连接" : "断开"}", + style: TextStyle( + fontSize: 8, + color: chatController.qosSendState.value + ? Colors.green + : Colors.red), + )), + const SizedBox(width: 10), + Obx(() => Text( + "QoS 接收 ${chatController.qosReceiveState.value ? "连接" : "断开"}", + style: TextStyle( + fontSize: 8, + color: chatController.qosReceiveState.value + ? Colors.green + : Colors.red), + )), + ], + )), + const SizedBox(height: 15), + Obx(() { + return Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("自动滚动到底部"), + const SizedBox(width: 5), + CupertinoSwitch( + value: autoScroll.value, + onChanged: (v) { + autoScroll.value = v; + }), + ], + ); + }), + const SizedBox(height: 15), + CupertinoTextField( + placeholder: '请输入接收者...', + controller: controller.accountController, + clearButtonMode: OverlayVisibilityMode.editing, + ), + const SizedBox(height: 15), + CupertinoTextField( + placeholder: '请输入消息...', + controller: controller.msgController, + keyboardType: TextInputType.text, + clearButtonMode: OverlayVisibilityMode.editing, + ), + const SizedBox(height: 15), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: SizedBox( + width: 100, + height: 30, + child: LoadingButton( + title: '发送', + loadingText: '发送中', + titleStyle: const TextStyle( + fontSize: 8, + color: Colors.white, + ), + radius: 17.5, + loading: controller.loading, + onTap: () { + controller.sendMsg(); + }, + ), + ), + ), + const SizedBox(width: 50), + Expanded( + child: SizedBox( + width: 100, + height: 30, + child: LoadingButton( + title: '批量发送 ($_multiCount)', + loadingText: '发送中', + titleStyle: const TextStyle( + fontSize: 8, + color: Colors.white, + ), + radius: 17.5, + loading: controller.loading, + onTap: () { + doMultiSend(); + }, + ), + ), + ), + const SizedBox(width: 50), + Expanded( + child: SizedBox( + height: 30, + child: LoadingButton( + title: '清屏', + loadingText: '清屏中', + titleStyle: const TextStyle( + fontSize: 8, + color: Colors.white, + ), + radius: 17.5, + loading: controller.loading, + onTap: () { + chatController.msgList.clear(); + }, + ), + ), + ), + ], + ), + const SizedBox(height: 15), + Obx(() => Text( + chatController.newestReceiveMsg.value, + style: const TextStyle(color: Colors.blue), + )), + const SizedBox(height: 15), + Obx(() => Text("${chatController.msgList.length}")), + const SizedBox(height: 5), + Expanded( + child: Obx(() => ListView.builder( + controller: scrollController, + itemCount: chatController.msgList.length, + reverse: false, + itemBuilder: (context, index) { + return Container( + color: bgColor, + padding: const EdgeInsets.all(8.0), + margin: const EdgeInsets.symmetric( + horizontal: 3.0, vertical: 5), + child: Text( + "$index ${chatController.msgList[index]}"), + ); + }, + ))) + ], + ), + ), + ), + ); + } + + doMultiSend() async { + for (int i = 0; i < _multiCount; i++) { + controller.sendMsg(); + sleep(const Duration(milliseconds: 100)); + } + } + + @override + void dispose() { + super.dispose(); + chatController.logout(); + } +} + +const int _multiCount = 10; diff --git a/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/test_login_page.dart b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/test_login_page.dart new file mode 100644 index 00000000..2e992309 --- /dev/null +++ b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/lib/test_login_page.dart @@ -0,0 +1,109 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:oktoast/oktoast.dart'; +import 'chat_controller.dart'; +import 'loading_button.dart'; +import 'test_home_page.dart'; + +class TestLoginController extends GetxController { + final ipController = TextEditingController(); + final accountController = TextEditingController(); + bool loading = false; + final storage = GetStorage(); + + @override + void onInit() { + super.onInit(); + String ip = storage.read("ip") ?? "rbcore.52im.net"; + String name = storage.read("name") ?? "f"; + ipController.text = ip; + accountController.text = name; + } + + final ChatController chatController = Get.find(); + + void doLogin() { + final ip = ipController.text ?? ''; + final name = accountController.text ?? ''; + if (ip.isEmpty || name.isEmpty) { + showToast("Ip 和 用户名 不可为空"); + return; + } + storage.write("ip", ip); + storage.write("name", name); + + chatController.doLogin(ip, 8901, name, name); + } +} + +class TestLoginPage extends StatelessWidget { + const TestLoginPage({super.key}); + + @override + Widget build(BuildContext context) { + var chatController = Get.put(ChatController()); + var homeController = Get.put(TestHomeController()); + TestLoginController controller = Get.put(TestLoginController()); + + return Scaffold( + appBar: AppBar( + title: const Text('登陆'), + ), + body: Center( + child: Padding( + padding: const EdgeInsets.only(top: 100, left: 15, right: 15), + child: Column( + children: [ + CupertinoTextField( + placeholder: '请输入ip...', + controller: controller.ipController, + clearButtonMode: OverlayVisibilityMode.editing, + ), + const SizedBox(height: 15), + CupertinoTextField( + placeholder: '请输入用户...', + controller: controller.accountController, + keyboardType: TextInputType.visiblePassword, + clearButtonMode: OverlayVisibilityMode.editing, + ), + const SizedBox(height: 15), + SizedBox( + width: 100, + height: 50, + child: LoadingButton( + title: '登陆', + loadingText: '登陆中', + radius: 17.5, + loading: controller.loading, + onTap: () { + // 登陆 + controller.doLogin(); + }, + ), + ), + const SizedBox(height: 15), + const SizedBox(height: 5), + Expanded( + child: Obx(() => ListView.builder( + itemCount: chatController.msgList.length, + reverse: false, + itemBuilder: (context, index) { + return Container( + color: Colors.lightGreen, + padding: const EdgeInsets.all(8.0), + margin: const EdgeInsets.symmetric( + horizontal: 3.0, vertical: 5), + child: Text( + "$index ${chatController.msgList[index]}"), + ); + }, + ))) + ], + ), + ), + ), + ); + } +} diff --git a/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/pubspec.lock b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/pubspec.lock new file mode 100644 index 00000000..8b5b84ec --- /dev/null +++ b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/pubspec.lock @@ -0,0 +1,324 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.17.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + duffer: + dependency: transitive + description: + name: duffer + sha256: "482fa7e454d66ab1cc6616fdb0c076bb971f7eb54b0ce4ab6ad870ada928feb0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.5" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + get: + dependency: "direct main" + description: + name: get + sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.6.5" + get_storage: + dependency: "direct main" + description: + name: get_storage + sha256: "39db1fffe779d0c22b3a744376e86febe4ade43bf65e06eab5af707dc84185a2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.7" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + logger: + dependency: "direct main" + description: + name: logger + sha256: "66cb048220ca51cf9011da69fa581e4ee2bed4be6e82870d9e9baae75739da49" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.15" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.1" + mobile_im_sdk_flutter_tcp: + dependency: "direct main" + description: + path: "../../../sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open" + relative: true + source: path + version: "1.0.0+1" + oktoast: + dependency: "direct main" + description: + name: oktoast + sha256: fd5dd5c7dd02c41c56bdbdbc163351bd6cd59cba61c416daf255e35d1a86dce1 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.3.1" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.3" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.15" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.27" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.4" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.11" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.7" + platform: + dependency: transitive + description: + name: platform + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.5" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.11.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + win32: + dependency: transitive + description: + name: win32 + sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.6" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: e0b1147eec179d3911f1f19b59206448f78195ca1d20514134e10641b7d7fbff + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" +sdks: + dart: ">=3.0.5 <4.0.0" + flutter: ">=3.3.0" diff --git a/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/pubspec.yaml b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/pubspec.yaml new file mode 100644 index 00000000..44fc5357 --- /dev/null +++ b/demo_src/TCP_Client/MobileIMSDK4fDemo_tcp/pubspec.yaml @@ -0,0 +1,86 @@ +name: mobile_im_sdk_flutter_tcp_example +description: mobile_im_sdk_flutter_tcp_example. +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=3.0.5 <4.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + mobile_im_sdk_flutter_tcp: + path: ../../../sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open + get: ^4.6.5 + get_storage: ^2.1.1 + oktoast: ^3.3.1 + logger: ^2.0.1 + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/.gitignore b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/.gitignore new file mode 100644 index 00000000..24476c5d --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/analysis_options.yaml b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/analysis_options.yaml new file mode 100644 index 00000000..61b6c4de --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/client_core_sdk.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/client_core_sdk.dart new file mode 100644 index 00000000..d8043ac3 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/client_core_sdk.dart @@ -0,0 +1,129 @@ +import 'package:mobile_im_sdk_flutter_tcp/sdk/conf/config_entity.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/auto_relogin_daemon.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/keep_alive_daemon.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/local_data_receiver.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/local_socket_provider.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/qos_4_receive_daemon.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/qos_4_send_daemon.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/event/chat_base_event.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/event/chat_message_event.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/event/message_qoS_event.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/c/p_login_info.dart'; + +class ClientCoreSDK { + static const String TAG = "ClientCoreSDK"; + static bool autoReLogin = true; + + static int getCurrentTimeStamp() { + return DateTime.now().millisecondsSinceEpoch; + } + + // + + factory ClientCoreSDK.getInstance() => _instance; + + static late final ClientCoreSDK _instance = ClientCoreSDK._internal(); + + ClientCoreSDK._internal(); + + // + + bool _init = false; + + bool _connectedToServer = true; + bool _loginHasInit = false; + + ChatBaseEvent? _chatBaseEvent; + ChatMessageEvent? _chatMessageEvent; + MessageQoSEvent? _messageQoSEvent; + + PLoginInfo? _currentLoginInfo; + + void init({SenseMode senseMode = SenseMode.MODE_10S}) { + if (!_init) { + ConfigEntity.setSenseMode(senseMode); + AutoReLoginDaemon.getInstance(); + KeepAliveDaemon.getInstance(); + LocalDataReceiver.getInstance(); + QoS4SendDaemon.getInstance(); + QoS4ReceiveDaemon.getInstance(); + + _init = true; + } + } + + void release() { + setConnectedToServer(false); + + LocalSocketProvider.getInstance().closeLocalSocket(); + AutoReLoginDaemon.getInstance().stop(); + KeepAliveDaemon.getInstance().stop(); + + QoS4SendDaemon.getInstance().stop(); + QoS4ReceiveDaemon.getInstance().stop(); + + QoS4SendDaemon.getInstance().clear(); + QoS4ReceiveDaemon.getInstance().clear(); + + _init = false; + setLoginHasInit(false); + setCurrentLoginInfo(null); + } + + void setCurrentLoginInfo(PLoginInfo? currentLoginInfo) { + _currentLoginInfo = currentLoginInfo; + } + + PLoginInfo? getCurrentLoginInfo() { + return _currentLoginInfo; + } + + void saveFirstLoginTime(int firstLoginTime) { + _currentLoginInfo?.setFirstLoginTime(firstLoginTime); + } + + bool isLoginHasInit() { + return _loginHasInit; + } + + ClientCoreSDK setLoginHasInit(bool loginHasInit) { + _loginHasInit = loginHasInit; + return this; + } + + bool isConnectedToServer() { + return _connectedToServer; + } + + void setConnectedToServer(bool connectedToServer) { + _connectedToServer = connectedToServer; + } + + bool isInitialed() { + return _init; + } + + void setChatBaseEvent(ChatBaseEvent chatBaseEvent) { + _chatBaseEvent = chatBaseEvent; + } + + ChatBaseEvent? getChatBaseEvent() { + return _chatBaseEvent; + } + + void setChatMessageEvent(ChatMessageEvent chatMessageEvent) { + _chatMessageEvent = chatMessageEvent; + } + + ChatMessageEvent? getChatMessageEvent() { + return _chatMessageEvent; + } + + void setMessageQoSEvent(MessageQoSEvent messageQoSEvent) { + _messageQoSEvent = messageQoSEvent; + } + + MessageQoSEvent? getMessageQoSEvent() { + return _messageQoSEvent; + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/conf/config_entity.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/conf/config_entity.dart new file mode 100644 index 00000000..7312a843 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/conf/config_entity.dart @@ -0,0 +1,102 @@ +// ignore_for_file: constant_identifier_names + +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/keep_alive_daemon.dart'; + +class ConfigEntity { + static String serverIP = "192.168.31.108"; // + static int serverPort = 8901; + + static void setSenseMode(SenseMode mode) { + int keepAliveInterval = 0; + int networkConnectionTimeout = 0; + switch (mode) { + case SenseMode.MODE_3S: + { + keepAliveInterval = 3000; // 3s + networkConnectionTimeout = keepAliveInterval * 1 + 2000; // 5s + break; + } + case SenseMode.MODE_5S: + { + keepAliveInterval = 5000; // 3s + networkConnectionTimeout = keepAliveInterval * 1 + 3000; // 8s + break; + } + case SenseMode.MODE_10S: + keepAliveInterval = 10000; // 10s + networkConnectionTimeout = keepAliveInterval * 1 + 5000; // 15s + break; + case SenseMode.MODE_15S: + keepAliveInterval = 15000; // 15s + networkConnectionTimeout = keepAliveInterval * 1 + 5000; // 20s + break; + case SenseMode.MODE_30S: + keepAliveInterval = 30000; // 30s + networkConnectionTimeout = keepAliveInterval * 1 + 5000; // 35s + break; + case SenseMode.MODE_60S: + keepAliveInterval = 60000; // 60s + networkConnectionTimeout = keepAliveInterval * 1 + 5000; // 65s + break; + case SenseMode.MODE_120S: + keepAliveInterval = 120000; // 120s + networkConnectionTimeout = keepAliveInterval * 1 + 5000; // 125s + break; + } + + if (keepAliveInterval > 0) { + KeepAliveDaemon.KEEP_ALIVE_INTERVAL = keepAliveInterval; + } + if (networkConnectionTimeout > 0) { + KeepAliveDaemon.NETWORK_CONNECTION_TIME_OUT = networkConnectionTimeout; + } + } +} + +/// 即时通讯核心框架预设的敏感度模式. +/// +///

+/// 对于客户端而言,此模式决定了用户与服务端网络会话的健康模式,原则上越敏感客户端的体验越好。 +/// +///

+/// 重要说明:客户端本模式的设定必须要与服务端的模式设制保持一致,否则可能因参数的不一致而导致 +/// IM算法的不匹配,进而出现不可预知的问题。 +/// +enum SenseMode { + /// 此模式下:
+ /// * KeepAlive心跳问隔为3秒;
+ /// * 5秒后未收到服务端心跳反馈即认为连接已断开(相当于连续2个心跳间隔(即算法最大6秒延迟后)后仍未收到服务端反馈)。 + MODE_3S, + + /// 此模式下:
+ /// * KeepAlive心跳问隔为5秒;
+ /// * 8秒后未收到服务端心跳反馈即认为连接已断开(相当于连续1个心跳间隔+3秒链路延迟容忍时间后仍未收到服务端反馈)。 + MODE_5S, + + /// 此模式下:
+ /// * KeepAlive心跳问隔为10秒;
+ /// * 15秒后未收到服务端心跳反馈即认为连接已断开(相当于连续2个心跳间隔(即算法最大20秒延迟后)后仍未收到服务端反馈)。 + MODE_10S, + + /// 此模式下:
+ /// * KeepAlive心跳问隔为15秒;
+ /// * 20秒后未收到服务端心跳反馈即认为连接已断开(相当于连续2个心跳间隔(即算法最大30秒延迟后)后仍未收到服务端反馈)。 + /// + /// @since 5.0 + MODE_15S, + + /// 此模式下:
+ /// * KeepAlive心跳问隔为30秒;
+ /// * 35秒后未收到服务端心跳反馈即认为连接已断开(相当于连续2个心跳间隔(即算法最大60秒延迟后)后仍未收到服务端反馈)。 + MODE_30S, + + /// 此模式下:
+ /// * KeepAlive心跳问隔为60秒;
+ /// * 65秒后未收到服务端心跳反馈即认为连接已断开(相当于连续2个心跳间隔(即算法最大120秒延迟后)后仍未收到服务端反馈)。 + MODE_60S, + + /// 此模式下:
+ /// * KeepAlive心跳问隔为120秒;
+ /// * 125秒后未收到服务端心跳反馈即认为连接已断开(相当于连续2个心跳间隔(即算法最大240秒延迟后)后仍未收到服务端反馈)。 + MODE_120S +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/auto_relogin_daemon.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/auto_relogin_daemon.dart new file mode 100644 index 00000000..0bbbfff2 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/auto_relogin_daemon.dart @@ -0,0 +1,118 @@ +// ignore_for_file: constant_identifier_names + +import 'dart:async'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/client_core_sdk.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/local_data_sender.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/log.dart'; + +class AutoReLoginDaemon { + static const String _tag = "AutoReLoginDaemon"; + static const int AUTO_RE$LOGIN_INTERVAL = 3000; + + // + + factory AutoReLoginDaemon.getInstance() => _instance; + + static late final AutoReLoginDaemon _instance = AutoReLoginDaemon._internal(); + + AutoReLoginDaemon._internal() { + init(); + } + + // + + bool _autoReLoginRunning = false; + bool _init = false; + bool _executing = false; + late void Function() _callback; + Timer? _timer; + + void init() { + if (_init) return; + + _callback = () async { + if (!_executing) { + final int code = await _doSendLogin(); + _onSendLogin(code); + } + }; + + _init = true; + } + + Future _doSendLogin() async { + // todo 重连有时会进行两次? 第一次会被关掉, 倒是也可正常使用,但是怕高并发时会白白多一倍的连接数 + //[INFO] - [15:37:48.748][IMCORE-tcp]与{uid:null}/10.10.2.61:55978的会话建立(channelActive)了... | (ServerCoreHandler^sessionCreated:396) + // [DEBUG] - [15:37:48.754]-Dio.netty.recycler.maxCapacityPerThread: 4096 | (Recycler^:100) + // [DEBUG] - [15:37:48.754]-Dio.netty.recycler.maxSharedCapacityFactor: 2 | (Recycler^:101) + // [DEBUG] - [15:37:48.754]-Dio.netty.recycler.linkCapacity: 16 | (Recycler^:102) + // [DEBUG] - [15:37:48.755]-Dio.netty.recycler.ratio: 8 | (Recycler^:103) + // [DEBUG] - [15:37:48.755]-Dio.netty.recycler.delayedQueue.ratio: 8 | (Recycler^:104) + // [INFO] - [15:37:48.771][IMCORE-tcp]与{uid:null}/10.10.2.61:55976的会话建立(channelActive)了... | (ServerCoreHandler^sessionCreated:396) + // [INFO] - [15:37:48.791][IMCORE-tcp]>> 客户端{uid:null}/10.10.2.61:55978发过来的登陆信息内容是:uid=b、token=b、firstLoginTime=1690443347718 | (LogicProcessor^processLogin:161) + // [DEBUG] - [15:37:48.792]【DEBUG_回调通知】正在调用回调方法:OnVerifyUserCallBack...(extra=null) | (ServerEventListenerImpl^onUserLoginVerify:60) + // [DEBUG] - [15:37:48.801]【@】当前在线用户共(1)人-------------------> | (OnlineProcessor^__printOnline:212) + // [DEBUG] - [15:37:48.801]【IM_回调通知onUserLoginSucess】用户:b 上线了! | (ServerEventListenerImpl^onUserLoginSucess:78) + // [WARN] - [15:37:53.822][IMCORE-tcp]>> 客户端{uid:null}/10.10.2.61:55976尚未登陆,{}处理未继续. | (LocalSendHelper^replyDataForUnlogined:368) + // [WARN] - [15:37:53.824][IMCORE-tcp]>> 客户端{uid:null}/10.10.2.61:55976未登陆,服务端反馈发送成功?true(会话即将关闭) | (LocalSendHelper$2^update:379) + // [WARN] - [15:37:53.825][IMCORE-unknow]【注意】会话{uid:null}/10.10.2.61:55976被系统close了,但它里面没有存放user_id,它 很可能是没有成功合法认证而被提前关闭,从而正常释放资源。 | (ServerCoreHandler^sessionClosed:378) + // [INFO] - [15:37:59.815][IMCORE-tcp]客户端{uid:b}/10.10.2.61:55978的会话已超时失效,很可能是对方非正常通出或网络故障,即 将以会话异常的方式执行关闭流程 ... | (MBTCPClientInboundHandler^exceptionCaught:58) + // [DEBUG] - [15:37:59.815][IMCORE-tcp]此客户端的Channel抛出了exceptionCaught,原因是:null,可以提前close掉了哦! | (ServerCoreHandler^exceptionCaught:154) + // io.netty.handler.timeout.ReadTimeoutException: null + // [INFO] - [15:37:59.816][IMCORE-unknow]{uid:b}/10.10.2.61:55978的会话已关闭(user_id=b, firstLoginTime=1690443347718)了... | (ServerCoreHandler^sessionClosed:321) + // [INFO] - [15:37:59.816].......... 【0】[当前正在被关闭的session] session.hashCode=79431750, session.ip+port=/10.10.2.61:55978 | (ServerCoreHandler^sessionClosed:327) + // [INFO] - [15:37:59.817].......... 【1】[处于在线列表中的session] session.hashCode=79431750, session.ip+port= | (ServerCoreHandler^sessionClosed:332) + // [DEBUG] - [15:37:59.817]【DEBUG_回调通知onUserLogout】用户:b 离线了(beKickoutCode=-1)! | (ServerEventListenerImpl^onUserLogout:94) + // [INFO] - [15:37:59.892][IMCORE-tcp]与{uid:null}/10.10.2.61:55980的会话建立(channelActive)了... | (ServerCoreHandler^sessionCreated:396) + // [INFO] - [15:37:59.896][IMCORE-tcp]>> 客户端{uid:null}/10.10.2.61:55980发过来的登陆信息内容是:uid=b、token=b、firstLoginTime=1690443347718 | (LogicProcessor^processLogin:161) + // [DEBUG] - [15:37:59.897]【DEBUG_回调通知】正在调用回调方法:OnVerifyUserCallBack...(extra=null) | (ServerEventListenerImpl^onUserLoginVerify:60) + // [DEBUG] - [15:37:59.898]【@】当前在线用户共(1)人-------------------> | (OnlineProcessor^__printOnline:212) + // [DEBUG] - [15:37:59.898]【IM_回调通知onUserLoginSucess】用户:b 上线了! | (ServerEventListenerImpl^onUserLoginSucess:78) + _executing = true; + Log.info( + "$_tag【IMCORE-TCP】自动重新登陆线程执行中, autoReLogin=${ClientCoreSDK.autoReLogin} ...", + _tag); + int code = -1; + if (ClientCoreSDK.autoReLogin) { + code = LocalDataSender.getInstance() + .sendLogin(ClientCoreSDK.getInstance().getCurrentLoginInfo()!); + } + return code; + } + + void _onSendLogin(int result) { + if (result == 0) { + _executing = false; + } + } + + void stop() { + _timer?.cancel(); + _autoReLoginRunning = false; + _executing = false; + } + + void start(bool immediately) { + stop(); + + int time = immediately ? 0 : AUTO_RE$LOGIN_INTERVAL; + + _timer = Timer(Duration(milliseconds: time), () { + _timer = Timer.periodic( + const Duration(milliseconds: AUTO_RE$LOGIN_INTERVAL), (timer) { + if (_autoReLoginRunning) _callback(); + }); + }); + + Log.info("自动重登启动", _tag); + _autoReLoginRunning = true; + } + + bool isAutoReLoginRunning() { + return _autoReLoginRunning; + } + + bool isInit() { + return _init; + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/keep_alive_daemon.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/keep_alive_daemon.dart new file mode 100644 index 00000000..cc198f28 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/keep_alive_daemon.dart @@ -0,0 +1,184 @@ +// ignore_for_file: non_constant_identifier_names + +import 'dart:async'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/client_core_sdk.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/local_data_sender.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/Handler.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/log.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/mb_observer.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/mb_simple_timer.dart'; + +class KeepAliveDaemon { + static const String _tag = "KeepAliveDaemon"; + + /// Keep Alive 心跳发送时间间隔(单位:毫秒),默认15000毫秒(即15秒) + /// + /// 心跳间隔越短则保持会话活性的健康度更佳,但将使得在大量客户端连接情况下服务端因此而增加负载, + /// 且手机将消耗更多电量和流量,所以此间隔需要权衡(建议为:大于3秒 且 小于270秒(即4分半钟) )! + /// + /// 说明:此参数用于设定客户端发送到服务端的心跳间隔,心跳包的作用是用来保持与服务端的会话活性 + /// (更准确的说是为了避免客户端因路由器的NAT算法而导致路由器端口老化,相关知识见此文:http://www.52im.net/thread-281-1-1.html). + /// 参定此参数的同时,也需要相应设置服务端的ServerLauncher.SESION_RECYCLER_EXPIRE参数。 + /// + static int KEEP_ALIVE_INTERVAL = 15 * 1000; + + /// 收到服务端响应心跳包的超时间时间(单位:毫秒),默认(15 * 1000 + 5000)= 20000 毫秒(即20秒) + /// + /// 超过这个时间客户端将判定与服务端的网络连接已断开(此间隔建议为(KEEP_ALIVE_INTERVAL * 1) + 5 秒), + /// 没有上限,但不可太长,否则将不能即时反映出与服务器端的连接断开(比如掉掉线时),请从 能忍受的反应时长和即时性上做出权衡。 + /// + /// 本参数除与 [KEEP_ALIVE_INTERVAL] 有关联外,不受其它设置影响。 + /// + static int NETWORK_CONNECTION_TIME_OUT = KEEP_ALIVE_INTERVAL + 5000; + + /// 心跳包超时检查定时器的运行间隔时间(单位:毫秒),默认(2 * 1000)= 2000 毫秒(即2秒) + /// + /// 此时间将决定断网感应灵敏度。建议设置值的范围为1~5秒内。 + /// + static int NETWORK_CONNECTION_TIME_OUT_CHECK_INTERVAL = 2 * 1000; + + bool _keepAliveRunning = false; + int _lastGetKeepAliveResponseFromServerTimestamp = 0; + MBObserver? _networkConnectionLostObserver; + + Timer? _keepAliveHandler; + late Runnable _keepAliveRunnable; + + bool _keepAliveTaskExecuting = false; + bool _keepAliveWillStop = false; + + late MBSimpleTimer _keepAliveTimeoutTimer; + + bool _init = false; + + // + + factory KeepAliveDaemon.getInstance() => _instance; + + static late final KeepAliveDaemon _instance = KeepAliveDaemon._internal(); + + KeepAliveDaemon._internal() { + _initFunc(); + } + + // + + void _initFunc() { + if (_init) return; + + _keepAliveRunnable = () async { + if (!_keepAliveTaskExecuting) { + final int code = _doKeepAlive(); + _onKeepAlive(code); + } + }; + + _keepAliveTimeoutTimer = + MBSimpleTimer(NETWORK_CONNECTION_TIME_OUT_CHECK_INTERVAL, () { + Log.info("【IMCORE-TCP】心跳[超时检查]线程执行中... $hashCode ", _tag); + _doTimeoutCheck(); + }); + + _init = true; + } + + int _doKeepAlive() { + _keepAliveTaskExecuting = true; + Log.info("【IMCORE-TCP】心跳包[发送]线程执行中...", _tag); + int code = LocalDataSender.getInstance().sendKeepAlive(); + + return code; + } + + void _onKeepAlive(int code) { + bool isInitialedForKeepAlive = _isInitialedForKeepAlive(); + if (isInitialedForKeepAlive) { + updateGetKeepAliveResponseFromServerTimestamp(); + } + + _keepAliveTaskExecuting = false; + if (!_keepAliveWillStop) { + _keepAliveHandler = + Timer(Duration(milliseconds: KEEP_ALIVE_INTERVAL), () { + _keepAliveRunnable.call(); + }); + } + } + + void _doTimeoutCheck() { + bool isInitialedForKeepAlive = _isInitialedForKeepAlive(); + if (!isInitialedForKeepAlive) { + int now = ClientCoreSDK.getCurrentTimeStamp(); + if (now - _lastGetKeepAliveResponseFromServerTimestamp >= + NETWORK_CONNECTION_TIME_OUT) { + Log.info("【IMCORE-TCP】心跳机制已判定网络断开,将进入断网通知和重连处理逻辑 ...", _tag); + notifyConnectionLost(); + _keepAliveWillStop = true; + } + } + } + + bool _isInitialedForKeepAlive() { + return _lastGetKeepAliveResponseFromServerTimestamp == 0; + } + + /// 心跳线程算法已判定需要与服务器的“通信通道”断开,调用此方法将进入框架的“通信通道”断开处理逻辑 + /// 本方法,目前属于 MobileIMSDK 框架算法的一部分,暂时无需也不建议由应用层开发者自行调用。 + void notifyConnectionLost() { + stop(); + _networkConnectionLostObserver?.update(true, null); + } + + /// 是否正在运行中 + bool isKeepAliveRunning() { + return _keepAliveRunning; + } + + /// 本类对象是否已补初始化过 + bool isInit() { + return _init; + } + + /// 收到服务端反馈的心跳包时调用此方法:作用是更新服务端最背后的响应时间戳 + /// 本方法的调用,目前属于 MobileIMSDK 算法的一部分,暂时无需也不建议由应用层自行调用。 + void updateGetKeepAliveResponseFromServerTimestamp() { + _lastGetKeepAliveResponseFromServerTimestamp = + ClientCoreSDK.getCurrentTimeStamp(); + } + + /// 设置网络断开事件观察者 + /// 本方法的调用,目前属于 MobileIMSDK 算法的一部分,暂时无需也不建议由应用层自行调用。 + void setNetworkConnectionLostObserver( + MBObserver networkConnectionLostObserver) { + _networkConnectionLostObserver = networkConnectionLostObserver; + } + + ///会尝试首先调用 stop()方法,以便确保实例被启动前是真正处 于停止状态,这也意味着可无害调用本方法。 + /// + /// 目前属于MobileIMSDK算法的一部分,暂时无需也不建议由应用层自行调用。 + /// + /// 参数: + /// [immediately] - true 表示立即执行线程作业,否则直到 [KEEP_ALIVE_INTERVAL] 执行间隔的到来才 进行首次作业的执行。 + /// + void start(bool immediately) { + stop(); + var time = immediately ? 0 : KEEP_ALIVE_INTERVAL; + _keepAliveHandler = Timer(Duration(milliseconds: time), () { + _keepAliveRunnable.call(); + }); + _keepAliveRunning = true; + _keepAliveWillStop = false; + Log.info("【IMCORE-TCP】心跳 start() time:$time ... ", _tag); + _keepAliveTimeoutTimer.start(immediately); + } + + /// 无条件停止本实例,目前属于MobileIMSDK算法的一部分,暂时无需也不建议由应用层自行调用。 + void stop() { + _keepAliveTimeoutTimer.stop(); + + _keepAliveHandler?.cancel(); + _keepAliveRunning = false; + _keepAliveWillStop = false; + _lastGetKeepAliveResponseFromServerTimestamp = 0; + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/local_data_receiver.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/local_data_receiver.dart new file mode 100644 index 00000000..478a171c --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/local_data_receiver.dart @@ -0,0 +1,252 @@ +import 'package:mobile_im_sdk_flutter_tcp/sdk/client_core_sdk.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/auto_relogin_daemon.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/keep_alive_daemon.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/local_data_sender.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/local_socket_provider.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/qos_4_receive_daemon.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/qos_4_send_daemon.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/log.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/mb_observer.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/error_code.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/protocol_i_type.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/protocol.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/protocol_factory.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/s/p_error_response.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/s/p_kickout_info.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/s/p_login_info_response.dart'; + +class LocalDataReceiver { + final String _tag = "LocalDataReceiver"; + + // + + factory LocalDataReceiver.getInstance() => _instance; + + static late final LocalDataReceiver _instance = LocalDataReceiver._internal(); + + LocalDataReceiver._internal() { + _init(); + } + + // + + void _init() {} + + void handleProtocolJson(String fullProtocolOfBodyJson) { + _handleProtocolImpl(fullProtocolOfBodyJson); + } + + void _handleProtocolImpl(String? fullProtocolOfBodyJson) { + if (fullProtocolOfBodyJson == null || fullProtocolOfBodyJson.isEmpty) { + Log.info("【IMCORE-TCP】无效的 fullProtocolOfBodyJson(.length == 0)!", _tag); + return; + } + + final Protocol pFromServer; + try { + pFromServer = ProtocolFactory.parseFromString(fullProtocolOfBodyJson); + + if (pFromServer.isQoS()) { + Log.info("pb预处理:$pFromServer", _tag); + if (pFromServer.getType() == + ProtocolTypeS.FROM_SERVER_TYPE_OF_RESPONSE$LOGIN && + ProtocolFactory.parsePLoginInfoResponse( + pFromServer.getDataContent()) + .getCode() != + 0) { + Log.info("IMCORE-TCP】这是服务端的登陆返回响应包,且服务端判定登陆失败(即code!=0),本次无需发送ACK应答包!", + _tag); + } else { + if (QoS4ReceiveDaemon.getInstance() + .hasReceived(pFromServer.getFp())) { + Log.info( + "【IMCORE-TCP】【QoS机制】${pFromServer.getFp()} 已经存在于发送列表中,这是重复包,通知应用层收到该包!", + _tag); + QoS4ReceiveDaemon.getInstance().addReceived(pFromServer); + _sendReceivedBack(pFromServer); + return; + } + + QoS4ReceiveDaemon.getInstance().addReceived(pFromServer); + _sendReceivedBack(pFromServer); + } + } + } catch (e) { + Log.error("pb预处理错误:${e.toString()}", _tag); + return; + } + + try { + switch (pFromServer.getType()) { + case ProtocolTypeC.FROM_CLIENT_TYPE_OF_COMMON$DATA: + { + _onReceivedCommonData(pFromServer); + break; + } + case ProtocolTypeS.FROM_SERVER_TYPE_OF_RESPONSE$KEEP$ALIVE: + { + _onServerResponseKeepAlive(); + break; + } + case ProtocolTypeC.FROM_CLIENT_TYPE_OF_RECEIVED: + { + _onMessageRecievedACK(pFromServer); + break; + } + case ProtocolTypeS.FROM_SERVER_TYPE_OF_RESPONSE$LOGIN: + { + _onServerResponseLogin(pFromServer); + break; + } + case ProtocolTypeS.FROM_SERVER_TYPE_OF_RESPONSE$FOR$ERROR: + { + _onServerResponseError(pFromServer); + break; + } + case ProtocolTypeS.FROM_SERVER_TYPE_OF_KICKOUT: + { + _onKickOut(pFromServer); + break; + } + default: + Log.info( + "【IMCORE-TCP】收到的服务端消息类型:${pFromServer.getType()},但目前该类型客户端不支持解析和处理!", + _tag); + break; + } + } catch (e) { + Log.info( + "【IMCORE-TCP】处理消息的过程中发生了错误. ${e.toString()},${pFromServer.toJson()}", + _tag); + } + } + + void _onReceivedCommonData(Protocol pFromServer) { + Log.info( + ">>>>>>>>>>>>>>>>>>>>>>>>>>>>收到${pFromServer.getFrom()}发过来的消息:${pFromServer.getDataContent()},${pFromServer.getTo()}", + _tag); + + ClientCoreSDK.getInstance().getChatMessageEvent()?.onReceiveMessage( + pFromServer.getFp(), + pFromServer.getFrom(), + pFromServer.getDataContent(), + pFromServer.getTypeu()); + } + + void _onServerResponseKeepAlive() { + Log.info("【IMCORE-TCP】收到服务端回过来的Keep Alive心跳响应包.", _tag); + KeepAliveDaemon.getInstance() + .updateGetKeepAliveResponseFromServerTimestamp(); + } + + void _onMessageRecievedACK(Protocol pFromServer) { + String theFingerPrint = pFromServer.getDataContent(); + + Log.info( + "【IMCORE-TCP】【QoS】收到 ${pFromServer.getFrom()}发过来的指纹为$theFingerPrint的应答包.", + _tag); + + ClientCoreSDK.getInstance() + .getMessageQoSEvent() + ?.messagesBeReceived(theFingerPrint); + + QoS4SendDaemon.getInstance().removeByFp(theFingerPrint); + } + + void _onServerResponseLogin(Protocol pFromServer) { + PLoginInfoResponse loginInfoRes = + ProtocolFactory.parsePLoginInfoResponse(pFromServer.getDataContent()); + if (loginInfoRes.getCode() == 0) { + if (!ClientCoreSDK.getInstance().isLoginHasInit()) { + ClientCoreSDK.getInstance() + .saveFirstLoginTime(loginInfoRes.getFirstLoginTime()); + } + _fireConnectedToServer(); + } else { + Log.info("【IMCORE-TCP】登陆验证失败,错误码=${loginInfoRes.getCode()}!", _tag); + LocalSocketProvider.getInstance().closeLocalSocket(); + ClientCoreSDK.getInstance().setConnectedToServer(false); + } + + ClientCoreSDK.getInstance() + .getChatBaseEvent() + ?.onLoginResponse(loginInfoRes.getCode()); + } + + void _onServerResponseError(Protocol pFromServer) { + PErrorResponse errorRes = + ProtocolFactory.parsePErrorResponse(pFromServer.getDataContent()); + if (errorRes.getErrorCode() == ErrorCodeForS.RESPONSE_FOR_UN_LOGIN) { + ClientCoreSDK.getInstance().setLoginHasInit(false); + + Log.info("【IMCORE-TCP】收到服务端的“尚未登陆”的错误消息,心跳线程将停止,请应用层重新登陆.", _tag); + KeepAliveDaemon.getInstance().stop(); + AutoReLoginDaemon.getInstance().start(false); + } + + ClientCoreSDK.getInstance() + .getChatMessageEvent() + ?.onErrorResponse(errorRes.getErrorCode(), errorRes.getErrorMsg()); + } + + void _onKickOut(Protocol pFromServer) { + Log.info("【IMCORE-TCP】收到服务端发过来的“被踢”指令.", _tag); + + ClientCoreSDK.getInstance().release(); + + PKickoutInfo kickOutInfo = + ProtocolFactory.parsePKickoutInfo(pFromServer.getDataContent()); + ClientCoreSDK.getInstance().getChatBaseEvent()?.onKickOut(kickOutInfo); + + ClientCoreSDK.getInstance().getChatBaseEvent()?.onLinkClose(-1); + } + + void _fireConnectedToServer() { + Log.info("【IMCORE-TCP】 取得和服务器的连接.", _tag); + + ClientCoreSDK.getInstance().setLoginHasInit(true); + AutoReLoginDaemon.getInstance().stop(); + + KeepAliveDaemon.getInstance() + .setNetworkConnectionLostObserver(MBObserver((success, extraObj) { + _fireDisconnectedToServer(); + })); + + KeepAliveDaemon.getInstance().start(false); + + QoS4SendDaemon.getInstance().startup(true); + QoS4ReceiveDaemon.getInstance().startup(true); + ClientCoreSDK.getInstance().setConnectedToServer(true); + } + + void _fireDisconnectedToServer() { + Log.info("【IMCORE-TCP】 失去和服务器的连接.", _tag); + + ClientCoreSDK.getInstance().setConnectedToServer(false); + LocalSocketProvider.getInstance().closeLocalSocket(); + + QoS4SendDaemon.getInstance().stop(); + QoS4ReceiveDaemon.getInstance().stop(); + + // 建议:此参数可由true改为false,防止服务端重启等情况下,客户端立即重连等 + AutoReLoginDaemon.getInstance().start(true); + + ClientCoreSDK.getInstance().getChatBaseEvent()?.onLinkClose(-1); + } + + void _sendReceivedBack(final Protocol pFromServer) async { + if (pFromServer.getFp() != null) { + var pb = ProtocolFactory.createRecivedBack( + pFromServer.getTo(), pFromServer.getFrom(), pFromServer.getFp(), + bridge: pFromServer.isBridge()); + var i = LocalDataSender.getInstance().sendCommonDataObj(pb); + Log.info( + "【IMCORE-TCP】【QoS】向 ${pFromServer.getFrom()} 发送 ${pFromServer.getFp()} 包的应答包成功,from= ${pFromServer.getTo()}, resultCode = $i!", + _tag); + } else { + Log.info( + "【IMCORE-TCP】【QoS】收到 ${pFromServer.getFrom()} 发过来需要QoS的包,但它的指纹码却为null!无法发应答包!", + _tag); + } + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/local_data_sender.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/local_data_sender.dart new file mode 100644 index 00000000..2959f86d --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/local_data_sender.dart @@ -0,0 +1,134 @@ +import 'package:mobile_im_sdk_flutter_tcp/sdk/client_core_sdk.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/qos_4_send_daemon.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/Ext.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/log.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/mb_observer.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/c/p_login_info.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/error_code.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/protocol.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/Protocol_factory.dart'; +import 'local_socket_provider.dart'; + +class LocalDataSender { + final String _tag = "LocalDataSender"; + + // + + factory LocalDataSender.getInstance() => _instance; + + static late final LocalDataSender _instance = LocalDataSender._internal(); + + LocalDataSender._internal(); + + // + + int sendLogout() { + int code = 0; + if (ClientCoreSDK.getInstance().isLoginHasInit()) { + final loginInfo = ClientCoreSDK.getInstance().getCurrentLoginInfo()!; + code = _send(ProtocolFactory.createPLogoutInfo(loginInfo)); + } + + if (code == 0) { + ClientCoreSDK.getInstance().setLoginHasInit(false); + ClientCoreSDK.getInstance().setCurrentLoginInfo(null); + ClientCoreSDK.getInstance().setConnectedToServer(false); + } + + return code; + } + + int sendLogin(PLoginInfo loginInfo) { + int codeForCheck = _checkBeforeSend(); + if (codeForCheck != ErrorCode.COMMON_CODE_OK) return codeForCheck; + + if (!LocalSocketProvider.getInstance().isLocalSocketReady()) { + Log.info( + "【IMCORE-TCP】发送登陆指令时,socket连接未就绪,首先开始尝试发起连接(登陆指令将在连接成功后的回调中自动发出)。。。。", + _tag); + + MBObserver connectionDoneObserver = MBObserver((success, extraObj) { + Log.info("【IMCORE-TCP】[来自 Tcp 的连接结果回调观察者通知]socket连接:$success ,$extraObj", + _tag); + if (success) { + _sendLoginImpl(loginInfo); + } + }); + + LocalSocketProvider.getInstance() + .setConnectionDoneObserver(connectionDoneObserver); + + return LocalSocketProvider.getInstance().resetLocalSocket() != null + ? ErrorCode.COMMON_CODE_OK + : ErrorCodeForC.BAD_CONNECT_TO_SERVER; + } else { + return _sendLoginImpl(loginInfo); + } + } + + int _sendLoginImpl(PLoginInfo loginInfo) { + int code = _send(ProtocolFactory.createPLoginInfo(loginInfo)); + if (code == 0) { + ClientCoreSDK.getInstance().setCurrentLoginInfo(loginInfo); + } + return code; + } + + int sendKeepAlive() { + return _send(ProtocolFactory.createPKeepAlive( + ClientCoreSDK.getInstance().getCurrentLoginInfo()!.getLoginUserId())); + } + + int sendCommonData(String dataContentWidthStr, String to_user_id, + {int typeu = -1, String? fingerPrint, bool QoS = true}) { + return sendCommonDataObj(ProtocolFactory.createCommonData( + dataContentWidthStr, + ClientCoreSDK.getInstance().getCurrentLoginInfo()!.getLoginUserId(), + to_user_id, + QoS, + fingerPrint: fingerPrint, + typeu: typeu)); + } + + int sendCommonDataObj(Protocol? p) { + if (p != null) { + int code = _send(p); + if (code == 0) { + if (p.isQoS() && !QoS4SendDaemon.getInstance().exist(p.getFp())) { + QoS4SendDaemon.getInstance().put(p); + } + } + return code; + } else { + return ErrorCode.COMMON_INVALID_Protocol; + } + } + + int _send(Protocol Protocol) { + int codeForCheck = _checkBeforeSend(); + if (codeForCheck != ErrorCode.COMMON_CODE_OK) { + return codeForCheck; + } + + var dataStr = Protocol.toJson().toJsonStr(); + Log.info(" _send 发送消息:$dataStr", _tag); + + var tcpSm = LocalSocketProvider.getInstance().getLocalSocket(); + Log.info(" _send tcpSm:${tcpSm?.isActive()}", _tag); + if (tcpSm != null && tcpSm.isActive()) { + return tcpSm.sendMsg(dataStr) + ? ErrorCode.COMMON_CODE_OK + : ErrorCode.COMMON_DATA_SEND_FAILED; + } else { + Log.info("【IMCORE-TCP】socket 未连接,无法发送,本条将被忽略(dataStr=$dataStr)!", _tag); + return ErrorCode.COMMON_CODE_OK; + } + } + + int _checkBeforeSend() { + if (!ClientCoreSDK.getInstance().isInitialed()) { + return ErrorCodeForC.CLIENT_SDK_NO_INITIALED; + } + return ErrorCode.COMMON_CODE_OK; + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/local_socket_provider.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/local_socket_provider.dart new file mode 100644 index 00000000..06a7c887 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/local_socket_provider.dart @@ -0,0 +1,109 @@ +import 'package:mobile_im_sdk_flutter_tcp/sdk/client_core_sdk.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/conf/config_entity.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/local_data_receiver.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/log.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/mb_observer.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/tcp_socket_manager.dart'; +import 'keep_alive_daemon.dart'; + +class LocalSocketProvider { + final String _tag = "LocalSocketProvider"; + + // + + factory LocalSocketProvider.getInstance() => _instance; + + static late final LocalSocketProvider _instance = + LocalSocketProvider._internal(); + + LocalSocketProvider._internal() { + _localSocket = TcpSocketManager(); + _localSocket.init(ConfigEntity.serverIP, ConfigEntity.serverPort, + onConnectionLost: () { + Log.info( + "【IMCORE】连接已断开。。。。(isLocalSocketReady=${isLocalSocketReady()}, ClientCoreSDK.connectedToServer=${ClientCoreSDK.getInstance().isConnectedToServer()})", + _tag); + + // 快速响应tcp连接断开事件,第一时间反馈给上层 + if (ClientCoreSDK.getInstance().isConnectedToServer()) { + Log.info( + "【IMCORE】连接已断开,立即提前进入框架的“通信通道”断开处理逻辑(而不是等心跳线程探测到,那就已经比较迟了)......", + _tag); + KeepAliveDaemon.getInstance().notifyConnectionLost(); + } + }, messageReceived: (msg) { + LocalDataReceiver.getInstance().handleProtocolJson(msg); + }); + } + + // + + late TcpSocketManager _localSocket; + + MBObserver? _connectionDoneObserver; + + void setConnectionDoneObserver(MBObserver connectionDoneObserver) { + _connectionDoneObserver = connectionDoneObserver; + } + + TcpSocketManager? resetLocalSocket() { + try { + closeLocalSocket(); + tryConnectToHost(); + return _localSocket; + } catch (e) { + Log.error("【IMCORE-TCP】重置localSocket时出错,原因是:${e.toString()}", _tag); + return null; + } + } + + tryConnectToHost() { + try { + Log.info("【IMCORE-TCP】tryConnectToHost并获取connection开始了...", _tag); + + _localSocket.connectSocket(onSuccess: () { + Log.info( + "【IMCORE-tryConnectToHost-异步回调】Connection established successfully", + _tag); + _connectionDoneObserver?.update(true, null); + _connectionDoneObserver = null; + }, onError: (e) { + Log.error( + "【IMCORE-tryConnectToHost-异步回调】连接失败,原因是:${e.toString()}", _tag); + }); + } catch (e) { + Log.error( + "【IMCORE-TCP】连接Server(IP[${ConfigEntity.serverIP}],PORT[${ConfigEntity.serverPort}])失败 ${e.toString()}", + _tag); + } + } + + bool isLocalSocketReady() { + return _localSocket.isActive(); + } + + TcpSocketManager? getLocalSocket() { + if (isLocalSocketReady()) { + return _localSocket; + } else { + return resetLocalSocket(); + } + } + + void closeLocalSocket({bool silent = true}) { + if (!silent) { + Log.info("【IMCORE-TCP】正在closeLocalSocket()...", _tag); + } + try { + _localSocket.close(); + } catch (e) { + Log.error( + "【IMCORE-TCP】在closeLocalSocket方法中试图释放localSocket资源时:${e.toString()}", + _tag); + } + + if (!silent) { + Log.info("【IMCORE-TCP】Socket处于未初化状态(可能是您还未登陆),无需关闭。", _tag); + } + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/qos_4_receive_daemon.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/qos_4_receive_daemon.dart new file mode 100644 index 00000000..9d033c01 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/qos_4_receive_daemon.dart @@ -0,0 +1,135 @@ +// ignore_for_file: constant_identifier_names + +import 'dart:collection'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/client_core_sdk.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/Ext.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/Handler.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/log.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/protocol.dart'; + +class QoS4ReceiveDaemon { + final String _tag = "QoS4ReceiveDaemon"; + + // + + factory QoS4ReceiveDaemon.getInstance() => _instance; + + static late final QoS4ReceiveDaemon _instance = QoS4ReceiveDaemon._internal(); + + QoS4ReceiveDaemon._internal() { + _initM(); + } + + // + + static const CHECK_INTERVAL = 5 * 60 * 1000; // 5分钟 + static const MESSAGES_VALID_TIME = 10 * 60 * 1000; // 10分钟 + + final LinkedHashMap _receivedMessages = + LinkedHashMap(); + + bool _running = false; + bool _executing = false; + bool _init = false; + + late Handler _handler; + late Runnable _runnable; + + void _initM() { + if (_init) return; + + _handler = Handler(); + _runnable = () async { + if (!_executing) { + _executing = true; + + Log.info( + "【IMCORE-TCP】【QoS接收方】+++++ START 暂存处理线程正在运行中,当前长度 ${_receivedMessages.length} .", + _tag); + + for (String key in _receivedMessages.keys) { + int receivedTime = _receivedMessages[key] ?? 0; + int delta = ClientCoreSDK.getCurrentTimeStamp() - receivedTime; + if (delta >= MESSAGES_VALID_TIME) { + _receivedMessages.remove(key); + Log.info( + "【IMCORE-TCP】【QoS接收方】指纹为 $key 的包已生存 $delta ms(最大允许${MESSAGES_VALID_TIME}ms), 马上将删除之.", + _tag); + } + } + } + }; + + Log.info( + "【IMCORE-TCP】【QoS接收方】+++++ END 暂存处理线程正在运行中,当前长度 ${_receivedMessages.length}.", + _tag); + + _executing = false; + + _handler.postDelayed(_runnable, CHECK_INTERVAL); + + _init = true; + } + + void startup(bool immediately) { + stop(); + + if (_receivedMessages.isNotEmpty) { + for (var key in _receivedMessages.keys) { + putImpl(key); + } + } + + _handler.postDelayed(_runnable, immediately ? 0 : CHECK_INTERVAL); + _running = true; + } + + void stop() { + _handler.removeCallbacks(_runnable); + _running = false; + } + + bool isRunning() { + return _running; + } + + bool isInit() { + return _init; + } + + void addReceived(Protocol p) { + if (!p.isQoS()) return; + + var fingerPrintOfProtocol = p.toJson().toJsonStr(); + + if (fingerPrintOfProtocol.isEmpty) { + Log.info("【IMCORE-TCP】无效的 fingerPrintOfProtocol isEmpty!", _tag); + return; + } + + if (_receivedMessages.containsKey(fingerPrintOfProtocol)) { + Log.info( + "【IMCORE-TCP】【QoS接收方】指纹为 $fingerPrintOfProtocol 的消息已经存在于接收列表中,该消息重复了(原理可能是对方因未收到应答包而错误重传导致),更新收到时间戳哦.", + _tag); + } + + putImpl(fingerPrintOfProtocol); + } + + void putImpl(String fingerPrintOfProtocol) { + _receivedMessages[fingerPrintOfProtocol] = + DateTime.now().millisecondsSinceEpoch; + } + + bool hasReceived(String fingerPrintOfProtocol) { + return _receivedMessages.containsKey(fingerPrintOfProtocol); + } + + void clear() { + _receivedMessages.clear(); + } + + int size() { + return _receivedMessages.length; + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/qos_4_send_daemon.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/qos_4_send_daemon.dart new file mode 100644 index 00000000..7aa49a37 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/core/qos_4_send_daemon.dart @@ -0,0 +1,191 @@ +// ignore_for_file: constant_identifier_names + +import 'dart:collection'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/client_core_sdk.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/local_data_sender.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/Handler.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/log.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/protocol.dart'; + +class QoS4SendDaemon { + final _tag = "QoS4SendDaemon"; + + // + + factory QoS4SendDaemon.getInstance() => _instance; + + static late final QoS4SendDaemon _instance = QoS4SendDaemon._internal(); + + QoS4SendDaemon._internal() { + _initM(); + } + + // + + static const CHECK_INTERVAL = 5000; + static const MESSAGES_JUST$NOW_TIME = 3 * 1000; + static const QOS_TRY_COUNT = 2; + + final HashMap _sentMessages = HashMap(); + final HashMap _sendMessagesTimestamp = HashMap(); + + bool _running = false; + bool _executing = false; + bool _init = false; + + late Handler _handler; + late Runnable _runnable; + + void _initM() { + if (_init) return; + + _handler = Handler(); + _runnable = () async { + if (!_executing) { + List lostMessages = []; + final List ret = _doRetryCheck(lostMessages); + _onRetryCheck(ret); + } + }; + + _init = true; + } + + List _doRetryCheck(List lostMessages) { + _executing = true; + + try { + if (_sentMessages.isNotEmpty) { + Log.info( + "【IMCORE-TCP】【QoS】====== 消息发送质量保证线程运行中, 当前需要处理的列表长度为 ${_sentMessages.length} ...", + _tag); + } + + final needRemoveKeyList = List.empty(growable: true); + + for (String key in _sentMessages.keys) { + final p = _sentMessages[key]; + if (p != null && p.isQoS()) { + if (p.getRetryCount() >= QOS_TRY_COUNT) { + Log.info( + "【IMCORE-TCP】【QoS】指纹为 ${p.getFp()} 的消息包重传次数已达 ${p.getRetryCount()}(最多 $QOS_TRY_COUNT 次)上限,将判定为丢包!", + _tag); + lostMessages.add(p); + needRemoveKeyList.add(p.getFp()); + } else { + // Bug Fix: 解决了无线网络延较大时,刚刚发出的消息在其应答包还在途中时被错误地进行重传 + final sendMessageTimestamp = _sendMessagesTimestamp[key]; + final delta = ClientCoreSDK.getCurrentTimeStamp() - + (sendMessageTimestamp ?? 0); + if (delta <= MESSAGES_JUST$NOW_TIME) { + Log.info( + "【IMCORE-TCP】【QoS】指纹为 $key 的包距\"刚刚\"发出才 $delta ms(<=${MESSAGES_JUST$NOW_TIME} ms将被认定是\"刚刚\"), 本次不需要重传哦.", + _tag); + } + // Bug Fix END + else { + var code = LocalDataSender.getInstance().sendCommonDataObj(p); + p.increaseRetryCount(); + if (code == 0) { + Log.info( + "【IMCORE-TCP】【QoS】指纹为 ${p.getFp()} 的消息包已成功进行重传,此次之后重传次数已达 ${p.getRetryCount()} (最多 $QOS_TRY_COUNT 次).", + _tag); + } else { + Log.info( + "【IMCORE-TCP】【QoS】指纹为 ${p.getFp()} 的消息包重传失败,它的重传次数之前已累计为 ${p.getRetryCount()} (最多 $QOS_TRY_COUNT 次).", + _tag); + } + } + } + } else { + needRemoveKeyList.add(key); + } + } + + for (var element in needRemoveKeyList) { + removeByFp(element); + } + } catch (eee) { + Log.error("【IMCORE-TCP】【QoS】消息发送质量保证线程运行时发生异常,$eee", _tag); + } + return lostMessages; + } + + void _onRetryCheck(List al) { + if (al.isNotEmpty) notifyMessageLost(al); + _executing = false; + _handler.postDelayed(_runnable, CHECK_INTERVAL); + } + + void notifyMessageLost(List lostMessages) { + ClientCoreSDK.getInstance() + .getMessageQoSEvent() + ?.messagesLost(lostMessages); + } + + void startup(bool immediately) { + stop(); + _handler.postDelayed(_runnable, immediately ? 0 : CHECK_INTERVAL); + _running = true; + } + + void stop() { + _handler.removeCallbacks(_runnable); + _running = false; + } + + bool isRunning() { + return _running; + } + + bool isInit() { + return _init; + } + + bool exist(String fingerPrint) { + return _sentMessages[fingerPrint] != null; + } + + void put(Protocol? p) { + if (p == null) { + Log.info("Invalid arg p==null.", _tag); + return; + } + if (p.getFp() == null) { + Log.info("Invalid arg p.getFp() == null.", _tag); + return; + } + + if (!p.isQoS()) { + Log.info("This protocol is not QoS pkg, ignore it!", _tag); + return; + } + + if (_sentMessages[p.getFp()] != null) { + Log.info( + "【IMCORE-TCP】【QoS】指纹为 ${p.getFp()} 的消息已经放入了发送质量保证队列,该消息为何会重复?(生成的指纹码重复?还是重复put?)", + _tag); + } + + var key = p.getFp(); + _sentMessages[key] = p; + _sendMessagesTimestamp[key] = ClientCoreSDK.getCurrentTimeStamp(); + } + + void removeByFp(final String fingerPrint) { + _sendMessagesTimestamp.remove(fingerPrint); + var removedObj = _sentMessages.remove(fingerPrint); + Log.info( + "【IMCORE-TCP】【QoS】指纹为 $fingerPrint 的消息已成功从发送质量保证队列中移除(可能是收到接收方的应答也可能是达到了重传的次数上限),重试次数= ${removedObj?.getRetryCount()}", + _tag); + } + + void clear() { + _sentMessages.clear(); + _sendMessagesTimestamp.clear(); + } + + int size() { + return _sentMessages.length; + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/event/chat_base_event.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/event/chat_base_event.dart new file mode 100644 index 00000000..8328c35f --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/event/chat_base_event.dart @@ -0,0 +1,22 @@ +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/s/p_kickout_info.dart'; + +abstract class ChatBaseEvent { + + /// 本地用户的登陆结果回调事件通知。 + /// + /// @param [errorCode] 服务端反馈的登录结果:0 表示登陆成功,否则为服务端自定义的出错代码(按照约定通常为>=1025的数) + void onLoginResponse(int errorCode); + + /// 与服务端的通信断开的回调事件通知。 + ///
+ /// 该消息只有在客户端连接服务器成功之后网络异常中断之时触发。
+ /// 导致与与服务端的通信断开的原因有(但不限于):无线网络信号不稳定、WiFi与2G/3G/4G等同开情况下的网络切换、手机系统的省电策略等。 + /// + /// @param [errorCode] 本回调参数表示表示连接断开的原因,目前错误码没有太多意义,仅作保留字段,目前通常为-1 + void onLinkClose(int errorCode); + + /// 本的用户被服务端踢出的回调事件通知。 + /// + /// @param [kickOutInfo] 被踢信息对象,[PKickoutInfo] 对象中的 code字段定义了被踢原因代码 + void onKickOut(PKickoutInfo kickOutInfo); +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/event/chat_message_event.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/event/chat_message_event.dart new file mode 100644 index 00000000..56366c19 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/event/chat_message_event.dart @@ -0,0 +1,22 @@ +/// 与IM服务器的数据交互事件在此 [ChatTransDataEvent] 子类中实现即可。 +abstract class ChatMessageEvent { + + /// 收到普通消息的回调事件通知。 + ///
应用层可以将此消息进一步按自已的IM协议进行定义,从而实现完整的即时通信软件逻辑。 + /// + /// @param [fingerPrintOfProtocol] 当该消息需要QoS支持时本回调参数为该消息的特征指纹码,否则为null + /// @param [userid] 消息的发送者id(MobileIMSDK框架中规定发送者id="0"即表示是由服务端主动发过的,否则表示的 + /// 是其它客户端发过来的消息) + /// @param [dataContent] 消息内容的文本表示形式 + /// @param [typeu] 意义:应用层专用字段——用于应用层存放聊天、推送等场景下的消息类型。 注意:此值为-1时表示未定 + /// 义。MobileIMSDK框架中,本字段为保留字段,不参与框架的核心算法,专留用应用 层自行定义 + /// 和使用。 默认:-1。 + void onReceiveMessage(String fingerPrintOfProtocol, String userid, + String dataContent, int typeu); + + /// 服务端反馈的出错信息回调事件通知。 + /// + /// @param [errorCode] 错误码,定义在常量表 [ErrorCodeForS] 类中 + /// @param [errorMsg] 描述错误内容的文本信息 + void onErrorResponse(int errorCode, String errorMsg); +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/event/message_qoS_event.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/event/message_qoS_event.dart new file mode 100644 index 00000000..f1b4c696 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/event/message_qoS_event.dart @@ -0,0 +1,25 @@ +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/protocol.dart'; + +/// 消息送达相关事件(由QoS机制通知上来的)在此 [MessageQoSEvent]子类中实现即可。 +abstract class MessageQoSEvent { + + /// 消息未送达的回调事件通知. + /// + /// @param [lostMessages] 由 MobileIMSDK QoS算法判定出来的未送达消息列表(此列表中的[Protocol]对象是原对象的 + /// clone(即原对象的深拷贝),请放心使用哦),应用层可通过指纹特征码找到原消息并可 + /// + void messagesLost(List lostMessages); + + /// 消息已被对方收到的回调事件通知. + ///

+ /// 目前,判定消息被对方收到是有两种可能:
+ ///

    + ///
  • 1) 对方确实是在线并且实时收到了;
  • + ///
  • 2) 对方不在线或者服务端转发过程中出错了,由服务端进行离线存储成功后的反馈(此种情况严格来讲不能算是“已被 + /// 收到”,但对于应用层来说,离线存储了的消息原则上就是已送达了的消息:因为用户下次登陆时肯定能通过HTTP协议取到)。
  • + ///
+ /// + /// @param [theFingerPrint] 已被收到的消息的指纹特征码(唯一ID),应用层可据此ID来找到原先已发生的消息并可在 + /// UI是将其标记为”已送达“或”已读“以便提升用户体验 + void messagesBeReceived(String theFingerPrint); +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/ext.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/ext.dart new file mode 100644 index 00000000..a6af2487 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/ext.dart @@ -0,0 +1,19 @@ +import 'dart:convert'; + +extension MapExt on Map { + String toJsonStr() { + var newMap = {}; + for (var element in entries) { + if (element.value != null) newMap[element.key] = element.value; + } + return json.encode(newMap); + } +} + +extension Tm on DateTime { + String hms() { + return "$hour:$minute:$second"; + } +} + +String nowHmsStrWithMark() => "[${DateTime.now().hms()}]"; diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/handler.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/handler.dart new file mode 100644 index 00000000..79a2436c --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/handler.dart @@ -0,0 +1,35 @@ +import 'dart:async'; +import 'dart:collection'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/log.dart'; + +class Handler { + final HashMap _mRunnableMap = HashMap(); + + void postDelayed(Runnable runnable, int delay) { + var key = runnable.getKey(); + _mRunnableMap[key]?.cancel(); + _mRunnableMap[key] = Timer(Duration(milliseconds: delay), () { + runnable.call(); + }); + } + + void removeCallbacks(Runnable runnable) { + var key = runnable.getKey(); + _mRunnableMap[key]?.cancel(); + _mRunnableMap.remove(key); + } + + void clearAll() { + _mRunnableMap.clear(); + } +} + +typedef Runnable = Function; + +extension RunnableExt on Runnable { + String getKey() { + var key = hashCode.toString(); + Log.info("Runnable Handler key:$key ","RunnableExt getKey"); + return key; + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/log.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/log.dart new file mode 100644 index 00000000..31465f4a --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/log.dart @@ -0,0 +1,87 @@ +import 'package:flutter/foundation.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/ext.dart'; +import 'package:stack_trace/stack_trace.dart'; + +class Log { + static Function? cacheLogInfo; + + static void info(dynamic message, String? tag) { + if (kDebugMode) { + var info = "[info] $tag ${nowHmsStrWithMark()}--> $message"; + WLog(info, mode: WLogMode.info); + cacheLogInfo?.call(info); + } + } + + static void debug(dynamic message, String? tag) { + if (kDebugMode) { + var info = "[debug] $tag ${nowHmsStrWithMark()}--> $message"; + WLog(info, mode: WLogMode.debug); + cacheLogInfo?.call(info); + } + } + + static void warn(dynamic message, String? tag) { + if (kDebugMode) { + var info = "[warning] $tag ${nowHmsStrWithMark()}--> $message"; + WLog(info, mode: WLogMode.warning); + cacheLogInfo?.call(info); + } + } + + static void error(dynamic message, String? tag) { + if (kDebugMode) { + var info = "[error] $tag ${nowHmsStrWithMark()}--> $message"; + WLog(info, mode: WLogMode.error); + cacheLogInfo?.call(info); + } + } +} + +enum WLogMode { + debug, // 💚 DEBUG + warning, // 💛 WARNING + info, // 💙 INFO + error, // ❤️ ERROR +} + +String WLog(dynamic msg, {WLogMode mode = WLogMode.debug}) { + if (kReleaseMode) { + // release模式不打印 + return ""; + } + var chain = Chain.current(); // Chain.forTrace(StackTrace.current); + // 将 core 和 flutter 包的堆栈合起来(即相关数据只剩其中一条) + chain = + chain.foldFrames((frame) => frame.isCore || frame.package == "flutter"); + // 取出所有信息帧 + final frames = chain.toTrace().frames; + // 找到当前函数的信息帧 + final idx = frames.indexWhere((element) => element.member == "WLog"); + if (idx == -1 || idx + 1 >= frames.length) { + return ""; + } + // 调用当前函数的函数信息帧 + final frame = frames[idx + 1]; + + var modeStr = ""; + switch (mode) { + case WLogMode.debug: + modeStr = "💚 DEBUG"; + break; + case WLogMode.warning: + modeStr = "💛 WARNING"; + break; + case WLogMode.info: + modeStr = "💙 INFO"; + break; + case WLogMode.error: + modeStr = "❤️ ERROR"; + break; + } + + final printStr = + "$modeStr ${frame.uri.toString().split("/").last}(${frame.line}) - $msg "; + debugPrint(printStr); + return printStr; +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/mb_observer.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/mb_observer.dart new file mode 100644 index 00000000..3fda877c --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/mb_observer.dart @@ -0,0 +1,9 @@ +class MBObserver { + final Function(bool success, Object? extraObj) _cb; + + MBObserver(this._cb); + + void update(bool success, Object? extraObj) { + _cb.call(success, extraObj); + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/mb_simple_timer.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/mb_simple_timer.dart new file mode 100644 index 00000000..5823f3f0 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/mb_simple_timer.dart @@ -0,0 +1,43 @@ +import 'dart:async'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/log.dart'; + +class MBSimpleTimer { + late final _tag = "MBSimpleTimer:$hashCode"; + + Timer? _timer; + int _interval = -1; + late Function _doAction; + + MBSimpleTimer(int interval, Function action) { + _interval = interval; + _doAction = action; + } + + void setInterval(int interval) { + _interval = interval; + } + + void _doCallback() { + try { + _doAction.call(); + } catch (e) { + Log.error(e, _tag); + } + } + + void start(bool immediately) { + stop(); + + _timer = Timer.periodic(Duration(milliseconds: _interval), (timer) { + _doCallback(); + }); + + if (immediately) { + _doCallback(); + } + } + + void stop() { + _timer?.cancel(); + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/tcp_socket_manager.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/tcp_socket_manager.dart new file mode 100644 index 00000000..3ffd2a0c --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/sdk/utils/tcp_socket_manager.dart @@ -0,0 +1,156 @@ +import 'dart:convert'; +import 'dart:core'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:mobile_im_sdk_flutter_tcp/sdk/core/keep_alive_daemon.dart'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/log.dart'; + +class TcpSocketManager { + final tag = "TcpSocketManager"; + + ///用于描述消息长度的固定长度 + static int tcpFrameFixedHeaderLength = 4; // 4 bytes + static int tcpFrameMaxBodyLength = 6 * 1024; // 6K bytes + + late String _host; + late int _port; + Socket? _mSocket; + bool _cancelOnError = false; + bool _isActive = false; + Function? _messageReceived; + Function? _onConnectionLost; + + void init(String host, int port, + {bool cancelOnError = false, + Function? messageReceived, + Function? onConnectionLost}) { + _host = host; + _port = port; + _cancelOnError = cancelOnError; + _messageReceived = messageReceived; + _onConnectionLost = onConnectionLost; + } + + void connectSocket({Function? onSuccess, Function? onError}) async { + if (_mSocket != null) return; + try { + await Socket.connect(_host, _port, + timeout: Duration( + milliseconds: KeepAliveDaemon.NETWORK_CONNECTION_TIME_OUT)) + .then((socket) { + _mSocket = socket; + _mSocket?.listen(_receivedMsgHandler, + onError: _errorHandler, + onDone: _doneHandler, + cancelOnError: _cancelOnError); + Log.info('---------连接成功------------$_mSocket', tag); + _isActive = true; + onSuccess?.call(); + }); + } catch (e) { + onError?.call(e); + _mSocket?.destroy(); + _mSocket = null; + _isActive = false; + Log.error("连接socket出现异常,e=${e.toString()}", tag); + } + } + + void close() { + _doneHandler(); + } + + bool isActive() { + return _isActive && _mSocket != null; + } + + void _errorHandler(error, StackTrace trace) { + Log.error("捕获socket异常信息:error=$error,trace=${trace.toString()}", tag); + close(); + } + + void _doneHandler() { + _isActive = false; + _mSocket?.destroy(); + _mSocket = null; + _onConnectionLost?.call(); + Log.warn("socket关闭处理", tag); + } + + void _receivedMsgHandler(Uint8List data) async { + _decodeHandle(data); + } + + // + + /// 缓存的网络数据,暂未处理(一般这里有数据,说明当前接收的数据不是一个完整的消息,需要等待其它数据的到来拼凑成一个完整的消息) */ + Uint8List _cacheData = Uint8List(0); + + /// 解码处理方法 + /// 处理服务器发过来的数据,注意,这里要处理粘包,这个data参数不一定是一个完整的包 + void _decodeHandle(Uint8List newData) async { + //拼凑当前最新未处理的网络数据 + _cacheData = Uint8List.fromList(_cacheData + newData); + + //缓存数据长度符合最小包长度才尝试解码 + while (_cacheData.length >= tcpFrameFixedHeaderLength) { + //读取消息长度 + var byteData = _cacheData.buffer.asByteData(); + var bodyLen = byteData.getInt32(0); + + //数据长度小于消息长度,说明不是完整的数据,暂不处理 + if (_cacheData.length < bodyLen + tcpFrameFixedHeaderLength) { + return; + } + + //读取 body 数据 + Uint8List pbBody; + if (bodyLen > 0) { + pbBody = _cacheData.sublist( + tcpFrameFixedHeaderLength, tcpFrameFixedHeaderLength + bodyLen); + } else { + pbBody = Uint8List.fromList(List.empty()); + } + + //整理缓存数据 + int totalLen = tcpFrameFixedHeaderLength + pbBody.length; + _cacheData = _cacheData.sublist(totalLen, _cacheData.length); + + var msgStr = utf8.decode(pbBody); + Log.info("[msg] 接收到的消息:$msgStr", tag); + //处理消息 + _messageReceived?.call(msgStr); + } + } + + // + + ///发送数据 + bool sendMsg(String message) { + var result = false; + if (!isActive()) return result; + + //序列化pb对象 + Uint8List pbBody = Uint8List.fromList(utf8.encode(message)); + int dataLength = pbBody.length; + + //包头部分 + var header = ByteData(tcpFrameFixedHeaderLength); + header.setInt32(0, dataLength); + + var msgInfoIntList = pbBody.buffer.asUint8List(); + //包头+message组合成一个完整的数据包 + var msg = header.buffer.asUint8List() + msgInfoIntList; + + //给服务器发消息 + try { + _mSocket?.add(msg); + Log.info("[msg] 给服务端发送消息,$message", tag); + result = true; + } catch (e) { + Log.error("send捕获异常:message=$message,e=${e.toString()}", tag); + } + return result; + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/c/p_keep_alive.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/c/p_keep_alive.dart new file mode 100644 index 00000000..6b1615b5 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/c/p_keep_alive.dart @@ -0,0 +1,10 @@ +class PKeepAlive { + PKeepAlive(); + + //命名式构造方法,也可以是用工厂构造方法 + PKeepAlive.fromJson(Map json); + + //如果想写成协议,归档(json和对象互转时,为了使用方便)时,可以继承协议,那么可以用普通方法,而不是构造方法 + Map toJson() => {}; + +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/c/p_login_info.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/c/p_login_info.dart new file mode 100644 index 00000000..34c1bf46 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/c/p_login_info.dart @@ -0,0 +1,64 @@ +import 'dart:core'; + +class PLoginInfo { + String? _loginUserId; + String? _loginToken; + String? _extra; + int _firstLoginTime = 0; + + PLoginInfo(String this._loginUserId, {String? loginToken, String? extra}) { + _loginToken = loginToken; + _extra = extra; + } + + //命名式构造方法,也可以是用工厂构造方法 + PLoginInfo.fromJson(Map json) + : _loginUserId = json['loginUserId'], + _loginToken = json['loginToken'], + _extra = json['extra'], + _firstLoginTime = json['firstLoginTime']; + + //如果想写成协议,归档(json和对象互转时,为了使用方便)时,可以继承协议,那么可以用普通方法,而不是构造方法 + Map toJson() => { + 'loginUserId': _loginUserId, + 'loginToken': _loginToken, + 'extra': _extra, + 'firstLoginTime': _firstLoginTime + }; + + getLoginUserId() { + return _loginUserId; + } + + void setLoginUserId(String loginUserId) { + _loginUserId = loginUserId; + } + + getLoginToken() { + return _loginToken; + } + + void setLoginToken(String loginToken) { + _loginToken = loginToken; + } + + getExtra() { + return _extra; + } + + void setExtra(String extra) { + _extra = extra; + } + + getFirstLoginTime() { + return _firstLoginTime; + } + + void setFirstLoginTime(int firstLoginTime) { + _firstLoginTime = firstLoginTime; + } + + bool isFirstLogin() { + return _firstLoginTime <= 0; + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/error_code.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/error_code.dart new file mode 100644 index 00000000..9723444d --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/error_code.dart @@ -0,0 +1,21 @@ +// ignore_for_file: constant_identifier_names + +class ErrorCode { + static const int COMMON_CODE_OK = 0; + static const int COMMON_NO_LOGIN = 1; + static const int COMMON_UNKNOWN_ERROR = 2; + static const int COMMON_DATA_SEND_FAILED = 3; + static const int COMMON_INVALID_Protocol = 4; +} + +class ErrorCodeForC extends ErrorCode { + static const int BROKEN_CONNECT_TO_SERVER = 201; + static const int BAD_CONNECT_TO_SERVER = 202; + static const int CLIENT_SDK_NO_INITIALED = 203; + static const int LOCAL_NETWORK_NOT_WORKING = 204; + static const int TO_SERVER_NET_INFO_NOT_SETUP = 205; +} + +class ErrorCodeForS extends ErrorCode { + static const int RESPONSE_FOR_UN_LOGIN = 301; +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/protocol.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/protocol.dart new file mode 100644 index 00000000..158f2ea2 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/protocol.dart @@ -0,0 +1,144 @@ +import 'dart:math'; + +class Protocol { + bool _bridge = false; + int _type = 0; + String? _dataContent; + String _from = "-1"; + String _to = "-1"; + String? _fp; + bool _QoS = false; + int? _retryCount; + int _sm = -1; + + ///意义:应用层专用字段——用于应用层存放聊天、推送等场景下的消息类型。 + ///注意:此值为-1时表示未定义。 + ///本字段为保留字段,不参与框架的核心算法,专留用应用 层自行定义和使用。 默认:-1 + int _typeu = -1; + + Protocol(this._type, this._dataContent, this._from, this._to, + {bool? QoS, String? fingerPrint, int typeu = -1}) { + _QoS = QoS ?? false; + _typeu = typeu; + _sm = -1; + + if (_QoS && fingerPrint == null) { + _fp = genFingerPrint(); + } else { + _fp = fingerPrint; + } + } + + //命名式构造方法,也可以是用工厂构造方法 + Protocol.fromJson(Map json) + : _bridge = json['bridge'], + _type = json['type'], + _dataContent = json['dataContent'], + _from = json['from'], + _to = json['to'], + _fp = json['fp'], + _QoS = json['QoS'], + _retryCount = json['retryCount'], + _sm = json['sm'], + _typeu = json['typeu']; + + //如果想写成协议,归档(json和对象互转时,为了使用方便)时,可以继承协议,那么可以用普通方法,而不是构造方法 + Map toJson() => { + 'bridge': _bridge, + 'type': _type, + 'dataContent': _dataContent, + 'from': _from, + 'to': _to, + 'fp': _fp, + 'QoS': _QoS, + 'retryCount': _retryCount, + 'sm': _sm, + 'typeu': _typeu, + }; + + getType() { + return _type; + } + + void setType(int type) { + _type = type; + } + + getDataContent() { + return _dataContent; + } + + void setDataContent(String dataContent) { + _dataContent = dataContent; + } + + getFrom() { + return _from; + } + + void setFrom(String from) { + _from = from; + } + + getTo() { + return _to; + } + + void setTo(String to) { + _to = to; + } + + getFp() { + return _fp; + } + + getRetryCount() { + return _retryCount ?? 0; + } + + void increaseRetryCount() { + int count = _retryCount ?? 0; + ++count; + _retryCount = count; + } + + isQoS() { + return _QoS; + } + + void setQoS(bool qoS) { + _QoS = qoS; + } + + isBridge() { + return _bridge; + } + + void setBridge(bool bridge) { + _bridge = bridge; + } + + getTypeu() { + return _typeu; + } + + void setTypeu(int typeu) { + _typeu = typeu; + } + + getSm() { + return _sm; + } + + void setSm(int sm) { + _sm = sm; + } + + static int genServerTimestamp() { + return DateTime.now().millisecondsSinceEpoch; + } + + static String genFingerPrint() { + return (genServerTimestamp()).toString() + Random().nextDouble().toString(); + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/protocol_factory.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/protocol_factory.dart new file mode 100644 index 00000000..48715b4b --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/protocol_factory.dart @@ -0,0 +1,120 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:mobile_im_sdk_flutter_tcp/sdk/utils/Ext.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/protocol.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/s/p_error_response.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/s/p_keep_alive_response.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/s/p_kickout_info.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/s/p_login_info_response.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/c/p_keep_alive.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/c/p_login_info.dart'; +import 'package:mobile_im_sdk_flutter_tcp/server/protocol/protocol_i_type.dart'; + +class ProtocolFactory { + static const serverUserId = "0"; + + static String create(Object c) { + if (c.runtimeType == Map) { + return (c as Map).toJsonStr(); + } + return json.encode(c); + } + + static T parseByteArray(Uint8List fullProtocolJSONBytes, int len, + T Function(Uint8List byteArray, int len) doParse) { + return doParse(fullProtocolJSONBytes, len); + } + + static T parseString(String dataContentOfProtocol, + T Function(String dataContentOfProtocol) doParse) { + return doParse(dataContentOfProtocol); + } + + static Protocol parseFromString(String dataContentOfProtocol) { + return Protocol.fromJson(jsonDecode(dataContentOfProtocol)); + } + + static Protocol createPKeepAliveResponse(String to_user_id) { + return Protocol(ProtocolTypeS.FROM_SERVER_TYPE_OF_RESPONSE$KEEP$ALIVE, + create(PKeepAliveResponse()), serverUserId, to_user_id); + } + + static PKeepAliveResponse parsePKeepAliveResponse( + String dataContentOfProtocol) { + return PKeepAliveResponse(); + } + + static Protocol createPKeepAlive(String from_user_id) { + return Protocol(ProtocolTypeC.FROM_CLIENT_TYPE_OF_KEEP$ALIVE, + create(PKeepAlive()), from_user_id, serverUserId); + } + + static PKeepAlive parsePKeepAlive(String dataContentOfProtocol) { + return PKeepAlive(); + } + + static Protocol createPErrorResponse( + int errorCode, String errorMsg, String user_id) { + return Protocol(ProtocolTypeS.FROM_SERVER_TYPE_OF_RESPONSE$FOR$ERROR, + create(PErrorResponse(errorCode, errorMsg)), serverUserId, user_id); + } + + static PErrorResponse parsePErrorResponse(String dataContentOfProtocol) { + return PErrorResponse.fromJson(json.decode(dataContentOfProtocol)); + } + + static Protocol createPLogoutInfo(PLoginInfo pLoginInfo) { + return Protocol( + ProtocolTypeC.FROM_CLIENT_TYPE_OF_LOGOUT, null, pLoginInfo.getLoginUserId(), serverUserId); + } + + static Protocol createPLoginInfo(PLoginInfo loginInfo) { + return Protocol( + ProtocolTypeC.FROM_CLIENT_TYPE_OF_LOGIN, + loginInfo.toJson().toJsonStr(), + loginInfo.getLoginUserId(), + serverUserId); + } + + static PLoginInfo parsePLoginInfo(String dataContentOfProtocol) { + return PLoginInfo.fromJson(json.decode(dataContentOfProtocol)); + } + + static Protocol createPLoginInfoResponse( + int code, int firstLoginTime, String user_id) { + return Protocol(ProtocolTypeS.FROM_SERVER_TYPE_OF_RESPONSE$LOGIN, + create(PLoginInfoResponse(code, firstLoginTime)), serverUserId, user_id, + QoS: true, fingerPrint: Protocol.genFingerPrint()); + } + + static PLoginInfoResponse parsePLoginInfoResponse( + String dataContentOfProtocol) { + return PLoginInfoResponse.fromJson(json.decode(dataContentOfProtocol)); + } + + static Protocol createCommonData( + String dataContent, String from_user_id, String to_user_id, bool QoS, + {String? fingerPrint, int typeu = -1}) { + return Protocol(ProtocolTypeC.FROM_CLIENT_TYPE_OF_COMMON$DATA, dataContent, + from_user_id, to_user_id, + QoS: QoS, fingerPrint: fingerPrint, typeu: typeu); + } + + static Protocol createRecivedBack( + String from_user_id, String to_user_id, String recievedMessageFingerPrint, + {bool bridge = false}) { + Protocol p = Protocol(ProtocolTypeC.FROM_CLIENT_TYPE_OF_RECEIVED, + recievedMessageFingerPrint, from_user_id, to_user_id); + p.setBridge(bridge); + return p; + } + + static Protocol createPKickout(String to_user_id, int code, String reason) { + return Protocol(ProtocolTypeS.FROM_SERVER_TYPE_OF_KICKOUT, + create(PKickoutInfo(code, reason: reason)), serverUserId, to_user_id); + } + + static PKickoutInfo parsePKickoutInfo(String dataContentOfProtocol) { + return PKickoutInfo.fromJson(json.decode(dataContentOfProtocol)); + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/protocol_i_type.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/protocol_i_type.dart new file mode 100644 index 00000000..b30fbfd2 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/protocol_i_type.dart @@ -0,0 +1,20 @@ +// ignore_for_file: constant_identifier_names + +abstract class ProtocolType {} + +abstract class ProtocolTypeC extends ProtocolType { + static const int FROM_CLIENT_TYPE_OF_LOGIN = 0; + static const int FROM_CLIENT_TYPE_OF_KEEP$ALIVE = 1; + static const int FROM_CLIENT_TYPE_OF_COMMON$DATA = 2; + static const int FROM_CLIENT_TYPE_OF_LOGOUT = 3; + static const int FROM_CLIENT_TYPE_OF_RECEIVED = 4; + static const int FROM_CLIENT_TYPE_OF_ECHO = 5; +} + +abstract class ProtocolTypeS extends ProtocolType{ + static const int FROM_SERVER_TYPE_OF_RESPONSE$LOGIN = 50; + static const int FROM_SERVER_TYPE_OF_RESPONSE$KEEP$ALIVE = 51; + static const int FROM_SERVER_TYPE_OF_RESPONSE$FOR$ERROR = 52; + static const int FROM_SERVER_TYPE_OF_RESPONSE$ECHO = 53; + static const int FROM_SERVER_TYPE_OF_KICKOUT = 54; +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/s/p_error_response.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/s/p_error_response.dart new file mode 100644 index 00000000..d1be2aaf --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/s/p_error_response.dart @@ -0,0 +1,35 @@ +import 'dart:core'; + +class PErrorResponse { + int _errorCode = -1; + String? _errorMsg; + + PErrorResponse(this._errorCode,this._errorMsg); + + //命名式构造方法,也可以是用工厂构造方法 + PErrorResponse.fromJson(Map json) + : _errorCode = json['errorCode'], + _errorMsg = json['errorMsg']; + + //如果想写成协议,归档(json和对象互转时,为了使用方便)时,可以继承协议,那么可以用普通方法,而不是构造方法 + Map toJson() => { + 'errorCode': _errorCode, + 'errorMsg': _errorMsg + }; + + getErrorCode() { + return _errorCode; + } + + void setErrorCode(int errorCode) { + _errorCode = errorCode; + } + + getErrorMsg() { + return _errorMsg; + } + + void setErrorMsg(String errorMsg) { + _errorMsg = errorMsg; + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/s/p_keep_alive_response.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/s/p_keep_alive_response.dart new file mode 100644 index 00000000..33344645 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/s/p_keep_alive_response.dart @@ -0,0 +1,10 @@ +class PKeepAliveResponse { + PKeepAliveResponse(); + + //命名式构造方法,也可以是用工厂构造方法 + PKeepAliveResponse.fromJson(Map json); + + //如果想写成协议,归档(json和对象互转时,为了使用方便)时,可以继承协议,那么可以用普通方法,而不是构造方法 + Map toJson() => {}; + +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/s/p_kickout_info.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/s/p_kickout_info.dart new file mode 100644 index 00000000..d2764e66 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/s/p_kickout_info.dart @@ -0,0 +1,37 @@ +// ignore_for_file: constant_identifier_names + +class PKickoutInfo { + static const KICK_OUT_FOR_DUPLICATE_LOGIN = 1; + static const KICK_OUT_FOR_ADMIN = 2; + int _code = -1; + String _reason = ""; + + PKickoutInfo(int code, {String reason = ""}) { + _code = code; + _reason = reason; + } + + //命名式构造方法,也可以是用工厂构造方法 + PKickoutInfo.fromJson(Map json) + : _code = json['code'], + _reason = json['reason']; + + //如果想写成协议,归档(json和对象互转时,为了使用方便)时,可以继承协议,那么可以用普通方法,而不是构造方法 + Map toJson() => {'code': _code, 'reason': _reason}; + + getCode() { + return _code; + } + + void setCode(int code) { + _code = code; + } + + getReason() { + return _reason; + } + + void setReason(String reason) { + _reason = reason; + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/s/p_login_info_response.dart b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/s/p_login_info_response.dart new file mode 100644 index 00000000..b409ce02 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/lib/server/protocol/s/p_login_info_response.dart @@ -0,0 +1,34 @@ +class PLoginInfoResponse { + int _code = 0; + int _firstLoginTime = 0; + + PLoginInfoResponse(int code, int firstLoginTime) { + _code = code; + _firstLoginTime = firstLoginTime; + } + + //命名式构造方法,也可以是用工厂构造方法 + PLoginInfoResponse.fromJson(Map json) + : _code = json['code'], + _firstLoginTime = json['firstLoginTime']; + + //如果想写成协议,归档(json和对象互转时,为了使用方便)时,可以继承协议,那么可以用普通方法,而不是构造方法 + Map toJson() => + {'code': _code, 'firstLoginTime': _firstLoginTime}; + + int getCode() { + return _code; + } + + void setCode(int code) { + _code = code; + } + + getFirstLoginTime() { + return _firstLoginTime; + } + + void setFirstLoginTime(int firstLoginTime) { + _firstLoginTime = firstLoginTime; + } +} diff --git a/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/pubspec.yaml b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/pubspec.yaml new file mode 100644 index 00000000..11962838 --- /dev/null +++ b/sdk_src/TCP_Client/MobileIMSDK4f_tcp_Open/pubspec.yaml @@ -0,0 +1,88 @@ +name: mobile_im_sdk_flutter_tcp +description: MobileImSdkFlutterTcp. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=3.0.5 <4.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + duffer: ^1.2.5 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages