-
Notifications
You must be signed in to change notification settings - Fork 4
Description
[toc]
VS Code 消息协议
VS Code 的消息协议定义在以下两个文件:
vs/base/parts/ipc/common/ipc.ts
vs/base/parts/ipc/common/ipc.net.ts
协议是基于二进制的。
ipc.ts
定义承载具体数据和操作的消息协议,最初只用于桌面 VS Code 的进程通信, 为了方便描述,这里的消息称为 基础消息
.
ipc.net.ts
定义通过网络(WebSocket)通信的消息协议, 用于 Web 版VS Code, 为了方便描述,这里的消息称为 网路消息
.
除了特有的消息类型,网络消息
是在 基础消息
前加上自定义网络消息头,即 网络消息 = 网络消息头 + 基础消息
。
基础消息协议
序列化和反序列
基础消息
支持多种类型的数据: undefined, Array、object、Int、String、Buffer.
// vs/base/parts/ipc/common/ipc.ts
enum DataType {
Undefined = 0,
String = 1,
Buffer = 2,
VSBuffer = 3,
Array = 4,
Object = 5,
Int = 6
}
这里只简单分析 serialize
. 对可变长度使用 Variable-length quantity (VLQ) 进行编码。
// vs/base/parts/ipc/common/ipc.ts
function createOneByteBuffer(value: number): VSBuffer {
const result = VSBuffer.alloc(1);
result.writeUInt8(value, 0);
return result;
}
const BufferPresets = {
Undefined: createOneByteBuffer(DataType.Undefined),
String: createOneByteBuffer(DataType.String),
Buffer: createOneByteBuffer(DataType.Buffer),
VSBuffer: createOneByteBuffer(DataType.VSBuffer),
Array: createOneByteBuffer(DataType.Array),
Object: createOneByteBuffer(DataType.Object),
Uint: createOneByteBuffer(DataType.Int),
};
export function serialize(writer: IWriter, data: any): void {
if (typeof data === 'undefined') {
writer.write(BufferPresets.Undefined);
} else if (typeof data === 'string') {
const buffer = VSBuffer.fromString(data);
writer.write(BufferPresets.String);
writeInt32VQL(writer, buffer.byteLength);
writer.write(buffer);
} else if (hasBuffer && Buffer.isBuffer(data)) {
const buffer = VSBuffer.wrap(data);
writer.write(BufferPresets.Buffer);
writeInt32VQL(writer, buffer.byteLength);
writer.write(buffer);
} else if (data instanceof VSBuffer) {
writer.write(BufferPresets.VSBuffer);
writeInt32VQL(writer, data.byteLength);
writer.write(data);
} else if (Array.isArray(data)) {
writer.write(BufferPresets.Array);
writeInt32VQL(writer, data.length);
for (const el of data) {
serialize(writer, el);
}
} else if (typeof data === 'number' && (data | 0) === data) {
// write a vql if it's a number that we can do bitwise operations on
writer.write(BufferPresets.Uint);
writeInt32VQL(writer, data);
} else {
const buffer = VSBuffer.fromString(JSON.stringify(data));
writer.write(BufferPresets.Object);
writeInt32VQL(writer, buffer.byteLength);
writer.write(buffer);
}
}
- 当 data 是
undefined
: 写入 1 字节的数据类型。 - 当 data 是
string
:- 按 utf-8 编码将字符转 encode 为 buffer.
- 写入 1 字节的数据类型。
- 写入 VQL 编码的 buffer 的字节长度。
- 写入 buffer.
- 当 data 是
Buffer
或VSBuffer
:- 写入 1 字节的数据类型。
- 写入 VQL 编码的 buffer 的字节长度。
- 写入 buffer.
- 当 data 是
Array
:- 写入 1 字节的数据类型。
- 写入 VQL 编码的数组长度。
- 序列化数组中的每个值。
- 当 data 是整数:
- 写入 1 字节的数据类型。
- 写入 data 的VQL 编码。
- 当 data 是
Object
:- 将 data 序列化为 JSON 字符串, 然后序列化为 buffer.
- 写入 1 字节的数据类型。
- 写入 VQL 编码的 buffer 的字节长度。
- 写入 buffer.
消息类型
VS Code 定义了 4 种请求类型和 5 种响应类型。
// vs/base/parts/ipc/common/ipc.ts
const enum RequestType {
Promise = 100,
PromiseCancel = 101,
EventListen = 102,
EventDispose = 103
}
const enum ResponseType {
Initialize = 200,
PromiseSuccess = 201,
PromiseError = 202,
PromiseErrorObj = 203,
EventFire = 204
}
消息头
基础消息
由两部分组成: 一个数组类型的 header 和消息体.
不同的请求类型和响应类型可能具有不同的 header, 包括请求/响应类型、请求id/响应id、channelName, name.
// vs/base/parts/ipc/common/ipc.ts
private sendRequest(request: IRawRequest): void {
switch (request.type) {
case RequestType.Promise:
case RequestType.EventListen: {
const msgLength = this.send([request.type, request.id, request.channelName, request.name], request.arg);
return;
}
case RequestType.PromiseCancel:
case RequestType.EventDispose: {
const msgLength = this.send([request.type, request.id]);
return;
}
}
}
private sendResponse(response: IRawResponse): void {
switch (response.type) {
case ResponseType.Initialize: {
const msgLength = this.send([response.type]);
return;
}
case ResponseType.PromiseSuccess:
case ResponseType.PromiseError:
case ResponseType.EventFire:
case ResponseType.PromiseErrorObj: {
const msgLength = this.send([response.type, response.id], response.data);
return;
}
}
}
private send(header: any, body: any = undefined): number {
const writer = new BufferWriter();
serialize(writer, header);
serialize(writer, body);
return this.sendBuffer(writer.buffer);
}
对于 RequestType.Promise
类型的请求,其 header 为 [type, id, channelName, name]
- type: 请求类型。
- id: 请求id。
- channelName: channel 是 VS Code 对某些行为或操作的分类。通常为字符串,如
"remoteFilesystem"
是对远程文件系统的操作。 - name: channel 上某种具体的行为或操作,也可能理解为command。如
"write"
是写操作。
[type, id, "remoteFilesystem", "write"]
即对远程文件进行写操作。
最终将 header 和 body 被序列化为一条二进制的基础消息
。
网络消息协议
桌面 VS Code 使用进程间通信(ipc), Web VS Code 使用网络(WebSocket) 通信。
与ipc相比,网络通信有更复杂的要求,比如为保证正确性和同步, 网络消息可能需要确认机制, 网络连接可能需要保活等。
为使用网络传输 基础消息
, VS Code 定义了用于网路传输的消息格式。
浏览器端 WebSocket 的局限
浏览器端 WebSocket 能力主要有如下局限:
- 没有 WebSocket 帧级别的API。即不能操作帧。
- 没有提供发送 Ping Frame 和 Pong Frame 的API。
浏览器端 WebSocket 只能收发数据 message (数据帧).
网络消息类型
VS Code 自定义了 10 种网络消息类型。除了普通消息,还包括控制消息,Ack 消息,以及用于保活的 KeepAlive 消息。
// vs/base/parts/ipc/common/ipc.net.ts
const enum ProtocolMessageType {
None = 0,
Regular = 1,
Control = 2,
Ack = 3,
Disconnect = 5,
ReplayRequest = 6,
Pause = 7,
Resume = 8,
KeepAlive = 9
}
网络消息格式
/-------------------------------|------\
| HEADER | |
|-------------------------------| DATA |
| TYPE | ID | ACK | DATA_LENGTH | |
\-------------------------------|------/
网络消息由 13 个字节的消息头(message header)和数据组成。
网络消息头
1 4 4 4
+------+----------------+----------------+----------------+
| type | id | ack | data length |
+------+----------------+----------------+----------------+
- type: 消息类型, 1 字节。
- id: 消息id, 4 字节,大端序的 32 位无符号整数。 0 表示忽略
- ack 已确认的消息id。4 字节, 大端序的 32 位无符号整数。对远端发送消息的确认。0 表示忽略。
- data length 数据长度。4 字节, 大端序的 32 位无符号整数。
// vs/base/parts/ipc/common/ipc.net.ts
export const enum ProtocolConstants {
HeaderLength = 13,
}
public write(msg: ProtocolMessage) {
const header = VSBuffer.alloc(ProtocolConstants.HeaderLength);
header.writeUInt8(msg.type, 0);
header.writeUInt32BE(msg.id, 1);
header.writeUInt32BE(msg.ack, 5);
header.writeUInt32BE(msg.data.byteLength, 9);
this._writeSoon(header, msg.data);
}
解析案例
上图消息为分段上传大文件时中间的一段。16进制显示。
网络消息头
为前 13 个字节:0100 0000 e900 0000 fc00 9800 35
- 第一个字节
01
。表示这是一条 Regular 消息。 - 后续 4 个字节
00 0000 e9
. message id,即 233. - 再后续 4 个字节
00 0000 fc
. ack id, 确认的消息id,即 252.
- 最后 4 个 字节
00 9800 35
. payload 长度。
数据部分, 即 基础消息
的部分。
- 读取 1 个字节
0x04
, 代表数据为 Array. 按 VQL 读取数组长度为0x04
, 数组长度为 4.基础请求头
是长度为 4 的数组。 - 读取 1 个字节
0x06
, 代表数据为 Int,按 VQL 读取值为0x64
, 即 100. 这是请求类型, 即RequestType.Promise
. - 读取 1 个字节
0x06
, 代表数据为 Int,按 VQL 读取值为0xe701
,基础消息id
. - 读取 1 个字节
0x01
, 代表数据为 String,按 VQL 读取其对应 buffer 的长度为0x10
, 即 16. - 读取 16 个字节,
0x72 0x65 0x6d 0x6f 0x74 0x65 0x46 0x69 0x6c 0x65 0x73 0x79 0x73 0x74 0x65 0x6d
,
解码为 uft-8 编码的字符串为"remoteFilesystem"
, 其为 channelName.const decoder = new TextDecoder('utf-8') const channelName = decoder.decode(new Uint8Array([0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d]))
- 读取 1 个字节
0x01
, 代表数据为 String,按 VQL 读取其对应 buffer 的长度为0x05
, 即 5. - 读取 5 个字节,
0x77 0x72 0x69 0x74 0x65
, 解码为 uft-8 编码的字符串为"write"
, 其为 name (command).const decoder = new TextDecoder('utf-8') const name = decoder.decode(new Uint8Array([0x77, 0x72, 0x69, 0x74, 0x65]))
至此,我们解析出了基础消息头的部分
, 为[100, 0xe701, "remoteFilesystem", "write"]
继续解析数据的部分:
- 读取 1 个字节
0x04
, 代表数据为 Array. 按 VQL 读取数组长度为0x05
, 数组长度为 5.基础请求体
是长度为 5 的数组。 - 读取 1 个字节
0x06
, 代表数据为 Int,按 VQL 读取0x19
, 即 25. 这是远程文件的句柄fd
。 - 读取 1 个字节
0x06
, 代表数据为 Int,按 VQL 读取0x8080a004
, 即 8912896. 这是写入远程文件的位置。 - 读取 1 个字节
0x03
, 代表数据为 Int, 按 VQL 读取其长度0x8080e004
, 即 9961472。表示要写入的数据长度为 9961472。 - 读取 9961472 个字节,表示要写入的数据。
- 后面还有两个字段,不再解析。分别是当前要写入数据在当前 chunk 的偏移量,以及剩余长度。
数据部分为 [fd, pos, data, offset, length]
// vs/platform/files/common/fileService.ts
private async doWriteBuffer(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, buffer: VSBuffer, length: number, posInFile: number, posInBuffer: number): Promise<void> {
let totalBytesWritten = 0;
while (totalBytesWritten < length) {
// Write through the provider
const bytesWritten = await provider.write(handle, posInFile + totalBytesWritten, buffer.buffer, posInBuffer + totalBytesWritten, length - totalBytesWritten);
totalBytesWritten += bytesWritten;
}
}
// vs/platform/files/common/diskFileSystemProviderClient.ts
write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
return this.channel.call('write', [fd, pos, VSBuffer.wrap(data), offset, length]);
}