Сделал подключение к серверу, добавил опкоды в opcode_map.dart и их значение, отправку sessionInit и пинги каждые 30 секунд

также доработал packet.dart (т.к. стандартная библа dart_lz4 не умеет работать с блоками)
добавил countries.dart (в будущем пригодится для подгрузки стран через которые доступен вход в MAX)
This commit is contained in:
klockky
2026-03-14 22:29:53 +03:00
parent d3f1147e60
commit b2d60ccdd4
13 changed files with 1430 additions and 83 deletions
+198
View File
@@ -0,0 +1,198 @@
import 'dart:async';
import 'dart:typed_data';
import '../core/config/config.dart';
import '../core/protocol/opcode_map.dart';
import '../core/protocol/packet.dart';
import '../core/transport/connection.dart';
import '../core/transport/dispatcher.dart';
import '../core/transport/receiver.dart';
import '../core/transport/sender.dart';
import '../core/utils/logger.dart';
enum SessionState { disconnected, connecting, connected, online }
/// Клиент API.
///
/// Подключение, хэндшейк, пинг, реконнект.
class Api {
final Connection _connection = Connection();
final PacketReceiver _receiver = PacketReceiver();
final PacketSender _sender = PacketSender();
final PacketDispatcher _dispatcher = PacketDispatcher();
SessionState _sessionState = SessionState.disconnected;
final _stateController = StreamController<SessionState>.broadcast();
Stream<SessionState> get stateStream => _stateController.stream;
SessionState get state => _sessionState;
StreamSubscription<Uint8List>? _dataSubscription;
StreamSubscription<SocketState>? _socketStateSubscription;
Timer? _pingTimer;
Timer? _reconnectTimer;
int _reconnectAttempts = 0;
bool _autoReconnect = false;
// Публичное API
/// Подключается к серверу, шлёт хэндшейк, запускает пинг.
Future<void> connect() async {
if (_sessionState != SessionState.disconnected) return;
_autoReconnect = true;
_setSessionState(SessionState.connecting);
_dataSubscription = _connection.dataStream.listen(_onDataReceived);
_socketStateSubscription = _connection.stateStream.listen((socketState) {
if (socketState == SocketState.disconnected &&
_sessionState != SessionState.disconnected) {
_onDisconnected();
}
});
try {
await _connection.connect(ServerConfig.host, ServerConfig.port);
} catch (e) {
logger.e('Не удалось подключиться: $e');
_cleanup();
_setSessionState(SessionState.disconnected);
_scheduleReconnect();
return;
}
_setSessionState(SessionState.connected);
_reconnectAttempts = 0;
try {
final response = await sendHandshake();
if (response.isOk) {
_setSessionState(SessionState.online);
_startPinging();
logger.i('Сессия онлайн, хэндшейк ок');
} else {
logger.e('Хэндшейк отклонён: ${response.payload}');
}
} catch (e) {
logger.e('Ошибка хэндшейка: $e');
}
}
/// Отключается без автореконнекта.
Future<void> disconnect() async {
_autoReconnect = false;
_reconnectTimer?.cancel();
_cleanup();
await _connection.disconnect();
_setSessionState(SessionState.disconnected);
}
/// Отправляет хэндшейк (opcode 6).
Future<Packet> sendHandshake() {
// TODO: заменить захардкоженные данные на реальные
final payload = <dynamic, dynamic>{
'mt_instanceid': '550e8400-e29b-41d4-a716-446655440000',
'clientSessionId': 42,
'deviceId': 'a1b2c3d4e5f6a7b8',
'userAgent': {
'deviceType': "ANDROID",
'locale': 'en',
'deviceLocale': 'en_US',
'osVersion': 'Ondroid 14',
'deviceName': 'KometPhone',
'appVersion': '26.8.1',
'screen': '1920x1080',
'timezone': 'Europe/Moscow',
'pushDeviceType': 'GCM',
'arch': 'arm64',
'buildNumber': 6606,
},
};
return sendRequest(Opcode.sessionInit, payload);
}
/// Отправляет запрос и ждёт ответ от сервера.
Future<Packet> sendRequest(
int opcode,
Map<dynamic, dynamic> payload,
) {
final seq = _sender.send(_connection, opcode, payload);
return _dispatcher.registerPending(seq).timeout(
ServerConfig.requestTimeout,
onTimeout: () =>
throw TimeoutException('${Opcode.name(opcode)} таймаут'),
);
}
/// Вешает обработчик на пуши с указанным опкодом.
void registerPushHandler(int opcode, void Function(Packet) handler) {
_dispatcher.registerHandler(opcode, handler);
}
/// Стрим всех входящих пушей от сервера.
Stream<Packet> get pushStream => _dispatcher.pushStream;
void dispose() {
_autoReconnect = false;
_reconnectTimer?.cancel();
_cleanup();
_dispatcher.dispose();
_connection.dispose();
_stateController.close();
}
// Внутрянка
void _setSessionState(SessionState state) {
if (_sessionState == state) return;
_sessionState = state;
_stateController.add(state);
logger.i('Сессия: ${state.name}');
}
void _onDataReceived(Uint8List data) {
for (final packet in _receiver.feed(data)) {
_dispatcher.dispatch(packet);
}
}
void _onDisconnected() {
_cleanup();
_setSessionState(SessionState.disconnected);
if (_autoReconnect) _scheduleReconnect();
}
void _cleanup() {
_pingTimer?.cancel();
_dataSubscription?.cancel();
_socketStateSubscription?.cancel();
_dataSubscription = null;
_socketStateSubscription = null;
_receiver.reset();
_dispatcher.clearPending();
}
void _startPinging() {
_pingTimer?.cancel();
_pingTimer = Timer.periodic(ServerConfig.pingInterval, (_) {
if (_connection.isConnected) {
_sender.send(_connection, Opcode.ping, {});
}
});
}
void _scheduleReconnect() {
if (_reconnectAttempts >= ServerConfig.maxReconnectAttempts) {
logger.e('Лимит попыток реконнекта');
return;
}
final delaySec = (2 * (1 << _reconnectAttempts)).clamp(2, 30);
_reconnectAttempts++;
logger.i('Реконнект через ${delaySec}с (попытка $_reconnectAttempts)');
_reconnectTimer?.cancel();
_reconnectTimer = Timer(Duration(seconds: delaySec), connect);
}
}