779 lines
19 KiB
JavaScript
779 lines
19 KiB
JavaScript
|
|
/**
|
|||
|
|
* BON (Binary Object Notation) 协议实现
|
|||
|
|
* 基于提供的真实 BON 源码重新实现
|
|||
|
|
*/
|
|||
|
|
import lz4 from 'lz4js';
|
|||
|
|
|
|||
|
|
// -----------------------------
|
|||
|
|
// BON 编解码器核心实现
|
|||
|
|
// -----------------------------
|
|||
|
|
|
|||
|
|
export class Int64 {
|
|||
|
|
constructor(high, low) {
|
|||
|
|
this.high = high;
|
|||
|
|
this.low = low;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export class DataReader {
|
|||
|
|
constructor(bytes) {
|
|||
|
|
this._data = bytes || new Uint8Array(0);
|
|||
|
|
this._view = null;
|
|||
|
|
this.position = 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
get data() { return this._data; }
|
|||
|
|
get dataView() {
|
|||
|
|
return this._view || (this._view = new DataView(this._data.buffer, this._data.byteOffset, this._data.byteLength));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
reset(bytes) {
|
|||
|
|
this._data = bytes;
|
|||
|
|
this.position = 0;
|
|||
|
|
this._view = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
validate(n) {
|
|||
|
|
if (this.position + n > this._data.length) {
|
|||
|
|
console.error('read eof');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
readUInt8() {
|
|||
|
|
if (!this.validate(1)) return;
|
|||
|
|
return this._data[this.position++];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
readInt16() {
|
|||
|
|
if (!this.validate(2)) return;
|
|||
|
|
const v = this._data[this.position++] | (this._data[this.position++] << 8);
|
|||
|
|
return (v << 16) >> 16;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
readInt32() {
|
|||
|
|
if (!this.validate(4)) return;
|
|||
|
|
const v = this._data[this.position++] | (this._data[this.position++] << 8) | (this._data[this.position++] << 16) | (this._data[this.position++] << 24);
|
|||
|
|
return v | 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
readInt64() {
|
|||
|
|
const lo = this.readInt32();
|
|||
|
|
if (lo === undefined) return;
|
|||
|
|
let _lo = lo;
|
|||
|
|
if (_lo < 0) _lo += 0x100000000;
|
|||
|
|
const hi = this.readInt32();
|
|||
|
|
if (hi === undefined) return;
|
|||
|
|
return _lo + 0x100000000 * hi;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
readFloat32() {
|
|||
|
|
if (!this.validate(4)) return;
|
|||
|
|
const v = this.dataView.getFloat32(this.position, true);
|
|||
|
|
this.position += 4;
|
|||
|
|
return v;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
readFloat64() {
|
|||
|
|
if (!this.validate(8)) return;
|
|||
|
|
const v = this.dataView.getFloat64(this.position, true);
|
|||
|
|
this.position += 8;
|
|||
|
|
return v;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
read7BitInt() {
|
|||
|
|
let value = 0;
|
|||
|
|
let shift = 0;
|
|||
|
|
let b = 0;
|
|||
|
|
let count = 0;
|
|||
|
|
do {
|
|||
|
|
if (count++ === 35) throw new Error('Format_Bad7BitInt32');
|
|||
|
|
b = this.readUInt8();
|
|||
|
|
value |= (b & 0x7F) << shift;
|
|||
|
|
shift += 7;
|
|||
|
|
} while ((b & 0x80) !== 0);
|
|||
|
|
return value >>> 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
readUTF() {
|
|||
|
|
const len = this.read7BitInt();
|
|||
|
|
return this.readUTFBytes(len);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
readUint8Array(length, copy = false) {
|
|||
|
|
const start = this.position;
|
|||
|
|
const end = start + length;
|
|||
|
|
const out = copy ? this._data.slice(start, end) : this._data.subarray(start, end);
|
|||
|
|
this.position = end;
|
|||
|
|
return out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
readUTFBytes(length) {
|
|||
|
|
if (length === 0) return '';
|
|||
|
|
if (!this.validate(length)) return;
|
|||
|
|
const str = new TextDecoder('utf8').decode(this._data.subarray(this.position, this.position + length));
|
|||
|
|
this.position += length;
|
|||
|
|
return str;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let _shared = new Uint8Array(524288); // 512 KB initial buffer
|
|||
|
|
|
|||
|
|
export class DataWriter {
|
|||
|
|
constructor() {
|
|||
|
|
this.position = 0;
|
|||
|
|
this._view = null;
|
|||
|
|
this.data = _shared;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
get dataView() {
|
|||
|
|
return this._view || (this._view = new DataView(this.data.buffer, 0, this.data.byteLength));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
reset() {
|
|||
|
|
this.data = _shared;
|
|||
|
|
this._view = null;
|
|||
|
|
this.position = 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ensureBuffer(size) {
|
|||
|
|
if (this.position + size <= _shared.byteLength) return;
|
|||
|
|
const prev = _shared;
|
|||
|
|
const need = this.position + size;
|
|||
|
|
const nextLen = Math.max(Math.floor((_shared.byteLength * 12) / 10), need);
|
|||
|
|
_shared = new Uint8Array(nextLen);
|
|||
|
|
_shared.set(prev, 0);
|
|||
|
|
this.data = _shared;
|
|||
|
|
this._view = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
writeInt8(v) {
|
|||
|
|
this.ensureBuffer(1);
|
|||
|
|
this.data[this.position++] = v | 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
writeInt16(v) {
|
|||
|
|
this.ensureBuffer(2);
|
|||
|
|
this.data[this.position++] = v | 0;
|
|||
|
|
this.data[this.position++] = (v >> 8) & 0xFF;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
writeInt32(v) {
|
|||
|
|
this.ensureBuffer(4);
|
|||
|
|
this.data[this.position++] = v | 0;
|
|||
|
|
this.data[this.position++] = (v >> 8) & 0xFF;
|
|||
|
|
this.data[this.position++] = (v >> 16) & 0xFF;
|
|||
|
|
this.data[this.position++] = (v >> 24) & 0xFF;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
writeInt64(v) {
|
|||
|
|
this.writeInt32(v);
|
|||
|
|
if (v < 0) {
|
|||
|
|
this.writeInt32(~Math.floor((-v) / 0x100000000));
|
|||
|
|
} else {
|
|||
|
|
this.writeInt32(Math.floor(v / 0x100000000) | 0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
writeFloat32(v) {
|
|||
|
|
this.ensureBuffer(4);
|
|||
|
|
this.dataView.setFloat32(this.position, v, true);
|
|||
|
|
this.position += 4;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
writeFloat64(v) {
|
|||
|
|
this.ensureBuffer(8);
|
|||
|
|
this.dataView.setFloat64(this.position, v, true);
|
|||
|
|
this.position += 8;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_write7BitInt(v) {
|
|||
|
|
let n = v >>> 0;
|
|||
|
|
while (n >= 0x80) {
|
|||
|
|
this.data[this.position++] = (n & 0xFF) | 0x80;
|
|||
|
|
n >>>= 7;
|
|||
|
|
}
|
|||
|
|
this.data[this.position++] = n & 0x7F;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
write7BitInt(v) {
|
|||
|
|
this.ensureBuffer(5);
|
|||
|
|
this._write7BitInt(v);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_7BitIntLen(v) {
|
|||
|
|
return v < 0 ? 5
|
|||
|
|
: v < 0x80 ? 1
|
|||
|
|
: v < 0x4000 ? 2
|
|||
|
|
: v < 0x200000 ? 3
|
|||
|
|
: v < 0x10000000 ? 4
|
|||
|
|
: 5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
writeUTF(str) {
|
|||
|
|
const t = str.length;
|
|||
|
|
if (t === 0) {
|
|||
|
|
this.write7BitInt(0);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const max = 6 * t;
|
|||
|
|
this.ensureBuffer(5 + max);
|
|||
|
|
const start = this.position;
|
|||
|
|
this.position += this._7BitIntLen(max);
|
|||
|
|
const from = this.position;
|
|||
|
|
const reserved = from - start;
|
|||
|
|
|
|||
|
|
const encoder = new TextEncoder();
|
|||
|
|
const { written } = encoder.encodeInto(str, this.data.subarray(this.position));
|
|||
|
|
this.position += written;
|
|||
|
|
const after = this.position;
|
|||
|
|
const size = after - from;
|
|||
|
|
|
|||
|
|
this.position = start;
|
|||
|
|
this._write7BitInt(size);
|
|||
|
|
const used = this.position - start;
|
|||
|
|
if (used !== reserved) {
|
|||
|
|
this.data.copyWithin(from + (used - reserved), from, after);
|
|||
|
|
}
|
|||
|
|
this.position = from + size + (used - reserved);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
writeUint8Array(src, offset = 0, length) {
|
|||
|
|
const start = offset | 0;
|
|||
|
|
const end = Math.min(src.byteLength, start + (length ?? src.byteLength));
|
|||
|
|
const n = end - start;
|
|||
|
|
if (n <= 0) return;
|
|||
|
|
this.ensureBuffer(n);
|
|||
|
|
this.data.set(src.subarray(start, end), this.position);
|
|||
|
|
this.position += n;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
writeUTFBytes(str) {
|
|||
|
|
this.ensureBuffer(6 * str.length);
|
|||
|
|
const encoder = new TextEncoder();
|
|||
|
|
const { written } = encoder.encodeInto(str, this.data.subarray(this.position));
|
|||
|
|
this.position += written;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getBytes(clone = false) {
|
|||
|
|
return clone ? this.data.slice(0, this.position) : this.data.subarray(0, this.position);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export class BonEncoder {
|
|||
|
|
constructor() {
|
|||
|
|
this.dw = new DataWriter();
|
|||
|
|
this.strMap = new Map();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
reset() {
|
|||
|
|
this.dw.reset();
|
|||
|
|
this.strMap.clear();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
encodeInt(v) {
|
|||
|
|
this.dw.writeInt8(1);
|
|||
|
|
this.dw.writeInt32(v | 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
encodeLong(v) {
|
|||
|
|
this.dw.writeInt8(2);
|
|||
|
|
if (typeof v === 'number') {
|
|||
|
|
this.dw.writeInt64(v);
|
|||
|
|
} else {
|
|||
|
|
this.dw.writeInt32(v.low | 0);
|
|||
|
|
this.dw.writeInt32(v.high | 0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
encodeFloat(v) {
|
|||
|
|
this.dw.writeInt8(3);
|
|||
|
|
this.dw.writeFloat32(v);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
encodeDouble(v) {
|
|||
|
|
this.dw.writeInt8(4);
|
|||
|
|
this.dw.writeFloat64(v);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
encodeNumber(v) {
|
|||
|
|
if ((v | 0) === v) this.encodeInt(v);
|
|||
|
|
else if (Math.floor(v) === v) this.encodeLong(v);
|
|||
|
|
else this.encodeDouble(v);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
encodeString(s) {
|
|||
|
|
const hit = this.strMap.get(s);
|
|||
|
|
if (hit !== undefined) {
|
|||
|
|
this.dw.writeInt8(99); // StringRef
|
|||
|
|
this.dw.write7BitInt(hit);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
this.dw.writeInt8(5); // String
|
|||
|
|
this.dw.writeUTF(s);
|
|||
|
|
this.strMap.set(s, this.strMap.size);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
encodeBoolean(b) {
|
|||
|
|
this.dw.writeInt8(6);
|
|||
|
|
this.dw.writeInt8(b ? 1 : 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
encodeNull() {
|
|||
|
|
this.dw.writeInt8(0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
encodeDateTime(d) {
|
|||
|
|
this.dw.writeInt8(10);
|
|||
|
|
this.dw.writeInt64(d.getTime());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
encodeBinary(u8) {
|
|||
|
|
this.dw.writeInt8(7);
|
|||
|
|
this.dw.write7BitInt(u8.byteLength);
|
|||
|
|
this.dw.writeUint8Array(u8);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
encodeArray(arr) {
|
|||
|
|
this.dw.writeInt8(9);
|
|||
|
|
this.dw.write7BitInt(arr.length);
|
|||
|
|
for (let i = 0; i < arr.length; i++) this.encode(arr[i]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
encodeMap(mp) {
|
|||
|
|
this.dw.writeInt8(8);
|
|||
|
|
this.dw.write7BitInt(mp.size);
|
|||
|
|
mp.forEach((v, k) => {
|
|||
|
|
this.encode(k);
|
|||
|
|
this.encode(v);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
encodeObject(obj) {
|
|||
|
|
this.dw.writeInt8(8);
|
|||
|
|
const keys = [];
|
|||
|
|
for (const k in obj) {
|
|||
|
|
if (!Object.prototype.hasOwnProperty.call(obj, k)) continue;
|
|||
|
|
if (k.startsWith('_')) continue;
|
|||
|
|
const type = typeof obj[k];
|
|||
|
|
if (type === 'function' || type === 'undefined') continue;
|
|||
|
|
keys.push(k);
|
|||
|
|
}
|
|||
|
|
this.dw.write7BitInt(keys.length);
|
|||
|
|
for (const k of keys) {
|
|||
|
|
this.encode(k);
|
|||
|
|
this.encode(obj[k]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
encode(v) {
|
|||
|
|
if (v == null) {
|
|||
|
|
this.encodeNull();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
switch (v.constructor) {
|
|||
|
|
case Number:
|
|||
|
|
this.encodeNumber(v);
|
|||
|
|
return;
|
|||
|
|
case Boolean:
|
|||
|
|
this.encodeBoolean(v);
|
|||
|
|
return;
|
|||
|
|
case String:
|
|||
|
|
this.encodeString(v);
|
|||
|
|
return;
|
|||
|
|
case Int64:
|
|||
|
|
this.encodeLong(v);
|
|||
|
|
return;
|
|||
|
|
case Array:
|
|||
|
|
this.encodeArray(v);
|
|||
|
|
return;
|
|||
|
|
case Map:
|
|||
|
|
this.encodeMap(v);
|
|||
|
|
return;
|
|||
|
|
case Date:
|
|||
|
|
this.encodeDateTime(v);
|
|||
|
|
return;
|
|||
|
|
case Uint8Array:
|
|||
|
|
this.encodeBinary(v);
|
|||
|
|
return;
|
|||
|
|
default:
|
|||
|
|
if (typeof v !== 'object') {
|
|||
|
|
this.encodeNull();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
this.encodeObject(v);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getBytes(clone = false) {
|
|||
|
|
return this.dw.getBytes(clone);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export class BonDecoder {
|
|||
|
|
constructor() {
|
|||
|
|
this.dr = new DataReader(new Uint8Array(0));
|
|||
|
|
this.strArr = [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
reset(bytes) {
|
|||
|
|
this.dr.reset(bytes);
|
|||
|
|
this.strArr.length = 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
decode() {
|
|||
|
|
const tag = this.dr.readUInt8();
|
|||
|
|
switch (tag) {
|
|||
|
|
default:
|
|||
|
|
return null;
|
|||
|
|
case 1:
|
|||
|
|
return this.dr.readInt32();
|
|||
|
|
case 2:
|
|||
|
|
return this.dr.readInt64();
|
|||
|
|
case 3:
|
|||
|
|
return this.dr.readFloat32();
|
|||
|
|
case 4:
|
|||
|
|
return this.dr.readFloat64();
|
|||
|
|
case 5: {
|
|||
|
|
const s = this.dr.readUTF();
|
|||
|
|
this.strArr.push(s);
|
|||
|
|
return s;
|
|||
|
|
}
|
|||
|
|
case 6:
|
|||
|
|
return this.dr.readUInt8() === 1;
|
|||
|
|
case 7: {
|
|||
|
|
const len = this.dr.read7BitInt();
|
|||
|
|
return this.dr.readUint8Array(len, false);
|
|||
|
|
}
|
|||
|
|
case 8: {
|
|||
|
|
const count = this.dr.read7BitInt();
|
|||
|
|
const obj = {};
|
|||
|
|
for (let i = 0; i < count; i++) {
|
|||
|
|
const k = this.decode();
|
|||
|
|
const v = this.decode();
|
|||
|
|
obj[k] = v;
|
|||
|
|
}
|
|||
|
|
return obj;
|
|||
|
|
}
|
|||
|
|
case 9: {
|
|||
|
|
const len = this.dr.read7BitInt();
|
|||
|
|
const arr = new Array(len);
|
|||
|
|
for (let i = 0; i < len; i++) arr[i] = this.decode();
|
|||
|
|
return arr;
|
|||
|
|
}
|
|||
|
|
case 10:
|
|||
|
|
return new Date(this.dr.readInt64());
|
|||
|
|
case 99:
|
|||
|
|
return this.strArr[this.dr.read7BitInt()];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 单例实例
|
|||
|
|
const _enc = new BonEncoder();
|
|||
|
|
const _dec = new BonDecoder();
|
|||
|
|
|
|||
|
|
// BON 编解码函数
|
|||
|
|
export const bon = {
|
|||
|
|
encode: (value, clone = true) => {
|
|||
|
|
_enc.reset();
|
|||
|
|
_enc.encode(value);
|
|||
|
|
return _enc.getBytes(clone);
|
|||
|
|
},
|
|||
|
|
decode: (bytes) => {
|
|||
|
|
_dec.reset(bytes);
|
|||
|
|
return _dec.decode();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/** —— 协议消息包装,与原 ProtoMsg 类等价 —— */
|
|||
|
|
export class ProtoMsg {
|
|||
|
|
constructor(raw) {
|
|||
|
|
if (raw?.cmd) {
|
|||
|
|
raw.cmd = raw.cmd.toLowerCase();
|
|||
|
|
}
|
|||
|
|
this._raw = raw;
|
|||
|
|
this._rawData = undefined;
|
|||
|
|
this._data = undefined;
|
|||
|
|
this._t = undefined;
|
|||
|
|
this._sendMsg = undefined;
|
|||
|
|
this.rtt = 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
get sendMsg() { return this._sendMsg; }
|
|||
|
|
get seq() { return this._raw.seq; }
|
|||
|
|
get resp() { return this._raw.resp; }
|
|||
|
|
get ack() { return this._raw.ack; }
|
|||
|
|
get cmd() { return this._raw?.cmd && this._raw?.cmd.toLowerCase(); }
|
|||
|
|
get code() { return ~~this._raw.code; }
|
|||
|
|
get error() { return this._raw.error; }
|
|||
|
|
get time() { return this._raw.time; }
|
|||
|
|
get body() { return this._raw.body; }
|
|||
|
|
|
|||
|
|
/** 惰性 decode body → rawData(bon.decode) */
|
|||
|
|
get rawData() {
|
|||
|
|
if (this._rawData !== undefined || this.body === undefined) return this._rawData;
|
|||
|
|
this._rawData = bon.decode(this.body);
|
|||
|
|
return this._rawData;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 指定数据类型 */
|
|||
|
|
setDataType(t) {
|
|||
|
|
if (t) this._t = { name: t.name ?? 'Anonymous', ctor: t };
|
|||
|
|
return this;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 配置"请求"对象,让 respType 自动对齐 */
|
|||
|
|
setSendMsg(msg) {
|
|||
|
|
this._sendMsg = msg;
|
|||
|
|
return this.setDataType(msg.respType);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 将 rawData 反序列化为业务对象 */
|
|||
|
|
getData(clazz) {
|
|||
|
|
if (this._data !== undefined || this.rawData === undefined) return this._data;
|
|||
|
|
|
|||
|
|
let t = this._t;
|
|||
|
|
if (clazz && t && clazz !== t.ctor) {
|
|||
|
|
console.warn(`getData type not match, ${clazz.name} != ${t.name}`);
|
|||
|
|
t = { name: clazz.name, ctor: clazz };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this._data = this.rawData;
|
|||
|
|
return this._data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
toLogString() {
|
|||
|
|
const e = { ...this._raw };
|
|||
|
|
delete e.body;
|
|||
|
|
e.data = this.rawData;
|
|||
|
|
e.rtt = this.rtt;
|
|||
|
|
return JSON.stringify(e);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** —— 加解密器注册表 —— */
|
|||
|
|
const registry = new Map();
|
|||
|
|
|
|||
|
|
/** lz4 + 头部掩码的 "lx" 方案 */
|
|||
|
|
const lx = {
|
|||
|
|
encrypt: (buf) => {
|
|||
|
|
let e = lz4.compress(buf);
|
|||
|
|
const t = 2 + ~~(Math.random() * 248);
|
|||
|
|
for (let n = Math.min(e.length, 100); --n >= 0; ) e[n] ^= t;
|
|||
|
|
|
|||
|
|
// 写入标识与混淆位
|
|||
|
|
e[0] = 112; e[1] = 108;
|
|||
|
|
e[2] = (e[2] & 0b10101010) | ((t >> 7 & 1) << 6) | ((t >> 6 & 1) << 4) | ((t >> 5 & 1) << 2) | (t >> 4 & 1);
|
|||
|
|
e[3] = (e[3] & 0b10101010) | ((t >> 3 & 1) << 6) | ((t >> 2 & 1) << 4) | ((t >> 1 & 1) << 2) | (t & 1);
|
|||
|
|
return e;
|
|||
|
|
},
|
|||
|
|
decrypt: (e) => {
|
|||
|
|
const t =
|
|||
|
|
((e[2] >> 6 & 1) << 7) | ((e[2] >> 4 & 1) << 6) | ((e[2] >> 2 & 1) << 5) | ((e[2] & 1) << 4) |
|
|||
|
|
((e[3] >> 6 & 1) << 3) | ((e[3] >> 4 & 1) << 2) | ((e[3] >> 2 & 1) << 1) | (e[3] & 1);
|
|||
|
|
for (let n = Math.min(100, e.length); --n >= 2; ) e[n] ^= t;
|
|||
|
|
e[0] = 4; e[1] = 34; e[2] = 77; e[3] = 24; // 还原头以便 lz4 解
|
|||
|
|
return lz4.decompress(e);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/** 随机首 4 字节 + XOR 的 "x" 方案 */
|
|||
|
|
const x = {
|
|||
|
|
encrypt: (e) => {
|
|||
|
|
const rnd = ~~(Math.random() * 0xFFFFFFFF) >>> 0;
|
|||
|
|
const n = new Uint8Array(e.length + 4);
|
|||
|
|
n[0] = rnd & 0xFF; n[1] = (rnd >>> 8) & 0xFF; n[2] = (rnd >>> 16) & 0xFF; n[3] = (rnd >>> 24) & 0xFF;
|
|||
|
|
n.set(e, 4);
|
|||
|
|
const r = 2 + ~~(Math.random() * 248);
|
|||
|
|
for (let i = n.length; --i >= 0; ) n[i] ^= r;
|
|||
|
|
n[0] = 112; n[1] = 120;
|
|||
|
|
n[2] = (n[2] & 0b10101010) | ((r >> 7 & 1) << 6) | ((r >> 6 & 1) << 4) | ((r >> 5 & 1) << 2) | (r >> 4 & 1);
|
|||
|
|
n[3] = (n[3] & 0b10101010) | ((r >> 3 & 1) << 6) | ((r >> 2 & 1) << 4) | ((r >> 1 & 1) << 2) | (r & 1);
|
|||
|
|
return n;
|
|||
|
|
},
|
|||
|
|
decrypt: (e) => {
|
|||
|
|
const t =
|
|||
|
|
((e[2] >> 6 & 1) << 7) | ((e[2] >> 4 & 1) << 6) | ((e[2] >> 2 & 1) << 5) | ((e[2] & 1) << 4) |
|
|||
|
|
((e[3] >> 6 & 1) << 3) | ((e[3] >> 4 & 1) << 2) | ((e[3] >> 2 & 1) << 1) | (e[3] & 1);
|
|||
|
|
for (let n = e.length; --n >= 4; ) e[n] ^= t;
|
|||
|
|
return e.subarray(4);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/** 依赖 globalThis.XXTEA 的 "xtm" 方案 */
|
|||
|
|
const xtm = {
|
|||
|
|
encrypt: (e) => globalThis.XXTEA ? globalThis.XXTEA.encryptMod({ data: e.buffer, length: e.length }) : e,
|
|||
|
|
decrypt: (e) => globalThis.XXTEA ? globalThis.XXTEA.decryptMod({ data: e.buffer, length: e.length }) : e,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/** 注册器 */
|
|||
|
|
function register(name, impl) {
|
|||
|
|
registry.set(name, impl);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
register('lx', lx);
|
|||
|
|
register('x', x);
|
|||
|
|
register('xtm', xtm);
|
|||
|
|
|
|||
|
|
/** 默认使用 x 加密(自动检测解密) */
|
|||
|
|
const passthrough = {
|
|||
|
|
encrypt: (e) => getEnc('x').encrypt(e),
|
|||
|
|
decrypt: (e) => {
|
|||
|
|
if (e.length > 4 && e[0] === 112 && e[1] === 108) e = getEnc('lx').decrypt(e);
|
|||
|
|
else if (e.length > 4 && e[0] === 112 && e[1] === 120) e = getEnc('x').decrypt(e);
|
|||
|
|
else if (e.length > 3 && e[0] === 112 && e[1] === 116) e = getEnc('xtm').decrypt(e);
|
|||
|
|
return e;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/** 对外:按名称取加解密器;找不到则用默认 */
|
|||
|
|
export function getEnc(name) {
|
|||
|
|
return registry.get(name) ?? passthrough;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 对外:encode(bon.encode → 加密) */
|
|||
|
|
export function encode(obj, enc) {
|
|||
|
|
let bytes = bon.encode(obj, false);
|
|||
|
|
const out = enc.encrypt(bytes);
|
|||
|
|
return out.buffer.byteLength === out.length ? out.buffer : out.buffer.slice(0, out.length);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 对外:parse(解密 → bon.decode → ProtoMsg) */
|
|||
|
|
export function parse(buf, enc) {
|
|||
|
|
const u8 = new Uint8Array(buf);
|
|||
|
|
const plain = enc.decrypt(u8);
|
|||
|
|
const raw = bon.decode(plain);
|
|||
|
|
return new ProtoMsg(raw);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 游戏消息模板
|
|||
|
|
export const GameMessages = {
|
|||
|
|
// 心跳消息
|
|||
|
|
heartBeat: (ack = 0, seq = 0) => ({
|
|||
|
|
ack,
|
|||
|
|
body: undefined,
|
|||
|
|
c: undefined,
|
|||
|
|
cmd: "_sys/ack",
|
|||
|
|
hint: undefined,
|
|||
|
|
seq,
|
|||
|
|
time: Date.now()
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
// 获取角色信息
|
|||
|
|
getRoleInfo: (ack = 0, seq = 0, params = {}) => ({
|
|||
|
|
cmd: "role_getroleinfo",
|
|||
|
|
body: encode({
|
|||
|
|
clientVersion: "1.65.3-wx",
|
|||
|
|
inviteUid: 0,
|
|||
|
|
platform: "hortor",
|
|||
|
|
platformExt: "mix",
|
|||
|
|
scene: "",
|
|||
|
|
...params
|
|||
|
|
}, getEnc('x')),
|
|||
|
|
ack: ack || 0,
|
|||
|
|
seq: seq || 0,
|
|||
|
|
time: Date.now()
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
// 获取数据包版本
|
|||
|
|
getDataBundleVer: (ack = 0, seq = 0, params = {}) => ({
|
|||
|
|
cmd: "system_getdatabundlever",
|
|||
|
|
body: encode({
|
|||
|
|
isAudit: false,
|
|||
|
|
...params
|
|||
|
|
}, getEnc('x')),
|
|||
|
|
ack: ack || 0,
|
|||
|
|
seq: seq || 0,
|
|||
|
|
time: Date.now()
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
// 购买金币
|
|||
|
|
buyGold: (ack = 0, seq = 0, params = {}) => ({
|
|||
|
|
ack,
|
|||
|
|
body: encode({
|
|||
|
|
buyNum: 1,
|
|||
|
|
...params
|
|||
|
|
}, getEnc('x')),
|
|||
|
|
cmd: "system_buygold",
|
|||
|
|
seq,
|
|||
|
|
time: Date.now()
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
// 签到奖励
|
|||
|
|
signInReward: (ack = 0, seq = 0, params = {}) => ({
|
|||
|
|
ack,
|
|||
|
|
body: encode({
|
|||
|
|
...params
|
|||
|
|
}, getEnc('x')),
|
|||
|
|
cmd: "system_signinreward",
|
|||
|
|
seq,
|
|||
|
|
time: Date.now()
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
// 领取每日任务奖励
|
|||
|
|
claimDailyReward: (ack = 0, seq = 0, params = {}) => ({
|
|||
|
|
ack,
|
|||
|
|
body: encode({
|
|||
|
|
rewardId: 0,
|
|||
|
|
...params
|
|||
|
|
}, getEnc('x')),
|
|||
|
|
cmd: "task_claimdailyreward",
|
|||
|
|
seq,
|
|||
|
|
time: Date.now()
|
|||
|
|
})
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 创建全局实例
|
|||
|
|
export const g_utils = {
|
|||
|
|
getEnc,
|
|||
|
|
encode: (obj, encName = 'x') => encode(obj, getEnc(encName)),
|
|||
|
|
parse: (data, encName = 'auto') => parse(data, getEnc(encName)),
|
|||
|
|
bon // 添加BON编解码器
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 兼容性导出(保持旧的接口)
|
|||
|
|
export const bonProtocol = {
|
|||
|
|
encode: bon.encode,
|
|||
|
|
decode: bon.decode,
|
|||
|
|
createMessage: (cmd, body = {}, ack = 0, seq = 0, options = {}) => ({
|
|||
|
|
cmd,
|
|||
|
|
body: bon.encode(body),
|
|||
|
|
ack: ack || 0,
|
|||
|
|
seq: seq || 0,
|
|||
|
|
time: Date.now(),
|
|||
|
|
...options
|
|||
|
|
}),
|
|||
|
|
parseMessage: (messageData) => {
|
|||
|
|
try {
|
|||
|
|
let message;
|
|||
|
|
if (typeof messageData === 'string') {
|
|||
|
|
message = JSON.parse(messageData);
|
|||
|
|
} else {
|
|||
|
|
message = messageData;
|
|||
|
|
}
|
|||
|
|
if (message.body && (message.body instanceof ArrayBuffer || message.body instanceof Uint8Array)) {
|
|||
|
|
message.body = bon.decode(message.body);
|
|||
|
|
}
|
|||
|
|
return message;
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('消息解析失败:', error);
|
|||
|
|
return {
|
|||
|
|
error: true,
|
|||
|
|
message: '消息解析失败',
|
|||
|
|
originalData: messageData
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
generateSeq: () => Math.floor(Math.random() * 1000000),
|
|||
|
|
generateMessageId: () => 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 导出单独的加密器类以兼容测试文件
|
|||
|
|
export const LXCrypto = lx;
|
|||
|
|
export const XCrypto = x;
|
|||
|
|
export const XTMCrypto = xtm;
|
|||
|
|
|
|||
|
|
export default { ProtoMsg, getEnc, encode, parse, GameMessages, g_utils, bon, bonProtocol };
|