在现代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);
}
如果withHeader为true,这意味着我们正在处理一个新数据包的开始。因此,初始化一个新的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方法是如何处理这些数据片段的。
第一个片段的处理
-
初始化缓冲区:
withHeader为true,所以this._buffer会被初始化为10KB的ArrayBuffer(dataLen + restDataLen = 6KB + 4KB)。
-
计算插入位置:
cpSrc = this._buffer.byteLength - (dataLen + restDataLen)- 计算过程为:
cpSrc = 10KB - (6KB + 4KB) = 0KB。 - 这意味着第一个片段的数据将从
this._buffer的0KB位置开始存放。
-
复制数据:
- 由于
cpSrc = 0KB,for循环将第一个片段的6KB数据从srcview复制到dstview的起始位置: dstview[i + 0KB] = srcview[i],这将第一个片段的数据从0KB到6KB的位置依次复制到this._buffer中。
- 由于
第二个片段的处理
-
无需重新初始化缓冲区:
- 由于
withHeader为false,这表示当前片段是属于之前数据包的后续部分,因此缓冲区不会被重新初始化。
- 由于
-
计算插入位置:
cpSrc = this._buffer.byteLength - (dataLen + restDataLen)- 计算过程为:
cpSrc = 10KB - (4KB + 0KB) = 6KB。 - 这意味着第二个片段的数据将从
this._buffer的6KB位置开始存放。
-
复制数据:
- 由于
cpSrc = 6KB,for循环将第二个片段的4KB数据从srcview复制到dstview的6KB位置: dstview[i + 6KB] = srcview[i],这将第二个片段的数据从6KB到10KB的位置依次复制到this._buffer中。
- 由于
数据包接收完毕
最后,由于restDataLen = 0KB,joint方法返回true,表示完整的10KB数据包已经接收完毕,数据被正确拼接并存储在this._buffer中。
总结
cpSrc计算:通过计算this._buffer的总长度减去当前片段长度和剩余长度,cpSrc准确确定了当前片段应插入缓冲区中的起始位置。- 数据拼接:每个片段的数据都按顺序复制到
this._buffer的相应位置,确保了数据的完整性和正确性。