Giới thiệu

“ezyfox-server-flutter-client” là một thư viện được phát triển để làm việc với EzyFox Server trong các ứng dụng Flutter. EzyFox Server là một máy chủ thời gian thực được sử dụng cho các ứng dụng như trò truyện trực tuyến (ở các bài viết trước đó mình đã hướng dẫn các bạn xây dựng ứng dụng trò truyện trực tuyến freechat), trò chơi trực tuyến và các ứng dụng thời gian thực khác. Thư viện này cung cấp một bộ API giúp các ứng dụng Flutter kết nối với EzyFox Server một cách hiệu quả và dễ dàng.

Các tính năng chính của “ezyfox-server-flutter-client”

  • Kết nối thời gian thực: Thư viện này cho phép các ứng dụng Flutter thiết lập kết nối thời gian thực với EzyFox Server thông qua WebSocket (giao thức truyền thông dựa trên TCP cho phép thiết lập kết nối hai chiều giữa máy khách (client) và máy chủ (server) trên nền tảng web.). Điều này giúp gửi và nhận dữ liệu một cách nhanh chóng và liên tục, phù hợp cho các ứng dụng yêu cầu độ trễ thấp như trò chơi trực tuyến hay ứng dụng chat.
  • Bảo mật: Hỗ trợ các kết nối bảo mật thông qua SSL (Secure Sockets Layer), một giao thức bảo mật giúp mã hóa dữ liệu truyền tải giữa máy khách (client) và máy chủ (server), bảo vệ thông tin khỏi các cuộc tấn công mạng.
  • Xử lý sự kiện và dữ liệu: Cung cấp các API để đăng ký và xử lý các sự kiện và dữ liệu nhận được từ máy chủ. Điều này giúp dễ dàng quản lý các logic phức tạp và phản hồi lại các sự kiện như người dùng đăng nhập, nhận tin nhắn mới, hoặc các sự kiện trò chơi.
  • Quản lý phiên (session management): Hỗ trợ quản lý các phiên làm việc của người dùng, bao gồm việc tạo mới, duy trì, và ngắt kết nối phiên. Điều này đặc biệt quan trọng trong các ứng dụng yêu cầu đăng nhập và bảo mật phiên người dùng.
  • Hỗ trợ mã hóa và giải mã: Thư viện hỗ trợ mã hóa dữ liệu trước khi gửi đến máy chủ và giải mã dữ liệu nhận được, giúp đảm bảo an toàn cho các thông tin nhạy cảm.
  • Khả năng mở rộng: Được thiết kế để dễ dàng tích hợp và mở rộng cho các tính năng tùy chỉnh dựa trên yêu cầu của ứng dụng.

Kiến trúc của Ezyfox Server

kientrucEzyfox

EzyFox Server là một socket container (môi trường biệt lập nơi ứng dụng chạy và có thể kết nối với các dịch vụ bên ngoài thông qua các sockets), chứa các zone (các zone chứa các plugin và app). Hiểu đơn giản là bạn có thể chia nhỏ thành các nghiệp vụ khác nhau trong ứng dụng để có thể phân quyền và dễ dàng quản lý. Ví dụ ở đây plugin A dùng để xác nhận yêu cầu đăng nhập, plugin B dùng để lập lịch app A cung cấp dịch vụ chat,…

Cấu hình file của thư viện ezyfox-server-flutter-client

ezy_client.dart

class EzyClient {
  late bool enableSSL;
  late bool enableDebug;
  late EzyConfig config;
  late String name;
  late EzyZone? zone;
  late EzyUser? me;
  late EzySetup setup;
  late EzyHandlerManager handlerManager;
  late Uint8List? privateKey;
  late int sessionId;
  late String? sessionToken;
  late Uint8List? sessionKey;

  EzyClient(this.config) {
    EzyProxy.run("init", config.toMap());
    name = config.getClientName();
    enableSSL = config.enableSSL;
    enableDebug = config.enableDebug;
    handlerManager = EzyHandlerManager.create(this);
    setup = EzySetup(handlerManager);
  }

  void connect(String host, int port) {
    privateKey = null;
    sessionKey = null;
    var params = Map();
    params["clientName"] = name;
    params["host"] = host;
    params["port"] = port;
    EzyProxy.run("connect", params);
  }

  Future reconnect() {
    privateKey = null;
    sessionKey = null;
    var params = Map();
    params["clientName"] = name;
    return EzyProxy.run("reconnect", params);
  }

  void disconnect([int reason = EzyDisconnectReason.CLOSE]) {
    var params = Map();
    params["clientName"] = name;
    params["reason"] = reason;
    EzyProxy.run("disconnect", params);
  }

  void close() {
    disconnect();
  }

  void send(String cmd, dynamic data, [bool encrypted = false]) {
    var shouldEncrypted = encrypted;
    if (encrypted && sessionKey == null) {
      if (enableDebug) {
        shouldEncrypted = false;
      } else {
        EzyLogger.error(
          "can not send command: $cmd, you must enable SSL "
          "or enable debug mode by configuration when you create the client",
        );
        return;
      }
    }
    var params = Map();
    params["clientName"] = name;
    var requestParams = Map();
    requestParams["command"] = cmd;
    requestParams["data"] = data;
    requestParams["encrypted"] = shouldEncrypted;
    params["request"] = requestParams;
    EzyProxy.run("send", params);
  }

  void startPingSchedule() {
    var params = Map();
    params["clientName"] = name;
    EzyProxy.run("startPingSchedule", params);
  }

  void setStatus(String status) {
    var params = Map();
    params["clientName"] = name;
    params["status"] = status;
    EzyProxy.run("setStatus", params);
  }

  void setSessionKey(Uint8List sessionKey) {
    this.sessionKey = sessionKey;
    var params = Map();
    params["clientName"] = name;
    params["sessionKey"] = sessionKey;
    EzyProxy.run("setSessionKey", params);
  }

  EzyApp? getApp() {
    if (zone != null) {
      var appManager = zone!.appManager;
      var app = appManager.getApp();
      return app;
    }
    return null;
  }

  EzyApp? getAppById(int appId) {
    if (zone != null) {
      var appManager = zone!.appManager;
      var app = appManager.getAppById(appId);
      return app;
    }
    return null;
  }

  void handleEvent(String eventType, Map data) {
    var eventHandlers = handlerManager.eventHandlers;
    eventHandlers.handle(eventType, data);
  }

  void handleData(String command, List data) {
    var dataHandlers = handlerManager.dataHandlers;
    dataHandlers.handle(command, data);
  }
}

Giải thích:

File này định nghĩa lớp Ezyclient, là lớp chính để quản lý kết nối và giao tiếp giữa client và server.
Các thuộc tính của lớp Ezyclient :

  • enableSSL: Kiểu boolean cho biết liệu SSL có được bật hay không.
  • enableDebug : Kiểu boolean cho biết chế độ gỡ lỗi có được bật không.
  • config: Đối tượng cấu hình của client.
  • name : tên của client.
  • zone : Đối tượng EzyZone, đại diện cho khu vực kết nối.
  • me : Đối tượng EzyUser, đại diện cho người dùng hiện tại.
  • setu : Đối tượng EzySetup, dùng để thiết lập các cấu hình cần thiết.
  • handlerManager : Đối tượng EzyHandlerManager, quản lý các trình xử lý sự kiện và dữ liệu.
  • privateKey : Khóa riêng(private key) của client.
  • sessionId : ID của phiên làm việc (session).
  • sessionToken : Token của phiên làm việc.
  • sessionKey : Khóa phiên (session key) để mã hóa dữ liệu.

Các phương thức của lớpEzyclient

  • EzyClient(this.config): Constructor để khởi tạo đối tượng EzyClient với cấu hình được cung cấp.
  • void connect(String host, int port): Kết nối đến server với địa chỉ host và cổng port được chỉ định.
  • Future reconnect() : Kết nối lại với server.
  • void disconnect([int reason = EzyDisconnectReason.CLOSE]) : Ngắt kết nối với server, với lý do ngắt kết nối có thể được chỉ định.
  • void close() : Đóng kết nối, tương đương với disconnect().
  • void send(String cmd, dynamic data, [bool encrypted = false]) : Gửi lệnh đến server với dữ liệu có thể được mã hóa nếu cần.
  • void startPingSchedule() : Bắt đầu lịch trình gửi tín hiệu kiểm tra kết nối (ping).
  • void setStatus(String status) : Đặt trạng thái của client.
  • void setSessionKey(Uint8List sessionKey) : Đặt khóa phiên để mã hóa dữ liệu.
  • EzyApp? getApp() : Lấy đối tượng EzyApp từ khu vực hiện tại.
  • EzyApp? getAppById(int appId) : Lấy đối tượng EzyApp theo ID.
  • void handleEvent(String eventType, Map data) : Xử lý sự kiện với loại sự kiện và dữ liệu được cung cấp.
  • void handleData(String command, List data): Xử lý dữ liệu với lệnh và dữ liệu được cung cấp.

ezy_clients.dart

class EzyClients {
  late String defaultClientName;
  late Map<String, EzyClient> clients;
  static final EzyClients _INSTANCE = EzyClients._();

  EzyClients._() {
    defaultClientName = "";
    clients = <String, EzyClient>{};
  }

  static EzyClients getInstance() {
    return _INSTANCE;
  }

  EzyClient newClient(EzyConfig config) {
    var client = EzyClient(config);
    addClient(client);
    if (defaultClientName == "") {
      defaultClientName = client.name;
    }
    return client;
  }

  EzyClient newDefaultClient(EzyConfig config) {
    var client = newClient(config);
    defaultClientName = client.name;
    return client;
  }

  void addClient(EzyClient client) {
    clients[client.name] = client;
  }

  EzyClient getClient(String clientName) {
    var client = clients[clientName]!;
    return client;
  }

  EzyClient getDefaultClient() {
    return clients[defaultClientName]!;
  }
}

Giải thích:

File này định nghĩa lớp EzyClients, một lớp singleton quản lý các đối tượng EzyClient. Là một thiết kế phổ biến để đảm bảo chỉ có một thể hiện của lớp EzyClients trong toàn bộ ứng dụng.
Các thuộc tính của lớp EzyClients:

  • defaultClientName: Tên của client mặc định. Đây là client sẽ được sử dụng nếu không có client nào khác được chỉ định.
  • clients: Một bản đồ (Map) lưu trữ các đối tượng EzyClient với tên của chúng là khóa.

Các phương thức của lớp Ezyclients:

  • EzyClients._(): Constructor riêng (private) để khởi tạo đối tượng EzyClients duy nhất. Khởi tạo defaultClientName là một chuỗi rỗng và clients là một bản đồ rỗng.
  • static EzyClients getInstance(): Phương thức tĩnh để lấy thể hiện duy nhất của lớp EzyClients. Đây là cách để đảm bảo rằng chỉ có một đối tượng EzyClients tồn tại.
  • EzyClient newClient(EzyConfig config): Tạo một đối tượng EzyClient mới với cấu hình được cung cấp và thêm vào bản đồ clients. Nếu defaultClientName là chuỗi rỗng, thì client mới tạo sẽ trở thành client mặc định.
    *EzyClient newDefaultClient(EzyConfig config)
    : Tạo một đối tượng EzyClient mới và đặt nó làm client mặc định. Phương thức này cũng gọi newClient() để thêm client vào bản đồ.
  • void addClient(EzyClient client): Thêm một đối tượng EzyClient vào bản đồ clients với tên của nó là khóa.
  • EzyClient getClient(String clientName): Lấy đối tượng EzyClient từ bản đồ clients dựa trên tên của nó. Nếu không tìm thấy client với tên đó, mã sẽ ném ra lỗi vì việc truy cập với !.
  • EzyClient getDefaultClient(): Lấy client mặc định từ bản đồ clients dựa trên defaultClientName. Nếu không có client nào được thiết lập mặc định, mã sẽ ném ra lỗi vì việc truy cập với !.

ezy_codec.dart

class EzyKeyPairProxy {
  late String publicKey;
  late Uint8List privateKey;

  EzyKeyPairProxy(this.publicKey, this.privateKey);
}

class EzyRSAProxy {
  static final EzyRSAProxy _INSTANCE = EzyRSAProxy._();

  EzyRSAProxy._();

  static EzyRSAProxy getInstance() {
    return _INSTANCE;
  }

  void _onKeyPairGenerated(Map result, Function(EzyKeyPairProxy) callback) {
    callback(EzyKeyPairProxy(result["publicKey"], result["privateKey"]));
  }

  void generateKeyPair(Function(EzyKeyPairProxy) callback) {
    EzyProxy.run("generateKeyPair", {})
        .then((result) => {_onKeyPairGenerated(result, callback)});
  }

  void decrypt(
      Uint8List message, Uint8List privateKey, Function(Uint8List) callback) {
    Map<String, Uint8List> params = {
      "message": message,
      "privateKey": privateKey
    };
    EzyProxy.run("rsaDecrypt", params).then((result) => {callback(result)});
  }
}

Giải thích:
File định nghĩa lớp EzyRSAProxy, một lớp singleton chịu trách nghiệm về các hoạt động mã hóa và giải mã RSA.

Các thuộc tính và Contructor

  • EzyRSAProxy._(): Constructor riêng (private) để khởi tạo đối tượng EzyRSAProxy duy nhất. Constructor này không thực hiện bất kỳ hành động nào, chỉ khởi tạo đối tượng.
  • static final EzyRSAProxy _INSTANCE = EzyRSAProxy._();: Đối tượng duy nhất của lớp EzyRSAProxy, được khởi tạo khi lớp lần đầu tiên được sử dụng. Đây là một phần của mẫu Singleton để đảm bảo rằng chỉ có một thể hiện của lớp này.

Các phương thức

  • static EzyRSAProxy getInstance(): Phương thức tĩnh để lấy thể hiện duy nhất của lớp EzyRSAProxy. Phương thức này cung cấp quyền truy cập toàn cục đến đối tượng singleton.
  • void _onKeyPairGenerated(Map result, Function(EzyKeyPairProxy) callback): Phương thức riêng (private) để xử lý kết quả khi một cặp khóa RSA mới được tạo ra. Nó tạo một đối tượng EzyKeyPairProxy từ kết quả và gọi callback với đối tượng này.
  • void generateKeyPair(Function(EzyKeyPairProxy) callback): Phương thức để yêu cầu tạo một cặp khóa RSA mới. Nó sử dụng EzyProxy.run để gửi yêu cầu và sau khi nhận được kết quả, gọi _onKeyPairGenerated để xử lý kết quả và gọi callback.
  • void decrypt(Uint8List message, Uint8List privateKey, Function(Uint8List) callback): Phương thức để giải mã một thông điệp RSA. Nó gửi yêu cầu giải mã đến EzyProxy và gọi callback với kết quả giải mã.

ezy_config.dart

class EzyConfig {
  late String zoneName = "";
  late String clientName;
  late bool enableSSL = false;
  late bool enableDebug = false;
  late EzyPingConfig ping = EzyPingConfig();
  late EzyReconnectConfig reconnect = EzyReconnectConfig();

  String getClientName() {
    if (clientName == null) return zoneName;
    return clientName;
  }

  Map toMap() {
    Map map = {};
    map["clientName"] = getClientName();
    map["zoneName"] = zoneName;
    map["enableSSL"] = enableSSL;
    map["enableDebug"] = enableDebug;
    map["ping"] = ping.toMap();
    map["reconnect"] = reconnect.toMap();
    return map;
  }
}

class EzyPingConfig {
  late int pingPeriod = 3000;
  late int maxLostPingCount = 5;

  Map toMap() {
    Map map = {};
    map["pingPeriod"] = pingPeriod;
    map["maxLostPingCount"] = maxLostPingCount;
    return map;
  }
}

class EzyReconnectConfig {
  late bool enable = true;
  late int maxReconnectCount = 5;
  late int reconnectPeriod = 3000;

  Map toMap() {
    Map map = {};
    map["enable"] = enable;
    map["maxReconnectCount"] = maxReconnectCount;
    map["reconnectPeriod"] = reconnectPeriod;
    return map;
  }
}

Giải thích:
File định nghĩa lớp EzyConfig cùng với hai lớp cầu hình liên quan là EzyPingConfigEzyReconnectConfig. Đây là các lớp cấu hình dùng để thiết lập các thông số cho một ứng dụng hoặc dịch vụ.
Lớp EzyConfig:

  • late String zoneName = "";: Tên khu vực hoặc zone. Được khởi tạo với chuỗi rỗng.
  • late String clientName;: Tên của client. Tham số này không được khởi tạo ngay lập tức, và cần được gán giá trị trước khi sử dụng.
  • late bool enableSSL = false;: Cờ để bật hoặc tắt SSL. Mặc định là false.
  • late bool enableDebug = false;: Cờ để bật hoặc tắt chế độ debug. Mặc định là false.
  • late EzyPingConfig ping = EzyPingConfig();: Cấu hình ping. Được khởi tạo với một đối tượng EzyPingConfig mới.
  • late EzyReconnectConfig reconnect = EzyReconnectConfig();: Cấu hình reconnect. Được khởi tạo với một đối tượng EzyReconnectConfig mới.
  • String getClientName(): Phương thức để lấy tên client. Nếu clientName là null, nó trả về zoneName. (Ghi chú: Trong Dart, late có nghĩa là giá trị sẽ được gán trước khi được sử dụng, vì vậy việc so sánh với null không cần thiết.)
  • Map toMap(): Phương thức để chuyển đổi cấu hình thành một đối tượng Map. Điều này thường hữu ích cho việc truyền cấu hình qua mạng hoặc lưu trữ cấu hình.

Lớp EzyPingConfig:

  • late int pingPeriod = 3000;: Thời gian giữa các lần ping (trong mili giây). Mặc định là 3000 ms.
  • late int maxLostPingCount = 5;: Số lần ping tối đa mà có thể bị mất trước khi coi kết nối là không ổn định. Mặc định là 5.
  • Map toMap(): Phương thức để chuyển đổi cấu hình ping thành một đối tượng Map.

Lớp EzyReconnectConfig:

  • late bool enable = true;: Cờ để bật hoặc tắt tính năng tự động reconnect. Mặc định là true.
  • late int maxReconnectCount = 5;: Số lần tối đa để thử reconnect. Mặc định là 5.
  • late int reconnectPeriod = 3000;: Thời gian giữa các lần thử reconnect (trong mili giây). Mặc định là 3000 ms.
  • Map toMap(): Phương thức để chuyển đổi cấu hình reconnect thành một đối tượng Map.

ezy_constants.dart

class EzyCommand {
  EzyCommand._();

  static const ERROR = "ERROR";
  static const HANDSHAKE = "HANDSHAKE";
  static const PING = "PING";
  static const PONG = "PONG";
  static const LOGIN = "LOGIN";
  static const LOGIN_ERROR = "LOGIN_ERROR";
  static const LOGOUT = "LOGOUT";
  static const APP_ACCESS = "APP_ACCESS";
  static const APP_REQUEST = "APP_REQUEST";
  static const APP_EXIT = "APP_EXIT";
  static const APP_ACCESS_ERROR = "APP_ACCESS_ERROR";
  static const APP_REQUEST_ERROR = "APP_REQUEST_ERROR";
  static const PLUGIN_INFO = "PLUGIN_INFO";
  static const PLUGIN_REQUEST = "PLUGIN_REQUEST";
}

class EzyEventType {
  EzyEventType._();

  static const CONNECTION_SUCCESS = "CONNECTION_SUCCESS";
  static const CONNECTION_FAILURE = "CONNECTION_FAILURE";
  static const DISCONNECTION = "DISCONNECTION";
  static const LOST_PING = "LOST_PING";
  static const TRY_CONNECT = "TRY_CONNECT";
}

class EzyConnectionStatus {
  EzyConnectionStatus._();

  static const NULL = "NULL";
  static const CONNECTING = "CONNECTING";
  static const CONNECTED = "CONNECTED";
  static const DISCONNECTED = "DISCONNECTED";
  static const FAILURE = "FAILURE";
  static const RECONNECTING = "RECONNECTING";
}

class EzyDisconnectReason {
  EzyDisconnectReason._();

  static const CLOSE = -1;
  static const UNKNOWN = 0;
  static const IDLE = 1;
  static const NOT_LOGGED_IN = 2;
  static const ANOTHER_SESSION_LOGIN = 3;
  static const ADMIN_BAN = 4;
  static const ADMIN_KICK = 5;
  static const MAX_REQUEST_PER_SECOND = 6;
  static const MAX_REQUEST_SIZE = 7;
  static const SERVER_ERROR = 8;
  static const SERVER_NOT_RESPONDING = 400;
  static const UNAUTHORIZED = 401;
}

class EzyDisconnectReasons {
  static const _REASON_NAMES = {
    EzyDisconnectReason.CLOSE: "CLOSE",
    EzyDisconnectReason.UNKNOWN: "UNKNOWN",
    EzyDisconnectReason.IDLE: "IDLE",
    EzyDisconnectReason.NOT_LOGGED_IN: "NOT_LOGGED_IN",
    EzyDisconnectReason.ANOTHER_SESSION_LOGIN: "ANOTHER_SESSION_LOGIN",
    EzyDisconnectReason.ADMIN_BAN: "ADMIN_BAN",
    EzyDisconnectReason.ADMIN_KICK: "ADMIN_KICK",
    EzyDisconnectReason.MAX_REQUEST_PER_SECOND: "MAX_REQUEST_PER_SECOND",
    EzyDisconnectReason.MAX_REQUEST_SIZE: "MAX_REQUEST_SIZE",
    EzyDisconnectReason.SERVER_ERROR: "SERVER_ERROR",
    EzyDisconnectReason.SERVER_NOT_RESPONDING: "SERVER_NOT_RESPONDING",
    EzyDisconnectReason.UNAUTHORIZED: "UNAUTHORIZED"
  };

  EzyDisconnectReasons._();

  static String getDisconnectReasonName(int reasonId) {
    return _REASON_NAMES[reasonId] ?? reasonId.toString();
  }
}

class EzyConnectionFailedReason {
  EzyConnectionFailedReason._();

  static const TIMEOUT = 0;
  static const NETWORK_UNREACHABLE = 1;
  static const UNKNOWN_HOST = 2;
  static const CONNECTION_REFUSED = 3;
  static const UNKNOWN = 4;
}

class EzyConnectionFailedReasons {
  static const _REASON_NAMES = {
    EzyConnectionFailedReason.TIMEOUT: "TIMEOUT",
    EzyConnectionFailedReason.NETWORK_UNREACHABLE: "NETWORK_UNREACHABLE",
    EzyConnectionFailedReason.UNKNOWN_HOST: "UNKNOWN_HOST",
    EzyConnectionFailedReason.CONNECTION_REFUSED: "CONNECTION_REFUSED",
    EzyConnectionFailedReason.UNKNOWN: "UNKNOWN"
  };
  EzyConnectionFailedReasons._();

  static String getConnectionFailedReasonName(int reasonId) {
    return _REASON_NAMES[reasonId] ?? reasonId.toString();
  }
}

Lớp EzyCommand: Lớp này định nghĩa các hằng số lệnh (command) dùng trong giao tiếp. Các lệnh này bao gồm:

  • ERROR: Chỉ báo lỗi.
  • HANDSHAKE: Lệnh bắt tay khi kết nối.
  • PING: Lệnh gửi ping để kiểm tra kết nối.
  • PONG: Lệnh phản hồi ping.
  • LOGIN: Lệnh đăng nhập.
  • LOGIN_ERROR: Lỗi xảy ra khi đăng nhập.
  • LOGOUT: Lệnh đăng xuất.
  • APP_ACCESS, APP_REQUEST, APP_EXIT: Các lệnh truy cập, yêu cầu và thoát khỏi ứng dụng.
  • APP_ACCESS_ERROR, APP_REQUEST_ERROR: Lỗi truy cập và yêu cầu ứng dụng.
  • PLUGIN_INFO, PLUGIN_REQUEST: Thông tin và yêu cầu từ plugin.

Lớp EzyEventType: Lớp này định nghĩa các hằng số cho các loại sự kiện liên quan đến kết nối:

  • CONNECTION_SUCCESS: Kết nối thành công.
  • CONNECTION_FAILURE: Kết nối thất bại.
  • DISCONNECTION: Ngắt kết nối.
  • LOST_PING: Mất tín hiệu ping.
  • TRY_CONNECT: Thử kết nối lại.

Lớp EzyConnectionStatus:Lớp này định nghĩa các trạng thái kết nối:

  • NULL: Trạng thái chưa xác định.
  • CONNECTING: Đang kết nối.
  • CONNECTED: Đã kết nối.
  • DISCONNECTED: Đã ngắt kết nối.
  • FAILURE: Kết nối thất bại.
  • RECONNECTING: Đang kết nối lại.

Lớp EzyDisconnectReason: Lớp này định nghĩa các lý do ngắt kết nối:

  • CLOSE: Đóng kết nối.
  • UNKNOWN: Lý do không xác định.
  • IDLE: Ngắt kết nối do không hoạt động.
  • NOT_LOGGED_IN: Chưa đăng nhập.
  • ANOTHER_SESSION_LOGIN: Phiên đăng nhập khác đã được thực hiện.
  • ADMIN_BAN: Bị cấm bởi quản trị viên.
  • ADMIN_KICK: Bị đuổi bởi quản trị viên.
  • MAX_REQUEST_PER_SECOND: Vượt quá số yêu cầu mỗi giây.
  • MAX_REQUEST_SIZE: Vượt quá kích thước yêu cầu tối đa.
  • SERVER_ERROR: Lỗi từ máy chủ.
  • SERVER_NOT_RESPONDING: Máy chủ không phản hồi.
  • UNAUTHORIZED: Không được phép truy cập.

Lớp EzyDisconnectReasons:Lớp này cung cấp một hàm để lấy tên lý do ngắt kết nối dựa trên mã lý do:

  • Hàm getDisconnectReasonName(int reasonId) sẽ trả về tên lý do ngắt kết nối hoặc mã lý do nếu không tìm thấy tên tương ứng.

Lớp EzyConnectionFailedReason:Lớp này định nghĩa các lý do khiến kết nối thất bại:

  • TIMEOUT: Quá thời gian chờ kết nối.
  • NETWORK_UNREACHABLE: Mạng không thể truy cập.
  • UNKNOWN_HOST: Máy chủ không xác định.
  • CONNECTION_REFUSED: Kết nối bị từ chối.
  • UNKNOWN: Lý do không xác định.

Lớp EzyConnectionFailedReasons:Lớp này cung cấp một hàm để lấy tên lý do thất bại khi kết nối dựa trên mã lý do:

  • Hàm getConnectionFailedReasonName(int reasonId) sẽ trả về tên lý do thất bại hoặc mã lý do nếu không tìm thấy tên tương ứng.

ezy_entities.dart

class EzyZone {
  late int id;
  late String name;
  late EzyClient client;
  late EzyAppManager appManager;

  EzyZone(this.client, this.id, this.name) {
    appManager = EzyAppManager(name);
  }

  EzyApp? getApp() {
    return appManager.getApp();
  }
}

class EzyApp {
  late int id;
  late String name;
  late EzyZone zone;
  late EzyClient client;
  late EzyAppDataHandlers dataHandlers;
  static const Map EMPTY_MAP = {};

  EzyApp(this.client, this.zone, this.id, this.name) {
    dataHandlers = client.handlerManager.getAppDataHandlers(name);
  }

  void send(String cmd, [dynamic data = EMPTY_MAP, bool encrypted = false]) {
    var requestData = [];
    requestData.add(id);
    var requestParams = [];
    requestParams.add(cmd);
    requestParams.add(data);
    requestData.add(requestParams);
    client.send(EzyCommand.APP_REQUEST, requestData, encrypted);
  }

  EzyAppDataHandler? getDataHandler(String cmd) {
    return dataHandlers.getHandler(cmd);
  }
}

class EzyUser {
  late int id;
  late String name;

  EzyUser(this.id, this.name);
}

File này định nghĩa ba lớp (EzyZone, EzyApp, EzyUser) trong Dart để hỗ trợ trong một hệ thống khách-hàng (client) kết nối với server.

Lớp EzyZone

  • Thuộc tính:

    • id: Một số nguyên, đại diện cho ID của zone.
    • name: Chuỗi ký tự, tên của zone.
    • client: Đối tượng EzyClient, đại diện cho client đang kết nối.
    • appManager: Đối tượng EzyAppManager, quản lý các ứng dụng (app) trong zone.
  • Phương thức:

    • Constructor (EzyZone): Khởi tạo một instance của EzyZone với client, id, và name. Đồng thời, nó khởi tạo appManager với tên của zone.
    • getApp: Trả về một đối tượng EzyApp từ appManager nếu có.
      Lớp EzyApp
  • Thuộc tính:

    • id: Một số nguyên, đại diện cho ID của ứng dụng.
    • name: Chuỗi ký tự, tên của ứng dụng.
    • zone: Đối tượng EzyZone, đại diện cho zone mà ứng dụng thuộc về.
    • client: Đối tượng EzyClient, đại diện cho client đang kết nối.
    • dataHandlers: Đối tượng EzyAppDataHandlers, quản lý các handler xử lý dữ liệu cho ứng dụng.
    • EMPTY_MAP: Một map trống ({}), được định nghĩa như một constant để sử dụng làm dữ liệu mặc định.
  • Phương thức:

    • Constructor (EzyApp): Khởi tạo một instance của EzyApp với client, zone, id, và name. Đồng thời, nó khởi tạo dataHandlers bằng cách lấy từ client.handlerManager.
    • send: Gửi một yêu cầu (request) đến server với một lệnh (cmd), dữ liệu (data), và một tùy chọn để mã hóa yêu cầu (encrypted). Yêu cầu này sẽ được gửi qua client bằng cách sử dụng lệnh EzyCommand.APP_REQUEST.
    • getDataHandler: Trả về một đối tượng EzyAppDataHandler để xử lý dữ liệu dựa trên lệnh (cmd) nếu có.

Lớp EzyUser

  • Thuộc tính:

    • id: Một số nguyên, đại diện cho ID của người dùng.
      name: Chuỗi ký tự, tên của người dùng.
  • Constructor (EzyUser): Khởi tạo một instance của EzyUser với id và name.

ezy_handlers.dart

class EzyEventHandler {
  void handle(Map event) {}
}

class EzyDataHandler {
  void handle(List data) {}
}

class EzyAbstractEventHandler extends EzyEventHandler {
  late EzyClient client;
}

class EzyAbstractDataHandler extends EzyDataHandler {
  late EzyClient client;
}

class EzyAppDataHandler<T> {
  void handle(EzyApp app, T data) {}
}

class EzyAbstractAppDataHandler<T> implements EzyAppDataHandler<T> {
  @override
  void handle(EzyApp app, T data) {
    process(app, data);
  }

  void process(EzyApp app, T data) {}
}

class EzyConnectionSuccessHandler extends EzyAbstractEventHandler {
  var clientType = "FLUTTER";
  var clientVersion = "1.0.0";

  @override
  void handle(Map event) {
    sendHandshakeRequest();
    postHandle();
  }

  void postHandle() {}

  void sendHandshakeRequest() {
    generateClientKey((clientKey) =>
        {client.send(EzyCommand.HANDSHAKE, newHandshakeRequest(clientKey))});
  }

  List newHandshakeRequest(String? clientKey) {
    var clientId = getClientId();
    var token = getStoredToken();
    var request = [];
    request.add(clientId);
    request.add(clientKey);
    request.add(clientType);
    request.add(clientVersion);
    request.add(_isEnableSSL(clientKey));
    request.add(token);
    return request;
  }

  bool _isEnableSSL(String? clientKey) {
    return client.enableSSL;
  }

  void _onKeyPairGenerated(
      EzyKeyPairProxy keyPair, Function(String?) callback) {
    client.privateKey = keyPair.privateKey;
    callback(keyPair.publicKey);
  }

  void generateClientKey(Function(String?) callback) {
    if (client.enableSSL) {
      EzyRSAProxy.getInstance().generateKeyPair(
          (keyPair) => {_onKeyPairGenerated(keyPair, callback)});
    } else {
      callback(null);
    }
  }

  String getClientId() {
    return UUID.random();
  }

  String getStoredToken() {
    return "";
  }
}

//=======================================================
class EzyConnectionFailureHandler extends EzyAbstractEventHandler {
  @override
  void handle(Map event) {
    var reason = event["reason"] as int;
    var reasonName =
        EzyConnectionFailedReasons.getConnectionFailedReasonName(reason);
    EzyLogger.warn("connection failure, reason = $reasonName");
    var config = client.config;
    var reconnectConfig = config.reconnect;
    var should = shouldReconnect(event);
    var reconnectEnable = reconnectConfig.enable;
    var mustReconnect = reconnectEnable && should;
    client.setStatus(EzyConnectionStatus.FAILURE);
    if (mustReconnect) {
      client.reconnect().then((value) => {_onReconnect(event, value)});
    } else {
      onConnectionFailed(event);
      postHandle(event);
    }
  }

  bool shouldReconnect(Map event) {
    return true;
  }

  void _onReconnect(Map event, bool success) {
    if (success) {
      onReconnecting(event);
    } else {
      onConnectionFailed(event);
    }
    postHandle(event);
  }

  void onReconnecting(Map event) {}

  void onConnectionFailed(Map event) {}

  void postHandle(Map event) {}
}

//=======================================================
class EzyDisconnectionHandler extends EzyAbstractEventHandler {
  @override
  void handle(Map event) {
    var reason = event["reason"] as int;
    var reasonName = EzyDisconnectReasons.getDisconnectReasonName(reason);
    EzyLogger.info("handle disconnection, reason = $reasonName");
    preHandle(event);
    var config = client.config;
    var reconnectConfig = config.reconnect;
    var should = shouldReconnect(event);
    var reconnectEnable = reconnectConfig.enable;
    var mustReconnect = reconnectEnable &&
        reason != EzyDisconnectReason.UNAUTHORIZED &&
        reason != EzyDisconnectReason.CLOSE &&
        should;
    client.setStatus(EzyConnectionStatus.DISCONNECTED);
    if (mustReconnect) {
      client.reconnect().then((value) => {_onReconnect(event, value)});
    } else {
      onDisconnected(event);
      postHandle(event);
    }
  }

  void _onReconnect(Map event, bool success) {
    if (success) {
      onReconnecting(event);
    } else {
      onDisconnected(event);
    }
    postHandle(event);
  }

  void preHandle(Map event) {}

  bool shouldReconnect(Map event) {
    var reason = event["reason"] as int;
    if (reason == EzyDisconnectReason.ANOTHER_SESSION_LOGIN) {
      return false;
    }
    return true;
  }

  void control(Map event) {}

  void onReconnecting(Map event) {}

  void onDisconnected(Map event) {}

  void postHandle(Map event) {}
}

//=======================================================
class EzyPongHandler extends EzyAbstractDataHandler {}

//=======================================================

class EzyHandshakeHandler extends EzyAbstractDataHandler {
  @override
  void handle(List data) {
    startPing();
    doHandle(data);
  }

  void _onSessionKeyDecrypted(List data, Uint8List? sessionKey, bool success) {
    if (sessionKey != null) {
      client.setSessionKey(sessionKey);
    }
    if (success) {
      handleLogin();
    }
    postHandle(data);
  }

  void doHandle(List data) {
    client.sessionToken = data[1] as String;
    client.sessionId = data[2] as int;
    if (client.enableSSL) {
      decryptSessionKey(
          data[3],
          (sessionKey, success) =>
              {_onSessionKeyDecrypted(data, sessionKey, success)});
    } else {
      _onSessionKeyDecrypted(data, null, true);
    }
  }

  void decryptSessionKey(
      Uint8List? encryptedSessionKey, Function(Uint8List?, bool) callback) {
    if (encryptedSessionKey == null) {
      if (client.enableDebug) {
        callback(null, true);
        return;
      }
      EzyLogger.error(
        "maybe server was not enable SSL, you must enable SSL on server or disable SSL on your client or enable debug mode",
      );
      client.close();
      callback(null, false);
      return;
    }
    EzyRSAProxy.getInstance().decrypt(encryptedSessionKey, client.privateKey!,
        (sessionKey) => {callback(sessionKey, true)});
  }

  void postHandle(List data) {}

  void handleLogin() {
    var loginRequest = getLoginRequest();
    client.send(EzyCommand.LOGIN, loginRequest, encryptedLoginRequest());
  }

  bool encryptedLoginRequest() {
    return false;
  }

  List getLoginRequest() {
    var array = [];
    array.add("test");
    array.add("test");
    array.add("test");
    array.add([]);
    return array;
  }

  void startPing() {
    client.startPingSchedule();
  }
}

//=======================================================
class EzyLoginSuccessHandler extends EzyAbstractDataHandler {
  @override
  void handle(List data) {
    var responseData = data[4];
    var user = newUser(data);
    var zone = newZone(data);
    client.me = user;
    client.zone = zone;
    handleLoginSuccess(responseData);
    EzyLogger.info("user: ${user.name} logged in successfully");
  }

  EzyUser newUser(List data) {
    var userId = data[2] as int;
    var username = data[3] as String;
    var user = EzyUser(userId, username);
    return user;
  }

  EzyZone newZone(List data) {
    var zoneId = data[0] as int;
    var zoneName = data[1] as String;
    var zone = EzyZone(client, zoneId, zoneName);
    return zone;
  }

  void handleLoginSuccess(dynamic responseData) {}
}

//=======================================================
class EzyLoginErrorHandler extends EzyAbstractDataHandler {
  @override
  void handle(List data) {
    client.disconnect(EzyDisconnectReason.UNAUTHORIZED);
    handleLoginError(data);
  }

  void handleLoginError(List data) {}
}

//=======================================================
class EzyAppAccessHandler extends EzyAbstractDataHandler {
  @override
  void handle(List data) {
    var zone = client.zone;
    var appManager = zone!.appManager;
    var app = newApp(zone, data);
    appManager.addApp(app);
    postHandle(app, data);
    EzyLogger.info("access app: ${app.name} successfully");
  }

  EzyApp newApp(EzyZone zone, List data) {
    var appId = data[0] as int;
    var appName = data[1] as String;
    var app = EzyApp(client, zone, appId, appName);
    return app;
  }

  void postHandle(EzyApp app, List data) {}
}

//=======================================================
class EzyAppExitHandler extends EzyAbstractDataHandler {
  @override
  void handle(List data) {
    var zone = client.zone;
    var appManager = zone!.appManager;
    var appId = data[0] as int;
    var reasonId = data[1] as int;
    var app = appManager.removeApp(appId);
    if (app != null) {
      postHandle(app, data);
      EzyLogger.info("user exit app: ${app.name}, reason: $reasonId");
    }
  }

  void postHandle(EzyApp app, List data) {}
}

//=======================================================
class EzyAppResponseHandler extends EzyAbstractDataHandler {
  @override
  void handle(List data) {
    var appId = data[0] as int;
    var responseData = data[1] as List;
    var cmd = responseData[0];
    var commandData = responseData[1] as Map;

    var app = client.getAppById(appId);
    if (app == null) {
      EzyLogger.info("receive message when has not joined app yet");
      return;
    }
    var handler = app.getDataHandler(cmd);
    if (handler != null) {
      handler.handle(app, commandData);
    } else {
      EzyLogger.warn("app: ${app.name} has no handler for command: $cmd");
    }
  }
}

//=======================================================
class EzyEventHandlers {
  late EzyClient client;
  late Map handlers;

  EzyEventHandlers(this.client) {
    handlers = Map();
  }

  void addHandler(String eventType, EzyEventHandler handler) {
    var abs = handler as EzyAbstractEventHandler;
    abs.client = client;
    handlers[eventType] = handler;
  }

  EzyEventHandler? getHandler(String eventType) {
    return handlers[eventType];
  }

  void handle(String eventType, Map data) {
    var handler = getHandler(eventType);
    if (handler != null) {
      handler.handle(data);
    } else {
      EzyLogger.warn("has no handler with event: $eventType");
    }
  }
}

//=======================================================

class EzyDataHandlers {
  late EzyClient client;
  late Map handlerByCommand;

  EzyDataHandlers(this.client) {
    handlerByCommand = Map();
  }

  void addHandler(String cmd, EzyDataHandler handler) {
    var abs = handler as EzyAbstractDataHandler;
    abs.client = client;
    handlerByCommand[cmd] = handler;
  }

  EzyDataHandler? getHandler(String cmd) {
    return handlerByCommand[cmd];
  }

  void handle(String cmd, dynamic data) {
    var handler = getHandler(cmd);
    if (handler != null) {
      handler.handle(data);
    } else {
      EzyLogger.warn("has no handler with command: $cmd");
    }
  }
}

//=======================================================

class EzyAppDataHandlers {
  late Map<String, EzyAppDataHandler> _handlerByAppName;

  EzyAppDataHandlers() {
    _handlerByAppName = Map();
  }

  void addHandler(String cmd, EzyAppDataHandler handler) {
    _handlerByAppName[cmd] = handler;
  }

  EzyAppDataHandler? getHandler(String cmd) {
    return _handlerByAppName[cmd];
  }
}

File code này có mục đích chính là xử lý các sự kiện và dữ liệu từ server thông qua các lớp handler (trình xử lý).
Các lớp cơ bản:

  • EzyEventHandler và EzyDataHandler: Đây là các lớp cơ bản, mỗi lớp có một phương thức handle để xử lý sự kiện hoặc dữ liệu. Lớp EzyEventHandler xử lý sự kiện dưới dạng Map, còn EzyDataHandler xử lý dữ liệu dưới dạng List.

  • EzyAbstractEventHandlerEzyAbstractDataHandler: Các lớp này mở rộng từ EzyEventHandler và EzyDataHandler. Chúng thêm thuộc tính client, có khả năng truy cập vào EzyClient, đại diện cho client đang hoạt động.

  • EzyAppDataHandlerEzyAbstractAppDataHandler: Các lớp này dùng để xử lý dữ liệu liên quan đến một ứng dụng cụ thể (EzyApp). EzyAbstractAppDataHandler định nghĩa một phương thức process để xử lý dữ liệu.

Các lớp xử lý sự kiện (Event Handlers):

  • EzyConnectionSuccessHandler: Xử lý sự kiện kết nối thành công. Lớp này gửi yêu cầu bắt tay (handshake) với server khi kết nối thành công, và tùy thuộc vào việc client có hỗ trợ SSL hay không, nó sẽ tạo khóa client để gửi đến server.

  • EzyConnectionFailureHandler: Xử lý sự kiện kết nối thất bại. Lớp này xác định lý do thất bại và quyết định xem có cần thử kết nối lại hay không, dựa trên cấu hình của client và sự kiện nhận được.

  • EzyDisconnectionHandler: Xử lý sự kiện ngắt kết nối. Lớp này quyết định xem có cần kết nối lại hay không dựa trên lý do ngắt kết nối và cấu hình của client.

  • EzyPongHandler: Xử lý phản hồi từ server khi client gửi ping.

  • EzyHandshakeHandler: Xử lý phản hồi của server sau khi yêu cầu bắt tay được gửi đi. Nó cũng chịu trách nhiệm giải mã khóa phiên (session key) nếu SSL được kích hoạt.

  • EzyLoginSuccessHandler: Xử lý sự kiện đăng nhập thành công. Lớp này khởi tạo đối tượng người dùng (EzyUser) và zone (EzyZone) cho client sau khi đăng nhập thành công.

  • EzyLoginErrorHandler: Xử lý lỗi đăng nhập. Nếu đăng nhập thất bại, client sẽ ngắt kết nối với server.

  • EzyAppAccessHandler: Xử lý sự kiện truy cập vào một ứng dụng (EzyApp). Lớp này tạo một ứng dụng mới và thêm nó vào quản lý ứng dụng (appManager).

  • EzyAppExitHandler: Xử lý sự kiện thoát khỏi một ứng dụng. Lớp này xóa ứng dụng khỏi quản lý ứng dụng và ghi lại lý do thoát.

  • EzyAppResponseHandler: Xử lý các phản hồi từ ứng dụng. Lớp này kiểm tra xem ứng dụng đã có trình xử lý dữ liệu tương ứng hay chưa, nếu có thì gọi trình xử lý đó để xử lý dữ liệu.

Các lớp quản lý handler:

  • EzyEventHandlers: Quản lý các trình xử lý sự kiện (EzyEventHandler). Nó có thể thêm, lấy, và xử lý sự kiện dựa trên loại sự kiện.

  • EzyDataHandlers: Quản lý các trình xử lý dữ liệu (EzyDataHandler). Nó có thể thêm, lấy, và xử lý dữ liệu dựa trên lệnh (command).

  • EzyAppDataHandlers: Quản lý các trình xử lý dữ liệu của ứng dụng (EzyAppDataHandler). Tương tự như EzyDataHandlers, nhưng dành riêng cho dữ liệu của ứng dụng.

ezy_loger.dart

class EzyLogger {
  EzyLogger._();

  static void error(String message) {
    Map params = {
      "level": "e",
      "message": message
    };
    EzyProxy.run("log", params);
  }

  static void warn(String message) {
    Map params = {
      "level": "w",
      "message": message
    };
    EzyProxy.run("log", params);
  }

  static void info(String message) {
    Map params = {
      "level": "i",
      "message": message
    };
    EzyProxy.run("log", params);
  }
}

File code này định nghĩa một lớp EzyLogger trong Dart, được sử dụng để ghi lại các thông tin nhật ký (logs) với các mức độ khác nhau: lỗi (error), cảnh báo (warn), và thông tin (info). Lớp này được thiết kế để gửi các thông tin nhật ký đến một đối tượng proxy thông qua phương thức run của lớp EzyProxy.

Lớp EzyLogger:

  • EzyLogger._();:
    • Đây là một constructor riêng (private constructor) được sử dụng để ngăn không cho tạo đối tượng từ lớp EzyLogger. Bởi vì tất cả các phương thức trong lớp đều là phương thức tĩnh (static), không cần thiết phải khởi tạo đối tượng của lớp này để sử dụng các phương thức của nó.
      Phương thức error(String message):
  • Phương thức này ghi lại một thông điệp nhật ký với mức độ là “error” (lỗi).
  • Tạo một Map chứa cặp key-value:
    • “level”: “e”: Xác định mức độ nhật ký là error (e).
    • “message”: message: Thông điệp nhật ký.
  • Gọi phương thức run của EzyProxy để gửi thông điệp nhật ký cùng với các tham số đến đối tượng proxy.
    Phương thức warn(String message):
  • Phương thức này ghi lại một thông điệp nhật ký với mức độ là “warn” (cảnh báo).
  • Tạo một Map chứa cặp key-value:
    • “level”: “w”: Xác định mức độ nhật ký là cảnh báo (w).
    • “message”: message: Thông điệp nhật ký.
  • Gọi phương thức run của EzyProxy để gửi thông điệp nhật ký cùng với các tham số đến đối tượng proxy.
    Phương thức info(String message):
  • Phương thức này ghi lại một thông điệp nhật ký với mức độ là “info” (thông tin).
  • Tạo một Map chứa cặp key-value:
    • “level”: “i”: Xác định mức độ nhật ký là thông tin (i).
    • “message”: message: Thông điệp nhật ký.
  • Gọi phương thức run của EzyProxy để gửi thông điệp nhật ký cùng với các tham số đến đối tượng proxy.
  • Sử dụng EzyProxy.run:
    Phương thức run của EzyProxy nhận vào hai tham số:
  • Tham số đầu tiên “log”: Xác định hành động cần thực hiện, trong trường hợp này là ghi nhật ký.
  • Tham số thứ hai params: Là một bản đồ chứa thông tin về mức độ nhật ký và thông điệp cần ghi.

ezy_manager.dart

class EzyAppManager {
  late String zoneName;
  late List<EzyApp> appList;
  late Map<int, EzyApp> appById;
  late Map<String, EzyApp> appByName;

  EzyAppManager(this.zoneName) {
    appList = [];
    appById = Map();
    appByName = Map();
  }

  EzyApp? getApp() {
    if (appList.isEmpty) {
      EzyLogger.warn("there is no app in zone: $zoneName");
      return null;
    }
    return appList[0];
  }

  void addApp(EzyApp app) {
    appList.add(app);
    appById[app.id] = app;
    appByName[app.name] = app;
  }

  EzyApp? removeApp(int appId) {
    var app = appById[appId];
    if (app != null) {
      appList.remove(app);
      appById.remove(app.id);
      appByName.remove(app.name);
    }
    return app;
  }

  EzyApp? getAppById(int appId) {
    return appById[appId];
  }

  EzyApp? getAppByName(String appName) {
    return appByName[appName];
  }
}

//===================================================
class EzyHandlerManager {
  late EzyClient client;
  late EzyDataHandlers dataHandlers;
  late EzyEventHandlers eventHandlers;
  late Map<String, EzyAppDataHandlers> appDataHandlersByAppName;

  EzyHandlerManager(this.client) {
    appDataHandlersByAppName = Map<String, EzyAppDataHandlers>();
    dataHandlers = _newDataHandlers();
    eventHandlers = _newEventHandlers();
  }

  static EzyHandlerManager create(EzyClient client) {
    var pRet = EzyHandlerManager(client);
    return pRet;
  }

  EzyEventHandlers _newEventHandlers() {
    var handlers = EzyEventHandlers(client);
    handlers.addHandler(
        EzyEventType.CONNECTION_SUCCESS, EzyConnectionSuccessHandler());
    handlers.addHandler(
        EzyEventType.CONNECTION_FAILURE, EzyConnectionFailureHandler());
    handlers.addHandler(EzyEventType.DISCONNECTION, EzyDisconnectionHandler());
    return handlers;
  }

  EzyDataHandlers _newDataHandlers() {
    var handlers = EzyDataHandlers(client);
    handlers.addHandler(EzyCommand.PONG, EzyPongHandler());
    handlers.addHandler(EzyCommand.HANDSHAKE, EzyHandshakeHandler());
    handlers.addHandler(EzyCommand.LOGIN, EzyLoginSuccessHandler());
    handlers.addHandler(EzyCommand.LOGIN_ERROR, EzyLoginErrorHandler());
    handlers.addHandler(EzyCommand.APP_ACCESS, EzyAppAccessHandler());
    handlers.addHandler(EzyCommand.APP_EXIT, EzyAppExitHandler());
    handlers.addHandler(EzyCommand.APP_REQUEST, EzyAppResponseHandler());
    return handlers;
  }

  EzyDataHandler? getDataHandler(String cmd) {
    return dataHandlers.getHandler(cmd);
  }

  EzyEventHandler? getEventHandler(String eventType) {
    return eventHandlers.getHandler(eventType);
  }

  EzyAppDataHandlers getAppDataHandlers(String appName) {
    var answer = appDataHandlersByAppName[appName];
    if (answer == null) {
      answer = EzyAppDataHandlers();
      appDataHandlersByAppName[appName] = answer;
    }
    return answer;
  }

  void addDataHandler(String cmd, EzyDataHandler handler) {
    dataHandlers.addHandler(cmd, handler);
  }

  void addEventHandler(String eventType, EzyEventHandler handler) {
    eventHandlers.addHandler(eventType, handler);
  }
}

File code bao gồm hai lớp chính là EzyAppManagerEzyHandlerManager. Cả hai lớp này đều đóng vai trò quản lý trong ứng dụng, cụ thể là quản lý các ứng dụng và xử lý các sự kiện hoặc dữ liệu từ server.

Lớp EzyAppManager:

  • Thuộc tính:
    • zoneName: Tên của “zone” (khu vực) trong ứng dụng. Zone thường được sử dụng để phân chia các ứng dụng trong một hệ thống.
    • appList: Một danh sách chứa các đối tượng EzyApp. Mỗi đối tượng EzyApp đại diện cho một ứng dụng trong zone.
    • appById: Một Map lưu trữ các ứng dụng với khóa là ID của ứng dụng (app.id).
    • appByName: Một Map lưu trữ các ứng dụng với khóa là tên của ứng dụng (app.name).
  • Phương thức:
    • EzyAppManager(this.zoneName): Constructor của lớp EzyAppManager, khởi tạo zoneName, appList, appById, và appByName.
    • getApp(): Trả về ứng dụng đầu tiên trong appList. Nếu không có ứng dụng nào, nó sẽ ghi một cảnh báo và trả về null.
    • addApp(EzyApp app): Thêm một ứng dụng vào danh sách appList và lưu trữ nó vào appById và appByName.
    • removeApp(int appId): Xóa một ứng dụng khỏi appList, appById, và appByName dựa trên appId. Trả về ứng dụng vừa bị xóa nếu tồn tại.
    • getAppById(int appId): Trả về ứng dụng dựa trên appId.
    • getAppByName(String appName): Trả về ứng dụng dựa trên appName.
      Lớp EzyHandlerManager:
  • Thuộc tính:
    • client: Một đối tượng EzyClient, đại diện cho client đang sử dụng ứng dụng.
      dataHandlers: Một đối tượng EzyDataHandlers, quản lý các handler (xử lý) cho dữ liệu nhận được từ server.
    • eventHandlers: Một đối tượng EzyEventHandlers, quản lý các handler cho sự kiện nhận được từ server.
    • appDataHandlersByAppName: Một Map lưu trữ các handler dữ liệu của ứng dụng dựa trên tên ứng dụng.
  • Phương thức:
    • EzyHandlerManager(this.client): Constructor của lớp EzyHandlerManager, khởi tạo các handler dữ liệu, sự kiện và appDataHandlersByAppName.
    • create(EzyClient client): Phương thức tạo một đối tượng EzyHandlerManager và trả về nó.
    • _newEventHandlers(): Tạo và cấu hình các handler sự kiện cho các loại sự kiện khác nhau như CONNECTION_SUCCESS, CONNECTION_FAILURE, DISCONNECTION.
    • _newDataHandlers(): Tạo và cấu hình các handler dữ liệu cho các lệnh khác nhau như PONG, HANDSHAKE, LOGIN, APP_ACCESS, v.v.
    • getDataHandler(String cmd): Trả về handler dữ liệu tương ứng với lệnh (cmd) đã được đăng ký.
      getEventHandler(String eventType): Trả về handler sự kiện tương ứng với loại sự kiện (eventType) đã được đăng ký.
    • getAppDataHandlers(String appName): Trả về handler dữ liệu của ứng dụng dựa trên tên ứng dụng (appName). Nếu chưa tồn tại, nó sẽ tạo mới và lưu trữ.
    • addDataHandler(String cmd, EzyDataHandler handler): Thêm một handler dữ liệu vào dataHandlers cho lệnh (cmd) cụ thể.
    • addEventHandler(String eventType, EzyEventHandler handler): Thêm một handler sự kiện vào eventHandlers cho loại sự kiện (eventType) cụ thể.

ezy_proxy.dart

class EzyProxy {
  late MethodChannel _methodChannel;

  static final EzyProxy _INSTANCE = EzyProxy._();

  EzyProxy._() {
    _methodChannel = const MethodChannel('com.tvd12.ezyfoxserver.client');
    _methodChannel.setMethodCallHandler(_handleSocketEventDatas);
  }

  static EzyProxy getInstance() {
    return _INSTANCE;
  }

  Future<void> _handleSocketEventDatas(MethodCall call) async {
    // type inference will work here avoiding an explicit cast
    switch (call.method) {
      case "ezy.event":
        _onSocketEvent(call.arguments);
        break;
      case "ezy.data":
        _onSocketData(call.arguments);
        break;
      default:
        EzyLogger.warn(
          "there is no handler for method: ${call.method}, ignore it",
        );
    }
  }

  void _onSocketEvent(Map arguments) {
    String clientName = arguments["clientName"];
    String eventType = arguments["eventType"];
    dynamic data = arguments["data"];
    EzyClient client = _getClient(clientName);
    client.handleEvent(eventType, data);
  }

  void _onSocketData(Map arguments) {
    String clientName = arguments["clientName"];
    String command = arguments["command"];
    dynamic data = arguments["data"];
    EzyClient client = _getClient(clientName);
    client.handleData(command, data);
  }

  static Future<T?> run<T>(String method, Map params) {
    return EzyProxy.getInstance()._methodChannel.invokeMethod(method, params);
  }

  EzyClient _getClient(String clientName) {
    return EzyClients.getInstance().getClient(clientName);
  }
}

File này định nghĩa lớp EzyProxy, lớp này đóng vai trò làm cầu nối giữa Flutter và phần native của ứng dụng (có thể là Android hoặc iOS) thông qua MethodChannel. Nó xử lý các sự kiện và dữ liệu từ server được truyền đến từ phần native và phân phối chúng đến các client tương ứng.
Thuộc tính và Constructor:

  • _methodChannel: Đây là một MethodChannel, dùng để giao tiếp với phần native. Kênh này được định danh bởi một chuỗi ‘com.tvd12.ezyfoxserver.client’.

  • _INSTANCE: Đây là một đối tượng singleton của lớp EzyProxy, được khởi tạo một lần duy nhất và được sử dụng lại mỗi khi cần.

  • EzyProxy._(): Đây là constructor riêng của lớp, sử dụng cú pháp dấu gạch dưới (_) để chỉ ra rằng nó là private. Constructor này khởi tạo MethodChannel và đăng ký một phương thức gọi lại (_handleSocketEventDatas) để xử lý các sự kiện đến từ phần native.
    Phương thức getInstance():

  • Phương thức này trả về đối tượng singleton _INSTANCE của lớp EzyProxy. Đây là cách để đảm bảo rằng chỉ có một đối tượng EzyProxy tồn tại trong suốt vòng đời của ứng dụng.
    Phương thức _handleSocketEventDatas():

  • _handleSocketEventDatas(MethodCall call): Phương thức này được sử dụng để xử lý các cuộc gọi phương thức từ phần native. Dựa trên giá trị call.method, phương thức này sẽ gọi _onSocketEvent() hoặc _onSocketData() để xử lý sự kiện hoặc dữ liệu tương ứng.
    Phương thức _onSocketEvent():

  • _onSocketEvent(Map arguments): Phương thức này xử lý các sự kiện socket được gửi từ server. Nó lấy clientName, eventType, và data từ arguments, sau đó gọi handleEvent() của đối tượng EzyClient tương ứng để xử lý sự kiện.
    Phương thức _onSocketData():

  • _onSocketData(Map arguments): Tương tự như _onSocketEvent(), phương thức này xử lý dữ liệu socket được gửi từ server. Nó lấy clientName, command, và data từ arguments, sau đó gọi handleData() của đối tượng EzyClient tương ứng để xử lý dữ liệu.
    Phương thức run():

  • static Future<T?> run<T>(String method, Map params): Phương thức tĩnh này được sử dụng để gọi một phương thức trong phần native. Nó sử dụng invokeMethod để gọi phương thức với tên method và các tham số params được truyền vào.
    Phương thức _getClient():

  • EzyClient _getClient(String clientName): Phương thức này lấy đối tượng EzyClient từ EzyClients dựa trên clientName. EzyClients có thể là một lớp khác quản lý danh sách các client.

ezy_setup.dart

class EzySetup {
  late EzyHandlerManager handlerManager;
  late Map<String, EzyAppSetup> appSetupByAppName;

  EzySetup(this.handlerManager) {
    appSetupByAppName = Map();
  }

  EzySetup addDataHandler(String cmd, EzyDataHandler handler) {
    handlerManager.addDataHandler(cmd, handler);
    return this;
  }

  EzySetup addEventHandler(String eventType, EzyEventHandler handler) {
    handlerManager.addEventHandler(eventType, handler);
    return this;
  }

  EzyAppSetup setupApp(String appName) {
    var appSetup = appSetupByAppName[appName];
    if (appSetup == null) {
      var appDataHandlers = handlerManager.getAppDataHandlers(appName);
      appSetup = EzyAppSetup(appDataHandlers, this);
      appSetupByAppName[appName] = appSetup;
    }
    return appSetup;
  }
}

class EzyAppSetup {
  late EzySetup parent;
  late EzyAppDataHandlers dataHandlers;

  EzyAppSetup(this.dataHandlers, this.parent);

  EzyAppSetup addDataHandler(String cmd, EzyAppDataHandler handler) {
    dataHandlers.addHandler(cmd, handler);
    return this;
  }

  EzySetup done() {
    return parent;
  }
}

File này định nghĩa hai lớp là EzySetupEzyAppSetup, cung cấp cách để cấu hình và thiết lập các bộ xử lý sự kiện và dữ liệu (handlers) cho các ứng dụng và client trong hệ thống.

Lớp EzySetup:

  • Thuộc tính:

    • handlerManager: Đây là một đối tượng của lớp EzyHandlerManager, dùng để quản lý các bộ xử lý sự kiện và dữ liệu cho ứng dụng.

    • appSetupByAppName: Đây là một Map chứa các đối tượng EzyAppSetup, được ánh xạ bởi tên ứng dụng (appName). Mỗi ứng dụng sẽ có một cấu hình riêng biệt.

  • Constructor:

    • EzySetup(this.handlerManager): Khởi tạo EzySetup với đối tượng handlerManager và tạo một Map để lưu các cấu hình ứng dụng (appSetupByAppName).
  • Phương thức:

    • addDataHandler(String cmd, EzyDataHandler handler): Phương thức này thêm một bộ xử lý dữ liệu (EzyDataHandler) cho một lệnh (cmd). Nó gọi phương thức addDataHandler của handlerManager để thêm handler và trả về đối tượng EzySetup (cho phép gọi chuỗi).
    • addEventHandler(String eventType, EzyEventHandler handler): Phương thức này thêm một bộ xử lý sự kiện (EzyEventHandler) cho một loại sự kiện (eventType). Nó gọi addEventHandler của handlerManager để thêm handler và cũng trả về đối tượng EzySetup.
    • setupApp(String appName): Phương thức này thiết lập cấu hình cho một ứng dụng dựa trên tên ứng dụng (appName). Nếu chưa có cấu hình cho ứng dụng đó, nó sẽ tạo một đối tượng EzyAppSetup, lưu vào appSetupByAppName, và trả về đối tượng EzyAppSetup để tiếp tục cấu hình.
      Lớp EzyAppSetup:
  • Thuộc tính:

    • parent: Đây là đối tượng EzySetup cha, dùng để quay lại cấu hình tổng thể sau khi thiết lập cấu hình cho một ứng dụng cụ thể.

    • dataHandlers: Đây là đối tượng EzyAppDataHandlers, dùng để quản lý các bộ xử lý dữ liệu cho ứng dụng cụ thể.

  • Constructor:

    • EzyAppSetup(this.dataHandlers, this.parent): Khởi tạo đối tượng EzyAppSetup với bộ xử lý dữ liệu (dataHandlers) và đối tượng cha (parent).
  • Phương thức:

    • addDataHandler(String cmd, EzyAppDataHandler handler): Phương thức này thêm một bộ xử lý dữ liệu cho một lệnh (cmd) trong một ứng dụng cụ thể. Nó trả về chính đối tượng EzyAppSetup để tiếp tục gọi chuỗi.

    • done(): Phương thức này dùng để kết thúc việc thiết lập cho một ứng dụng và quay trở lại cấu hình tổng thể (EzySetup). Điều này cho phép tiếp tục thiết lập cho các phần khác của hệ thống.

ezy_util.dart

class UUID {
  static const _CHARS =
      'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
  UUID._() {}

  static String random() {
    return "${_randomString(8)}-${_randomString(8)}-${_randomString(8)}-${_randomString(8)}";
  }

  static String _randomString(int length) {
    Random random = Random();
    return String.fromCharCodes(Iterable.generate(
        length, (_) => _CHARS.codeUnitAt(random.nextInt(_CHARS.length))));
  }
}

File này định nghĩa một lớp UUID, cung cấp chức năng tạo ra các chuỗi UUID ngẫu nhiên.

Thuộc tính tĩnh _CHARS:

  • CHARS là một chuỗi hằng (const) chứa tất cả các ký tự có thể được sử dụng để tạo ra chuỗi UUID. Chuỗi này bao gồm các ký tự chữ cái (viết hoa và viết thường) và các chữ số.
    Constructor ẩn UUID.
    () {}:
  • Đây là một constructor ẩn (private constructor), nghĩa là lớp UUID không thể được khởi tạo từ bên ngoài. Mục đích của việc này là để ngăn không cho lớp này bị khởi tạo một cách vô ý và chỉ sử dụng các phương thức tĩnh mà lớp cung cấp.
    Phương thức tĩnh random():
  • Phương thức random() trả về một chuỗi UUID ngẫu nhiên có định dạng xxxxxxxx-xxxxxxxx-xxxxxxxx-xxxxxxxx, trong đó mỗi phần xxxxxxxx là một chuỗi ngẫu nhiên dài 8 ký tự. Để tạo chuỗi này, nó gọi phương thức _randomString(int length) bốn lần và nối các kết quả lại với nhau bằng dấu gạch ngang (-).
    Phương thức tĩnh _randomString(int length):
  • Phương thức _randomString(int length) tạo ra một chuỗi ngẫu nhiên có độ dài xác định (length).
  • Nó sử dụng lớp Random để sinh ra các số ngẫu nhiên.
  • Sử dụng Iterable.generate để tạo ra một chuỗi các mã ký tự Unicode (mã của các ký tự trong _CHARS), sau đó sử dụng String.fromCharCodes để chuyển đổi các mã ký tự đó thành một chuỗi.

Tổng kết

Thư viện ezyfox-server-flutter-client là một giải pháp cho việc tích hợp ứng dụng Flutter với máy chủ EzyFox Server, một nền tảng mạnh mẽ cho phát triển các ứng dụng thời gian thực như chat, game, hay các ứng dụng tương tác trực tuyến.