为了学术兴趣,我正在开发自己的类似于 GRPC 的协议。我选择 NodejS 作为语言,因为它对我来说做很多事情既简单又愉快。
我自己开发了数据格式,准确来说是这样的
0x01 - 请求 0x02 - 答案
包含要调用的方法名称的字符串的长度。
包含要调用的方法名称的字符串。
表示参数数量的整数值。
取决于参数类型。
我开发了序列化/反序列化算法,但是反序列化抛出错误
这是我要序列化的数据结构
{
"messageType": "request",
"callMethod": "PetService.createPet",
"parameters": [
{
"id": 12,
"name": "john",
"owner": {
"id": 1,
"name": "beria"
}
},
14
]
}
这是我指定字节的枚举代码
export enum MessageTypeBytes {
REQUEST = 0x01,
RESPONSE = 0x02,
}
export enum DataTypeBytes {
INTEGER = 0x01,
STRING = 0x02,
BOOLEAN = 0x03,
OBJECT = 0x04,
}
export enum ServiceBytes {
EOL = 0xFF,
}
这就是序列化算法
import { Schema, MessageType, MessageTypeBytes, DataTypeBytes } from "@koda-rpc/common";
import { match } from "ts-pattern";
import { validateParams } from "./validation";
export interface ISerializeOptions {
callMethod: string;
messageType: MessageType;
parameters: Array<unknown>;
schema: Schema;
}
export const serialize = async ({
callMethod,
messageType,
parameters,
schema,
}: ISerializeOptions) => {
console.log(JSON.stringify({
messageType,
callMethod,
parameters,
}, null, 2));
await validateParams(
parameters,
schema,
callMethod,
);
let buffer: Buffer = Buffer.alloc(0);
// Message type
let msgTypeByte = match<MessageType>(messageType)
.with(MessageType.REQUEST, () => MessageTypeBytes.REQUEST)
.with(MessageType.RESPONSE, () => MessageTypeBytes.RESPONSE)
.exhaustive();
buffer = Buffer.concat([buffer, Buffer.from([msgTypeByte])]);
// Method length
const methodLengthBuffer = Buffer.alloc(1);
methodLengthBuffer.writeUint8(callMethod.length);
buffer = Buffer.concat([buffer, methodLengthBuffer]);
// Called method
buffer = Buffer.concat([buffer, Buffer.from(callMethod)]);
// Parameters count (in symbols)
const paramsCountBuffer = Buffer.alloc(1);
paramsCountBuffer.writeUint8(parameters.length);
buffer = Buffer.concat([buffer, paramsCountBuffer]);
// Parameters
parameters.forEach(parameter => {
if (typeof parameter === 'number') {
buffer = Buffer.concat([buffer, Buffer.from([DataTypeBytes.NUMBER])]);
const numberValueBuffer = Buffer.alloc(4);
numberValueBuffer.writeUint32LE(parameter);
buffer = Buffer.concat([buffer, numberValueBuffer])
} else if (typeof parameter === 'string') {
buffer = Buffer.concat([buffer, Buffer.from([DataTypeBytes.STRING])]);
const stringLengthBuffer = Buffer.alloc(1);
stringLengthBuffer.writeUInt8(parameter.length);
buffer = Buffer.concat([buffer, stringLengthBuffer]);
buffer = Buffer.concat([buffer, Buffer.from(parameter)]);
} else if (typeof parameter === 'boolean') {
buffer = Buffer.concat([buffer, Buffer.from([DataTypeBytes.BOOLEAN])]);
const boolValueBuffer = Buffer.alloc(1);
boolValueBuffer.writeUInt8(parameter ? 0x01 : 0x00);
buffer = Buffer.concat([buffer, boolValueBuffer]);
} else if (typeof parameter === 'object') {
buffer = Buffer.concat([buffer, Buffer.from([DataTypeBytes.OBJECT])]);
buffer = Buffer.concat([buffer, serializeObject(parameter)]);
} else {
throw new Error(`Unsupported parameter type: ${typeof parameter}`);
}
});
return buffer;
};
const serializeObject = (obj: object): Buffer => {
let buffer = Buffer.alloc(0);
Object.entries(obj).forEach(([key, value]) => {
// Длина ключа
const keyLengthBuffer = Buffer.alloc(1);
keyLengthBuffer.writeUInt8(key.length);
buffer = Buffer.concat([buffer, keyLengthBuffer]);
// Ключ
buffer = Buffer.concat([buffer, Buffer.from(key)]);
if (typeof value === 'number') {
buffer = Buffer.concat([buffer, Buffer.from([DataTypeBytes.NUMBER])]);
const numberValueBuffer = Buffer.alloc(4);
numberValueBuffer.writeInt32LE(value);
buffer = Buffer.concat([buffer, numberValueBuffer]);
} else if (typeof value === 'string') {
buffer = Buffer.concat([buffer, Buffer.from([DataTypeBytes.STRING])]);
const stringLengthBuffer = Buffer.alloc(1);
stringLengthBuffer.writeUInt8(value.length);
buffer = Buffer.concat([buffer, stringLengthBuffer]);
buffer = Buffer.concat([buffer, Buffer.from(value)]);
} else if (typeof value === 'boolean') {
buffer = Buffer.concat([buffer, Buffer.from([DataTypeBytes.BOOLEAN])]);
const boolValueBuffer = Buffer.alloc(1);
boolValueBuffer.writeUInt8(value ? 0x01 : 0x00);
buffer = Buffer.concat([buffer, boolValueBuffer]);
} else if (typeof value === 'object') {
buffer = Buffer.concat([buffer, Buffer.from([DataTypeBytes.OBJECT])]);
buffer = Buffer.concat([buffer, serializeObject(value)]);
} else {
throw new Error(`Unsupported value type: ${typeof value}`);
}
});
return buffer;
}
这是一个反序列化算法
import { DataTypeBytes, MessageType, MessageTypeBytes, Schema } from "@koda-rpc/common";
import { validateParams } from "./validation";
import { match } from "ts-pattern";
import { IDeserializedData } from "./types";
interface IDeserializeOptions {
buffer: Buffer;
messageType: MessageType;
schema: Schema;
}
export const deserialize = async ({
buffer,
schema,
}: IDeserializeOptions): Promise<IDeserializedData> => {
// Распаковываем данные с использованием zlib
const uncompressedBuffer = buffer;
let index = 0;
// Читаем тип сообщения
const msgTypeByte = uncompressedBuffer[index++] as MessageTypeBytes;
const msgType = match<MessageTypeBytes>(msgTypeByte)
.with(MessageTypeBytes.REQUEST, () => MessageType.REQUEST)
.with(MessageTypeBytes.RESPONSE, () => MessageType.RESPONSE)
.otherwise(() => '');
// Читаем длину метода
const methodLength = uncompressedBuffer[index++];
// Читаем вызываемый метод
const callMethod = uncompressedBuffer.slice(index, index + methodLength).toString();
index += methodLength;
// Читаем количество параметров
const parametersCount = uncompressedBuffer[index++];
const parameters: unknown[] = [];
// Читаем параметры
for (let i = 0; i < parametersCount; i++) {
// Читаем тип параметра
const parameterType = uncompressedBuffer[index++];
if (parameterType === DataTypeBytes.NUMBER) {
const numberValue = uncompressedBuffer.readInt32LE(index);
parameters.push(numberValue);
index += 4;
} else if (parameterType === DataTypeBytes.STRING) {
const stringLength = uncompressedBuffer[index++];
const stringValue = uncompressedBuffer.slice(index, index + stringLength).toString();
parameters.push(stringValue);
index += stringLength;
} else if (parameterType === DataTypeBytes.BOOLEAN) {
const booleanValue = uncompressedBuffer[index++] === 0x01;
parameters.push(booleanValue);
} else if (parameterType === DataTypeBytes.OBJECT) {
const { parsedObject, newIndex } = deserializeObject(uncompressedBuffer.slice(index));
parameters.push(parsedObject);
index += newIndex;
} else {
throw new Error(`Unsupported parameter type: ${parameterType}`);
}
}
// Проверяем наличие завершающего байта
const endByte = uncompressedBuffer[index];
if (endByte !== 0xFF) {
throw new Error('Invalid end byte');
}
await validateParams(
parameters,
schema,
callMethod,
);
return {
messageType: msgType as MessageType,
callMethod,
parameters,
};
}
const deserializeObject = (buffer: Buffer): { parsedObject: object; newIndex: number } => {
let index = 0;
const parsedObject: { [key: string]: any } = {};
while (index < buffer.length) {
// Читаем длину ключа
const keyLength = buffer[index++];
// Читаем ключ
const key = buffer.slice(index, index + keyLength).toString();
index += keyLength;
// Читаем тип значения
const valueType = buffer[index++];
if (valueType === DataTypeBytes.NUMBER) {
const numberValue = buffer.readInt32LE(index);
parsedObject[key] = numberValue;
index += 4;
} else if (valueType === DataTypeBytes.STRING) {
const stringLength = buffer[index++];
const stringValue = buffer.slice(index, index + stringLength).toString();
parsedObject[key] = stringValue;
index += stringLength;
} else if (valueType === DataTypeBytes.BOOLEAN) {
const booleanValue = buffer[index++] === 0x01;
parsedObject[key] = booleanValue;
} else if (valueType === DataTypeBytes.OBJECT) {
const { parsedObject: nestedObject, newIndex } = deserializeObject(buffer.slice(index));
parsedObject[key] = nestedObject;
index += newIndex;
} else {
throw new Error(`Unsupported value type: ${valueType}`);
}
}
return { parsedObject, newIndex: index };
}
序列化效果很好,产生这种格式
01145065 74536572 76696365 2E637265 61746550 65740204 02696400 0C000000 046E616D 6502046A 6F686E05 6F776E65 72040269 64000100 0000046E 616D6502 05626572 6961000E 000000
但是反序列化算法会抛出错误
Error: Unsupported value type: 0
我对二进制格式的世界相当陌生,所以请帮助我。我只是无法取得工作成果。如果你能准确地告诉我在哪里以及如何修复它,那就太好了 =(
在实际的序列化输出中,与您在问题中提到的内容略有不同:
01 14 50 65 74 53 65 72 76 69 63 65 2e 63 72 65 61 74 65 50 65 74 02 04 02 69 64 00 01 0c 00 00 00 04 6e 61 6d 65 02 04 6a 6f 68 6e 05 6f 77 6e 65 72 04 02 69 64 00 01 01 00 00 00 04 6e 61 6d 65 02 05 62 65 72 69 61 01 0e 00 00 00
。
根据您提供的输出,原因是这只是一个不正确的序列化,因为整数值类型使用
00
而不是 01
。
在实际输出中,问题源于缺乏对象框架机制,例如键计数前缀或边界标记后缀,如
ServiceBytes.EOL
(您没有使用),但针对对象。换句话说,即使遇到最后一个键 deserializeObject
,01 0e 00 00 00
函数也会继续将尾部 [0].owner.name
解析为对象。这导致 0e
被解释为键,00
被解释为值类型。