Skip to content

VS Code: Transport protocol #45

@xwcoder

Description

@xwcoder

[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 是 BufferVSBuffer:
    • 写入 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 (数据帧).

网络消息类型

binary frame
VS Code 使用 WebSocket 的二进制帧。

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);
}

解析案例

binary frame

上图消息为分段上传大文件时中间的一段。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.
    binary frame
  • 最后 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]);
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions