Files
Qlyra/lib/core/protocol/packet.dart
T

186 lines
5.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:typed_data';
import 'package:dart_lz4/dart_lz4.dart';
import 'package:msgpack_dart/msgpack_dart.dart' as msgpack;
import '../utils/logger.dart';
/// ver(1) + cmd(1) + seq(2) + opcode(2) + packedLen(4) = 10
const int headerSize = 10;
const int _maxDecompressedSize = 1048576; // 1 MB
/// Типы команд в протоколе
abstract class CmdType {
static const int request = 0; // запрос клиента
static const int push = 1; // пуш от сервера
static const int ok = 1; // ответ: ок
static const int notFound = 2; // ответ: не найдено
static const int error = 3; // ответ: ошибка
}
/// Распакованный бинарный пакет
///
/// Формат заголовка (10 байт):
/// ```
/// [0] ver — версия протокола (uint8) (по умолчанию 10)
/// [1] cmd — тип команды (uint8) (при отправке от клиента равно 0)
/// [2..3] seq — порядковый номер (uint16 BE)
/// [4..5] opcode — код операции (uint16 BE)
/// [6..9] packedLen — флаг сжатия [6] + длина payload [7..9] (uint32 BE)
/// [10..] payload — данные в MsgPack, опционально сжатые LZ4
/// ```
class Packet {
int api;
int cmd;
int seq;
int opcode;
dynamic payload;
Packet({
this.api = 10,
this.cmd = 0,
this.seq = 0,
this.opcode = 0,
this.payload,
});
bool get isOk => cmd == CmdType.ok;
bool get isError => cmd == CmdType.error;
bool get isPush => cmd == CmdType.push;
@override
String toString() =>
'Packet(ver=$api cmd=$cmd seq=$seq opcode=$opcode payload=$payload)';
}
/// Упаковка пакета для отправки на сервер
Uint8List packPacket(int opcode, Map<dynamic, dynamic> payload, {int seq = 0}) {
final header = ByteData(headerSize);
header.setUint8(0, 10);
header.setUint8(1, CmdType.request);
header.setUint16(2, seq, Endian.big);
header.setUint16(4, opcode, Endian.big);
final payloadBytes = msgpack.serialize(payload);
final payloadLen = payloadBytes.length & 0xFFFFFF;
header.setUint32(6, payloadLen, Endian.big);
return Uint8List.fromList(header.buffer.asUint8List() + payloadBytes);
}
/// Распаковка пакета от сервера
Packet unpackPacket(Uint8List packet) {
// Для удобства расшифровки пакета переводим в ByteData
ByteData packetData = ByteData.view(
packet.buffer,
packet.offsetInBytes,
packet.lengthInBytes,
);
// Объяснение каждой переменной смотри в классе Packet
// API версия и cmd представляют из себя 8 битные числа
final apiVer = packetData.getUint8(0) & 0xFF;
final cmd = packetData.getUint8(1) & 0xFF;
// Sequence и OPCode представляют из себя 16 битные числа
final seq = packetData.getUint16(2) & 0xFFFF;
final opcode = packetData.getUint16(4) & 0xFFFF;
// После базовых переменных идет длина пакета, является 32 битным числом
final packedLen = packetData.getUint32(6);
// Compression flag показывает, сжат ли payload
final compFlag = packedLen >> 24;
// Длина payload'а
final payloadLength = packedLen & 0xFFFFFF;
// Байты payload'а, могут быть сжаты LZ4
var payloadBytes = packet.buffer.asUint8List(10, payloadLength);
dynamic payload;
print(compFlag);
// Если payload пустой, ничего не делаем (так может быть при получении пинга)
if (payloadBytes.isNotEmpty) {
if (compFlag != 0) {
try {
payloadBytes = lz4Decompress(
payloadBytes,
decompressedSize: _maxDecompressedSize,
);
} catch (_) {
// dart_lz4 не умеет block-формат, фолбэк на ручной декомпрессор
try {
payloadBytes = _lz4BlockDecompress(payloadBytes, _maxDecompressedSize);
} catch (e) {
logger.e("Ошибка декомпрессии LZ4: $e", error: e);
}
}
}
try {
payload = msgpack.deserialize(payloadBytes);
} catch (e) {
logger.e("Ошибка десериализации MsgPack: $e", error: e);
}
}
return Packet(
api: apiVer,
cmd: cmd,
seq: seq,
opcode: opcode,
payload: payload,
);
}
/// LZ4 block декомпрессия (без frame-заголовка).
/// Сервер шлёт именно block-формат, dart_lz4 его не поддерживает.
Uint8List _lz4BlockDecompress(Uint8List src, int maxSize) {
final dst = BytesBuilder(copy: false);
int pos = 0;
while (pos < src.length) {
final token = src[pos++];
var litLen = token >> 4;
if (litLen == 15) {
while (pos < src.length) {
final b = src[pos++];
litLen += b;
if (b != 255) break;
}
}
if (litLen > 0) {
dst.add(src.sublist(pos, pos + litLen));
pos += litLen;
}
if (pos >= src.length) break;
final offset = src[pos] | (src[pos + 1] << 8);
pos += 2;
if (offset == 0) throw StateError('LZ4: offset = 0');
var matchLen = (token & 0x0F) + 4;
if ((token & 0x0F) == 0x0F) {
while (pos < src.length) {
final b = src[pos++];
matchLen += b;
if (b != 255) break;
}
}
final out = dst.toBytes();
final start = out.length - offset;
dst.add(List<int>.generate(matchLen, (i) => out[start + (i % offset)]));
if (dst.length > maxSize) throw StateError('LZ4: превышен лимит');
}
return dst.toBytes();
}