深入理解joint方法:WebSocket数据分片的拼接实现

417 阅读5分钟

在现代Web开发中,WebSocket是一个常用的技术,用于在客户端和服务器之间建立实时双向通信。由于网络传输的限制或数据量较大,WebSocket传输的数据可能会被分成多个片段。在这种情况下,客户端需要将这些分片数据重新拼接成一个完整的数据包,才能正确处理。本文将深入探讨joint方法的实现,它在处理和拼接WebSocket分片数据时起到了至关重要的作用。

背景知识

在开始讨论具体实现之前,了解一些基础概念是有必要的:

  • 分片数据:当数据包过大或网络条件限制时,数据会被拆分为多个小片段进行传输。每个片段到达客户端后,客户端需要将这些片段重新组合成原始数据包。
  • ArrayBuffer:这是JavaScript中的一个对象,用于表示通用的、固定长度的二进制数据缓冲区。我们可以通过TypedArray视图(如Uint8Array)对其进行字节级别的操作。

joint方法简介

joint方法的主要职责是将分片的数据拼接起来,直到形成一个完整的数据包。以下是该方法的完整代码:

joint(buffer, bufferOffset, dataLen, restDataLen, withHeader) {
  if (withHeader) {
    this._buffer = new ArrayBuffer(dataLen + restDataLen);
  }

  if (dataLen === 0) {
    return restDataLen <= 0;
  }

  let dstview = new Uint8Array(this._buffer);
  let srcview = new Uint8Array(buffer, bufferOffset, dataLen);
  let cpSrc = this._buffer.byteLength - (dataLen + restDataLen);
  for (let i = 0; i < dataLen; i++) {
    dstview[i + cpSrc] = srcview[i];
  }

  return restDataLen <= 0;
}

方法参数解析

  • buffer:当前接收到的分片数据,是一个ArrayBuffer对象。
  • bufferOffset:当前分片数据的起始偏移量,用于指定从buffer的哪个字节开始读取数据。
  • dataLen:当前分片中有效数据的长度,表示读取了多少字节的数据。
  • restDataLen:剩余未接收的数据长度,帮助判断是否还有更多的分片数据需要接收。
  • withHeader:一个布尔值,表示当前分片数据是否包含头部信息(通常用于标识这是一个新数据包的起始部分)。

核心实现步骤

1. 初始化缓冲区

if (withHeader) {
  this._buffer = new ArrayBuffer(dataLen + restDataLen);
}

如果withHeadertrue,这意味着我们正在处理一个新数据包的开始。因此,初始化一个新的ArrayBuffer,其大小为当前片段数据的长度(dataLen)加上剩余数据的长度(restDataLen)。这确保了this._buffer足够大,能够容纳完整的数据包。

2. 处理空数据片段

if (dataLen === 0) {
  return restDataLen <= 0;
}

如果当前分片中没有有效数据(dataLen === 0),我们检查剩余数据的长度(restDataLen)。如果没有剩余数据需要接收,则返回true,表示数据包已经接收完毕。

3. 计算数据插入位置

let cpSrc = this._buffer.byteLength - (dataLen + restDataLen);

cpSrc是一个关键变量,计算出当前数据片段应该存放在this._buffer中的起始位置。其计算逻辑确保数据片段按照正确的顺序插入到缓冲区中,从而拼接成完整的数据包。

4. 复制数据到目标缓冲区

for (let i = 0; i < dataLen; i++) {
  dstview[i + cpSrc] = srcview[i];
}

通过Uint8Array视图,我们可以逐字节地操作ArrayBuffer。这里,我们将当前数据片段的每个字节数据从srcview复制到目标缓冲区dstview中的正确位置。cpSrc确保了数据片段在缓冲区中的起始位置,使得拼接顺序正确无误。

5. 判断数据包是否接收完毕

return restDataLen <= 0;

最后,我们检查是否还有剩余的数据片段需要接收。如果没有剩余数据,返回true,表示整个数据包已经完整接收。

示例场景:详细分析

假设我们接收到一个10KB的数据包,但由于网络原因,这个数据包被分成了两个片段传输:

  • 第一个片段包含6KB的数据(dataLen = 6KB),还剩4KB未接收(restDataLen = 4KB)。
  • 第二个片段包含剩下的4KB数据(dataLen = 4KB),此时没有剩余数据了(restDataLen = 0KB)。

我们将通过详细的步骤解析,来理解joint方法是如何处理这些数据片段的。

第一个片段的处理

  1. 初始化缓冲区:

    • withHeadertrue,所以this._buffer会被初始化为10KBArrayBufferdataLen + restDataLen = 6KB + 4KB)。
  2. 计算插入位置:

    • cpSrc = this._buffer.byteLength - (dataLen + restDataLen)
    • 计算过程为:cpSrc = 10KB - (6KB + 4KB) = 0KB
    • 这意味着第一个片段的数据将从this._buffer0KB位置开始存放。
  3. 复制数据:

    • 由于cpSrc = 0KBfor循环将第一个片段的6KB数据从srcview复制到dstview的起始位置:
    • dstview[i + 0KB] = srcview[i],这将第一个片段的数据从0KB6KB的位置依次复制到this._buffer中。

第二个片段的处理

  1. 无需重新初始化缓冲区:

    • 由于withHeaderfalse,这表示当前片段是属于之前数据包的后续部分,因此缓冲区不会被重新初始化。
  2. 计算插入位置:

    • cpSrc = this._buffer.byteLength - (dataLen + restDataLen)
    • 计算过程为:cpSrc = 10KB - (4KB + 0KB) = 6KB
    • 这意味着第二个片段的数据将从this._buffer6KB位置开始存放。
  3. 复制数据:

    • 由于cpSrc = 6KBfor循环将第二个片段的4KB数据从srcview复制到dstview6KB位置:
    • dstview[i + 6KB] = srcview[i],这将第二个片段的数据从6KB10KB的位置依次复制到this._buffer中。

数据包接收完毕

最后,由于restDataLen = 0KBjoint方法返回true,表示完整的10KB数据包已经接收完毕,数据被正确拼接并存储在this._buffer中。

总结

  • cpSrc计算:通过计算this._buffer的总长度减去当前片段长度和剩余长度,cpSrc准确确定了当前片段应插入缓冲区中的起始位置。
  • 数据拼接:每个片段的数据都按顺序复制到this._buffer的相应位置,确保了数据的完整性和正确性。