TextEncoder / TextDecoder 缺字节错误调试

/ 0评 / 0

一般而言,一个定长字符串是这样设计的:开头 4 字节用于保存字符串长度,接下来是字符串本身。这样一个设计是为了可以将多个(足够长的)字符串拼接在一个包里而无需单独传输,在协议本身不能保证包到达的顺序的情况下这样做可以保证数据的到达顺序。至于为什么不用变长头的原因一是实现起来比较复杂,毕竟 4 字节已经足够表示 2^32 - 1 个字节了,没人会一次性传输 4 GiB 的字符串的;再是现在的网络环境已经不差这多出来的几个字节了。当然,并不是说当前实现不能改成变长头,只是目前而言不需要。

有些跑题,转回来。

在 JavaScript 中,将字符串编码成二进制的工作由 TextEncoderTextDecoder 进行。编码方式默认是 UTF-8. 以及实际上 NodeJS 的 TextDecoder 也只支持 UTF-8. 所以前后端就都统一使用 UTF-8. 目前而言,没有问题。

接下来好玩的事情是将这一堆字节流从网络上发送出去。既然都用上 JS 了,那么直接使用 WebSocket 也是在情理之中的。(请不要来一句「WebSocket 是可以保证包到达顺序的」这样的话。)首先遇到的问题就是,发出的是 Buffer, 收到的是 Blob. 之前指定的 arraybuffer 参数被一概忽略。于是上上下下转换了一番,有了 ArrayBufferDataView. DataView 用于解析包头(类型编号的数据区长度)。

然后 TextDecoder 需要 TypedArray 作为参数。好在 ArrayBuffer 提供了 slice 方法可以截取一段内容,Uint8Array 也可以接受 ArrayBuffer 进行构造(因为前者是后者的另一个 View)。

现在,观察这样一段代码:

function webSocketProcessing(packet: Packet) {
  // packet.data is a DataView of received ArrayBuffer
  // with an offset > 0 (the first few bytes are the header that
  // is irrelevant with further handling process).
  let length = packet.data.getUint32(0);
  let str = tearString(packet.data, 4, length);
  console.log(str);
}

function tearString(data: DataView, offset: number, length: number) {
  let arr = new Uint8Array(data.buffer.slice(offset, length));
  let decoder = new TextDecoder();
  let result = decoder.decode(arr);
  if (decoder.fatal) {
    // error handling here
  }
  return result;
}

看起来很正常。 但是聪明的你可能已经看出来高亮的那一行显然是有问题的(不然也不会被重点关照)。

这个问题就是,DataView 是一个 View, 而不是一个副本。通过 DataViewbuffer 属性访问到的 ArrayBuffer 是不包含任何偏移量的。这就导致了 length 是正确的值,但是解析出来的字符串却总是前面有奇怪的符号,或者干脆触发错误。

但是不止于此,ArrayBuffer#slice 的定义是 ArrayBuffer#slice(start: number, end?: number), 而不是 ArrayBuffer#slice(start: number, length?: number). 这就导致了高亮一行犯下第二个错误,导致获取的字符串长度总是小于发送的字符串长度。

所以,正确的写法应是:let arr = new Uint8Array(data.buffer.slice(offset + data.byteOffset, offset + data.byteOffset + length);

总之,多看 API 有好处。以及 TypeScript 应该全面取代 JavaScript!(今日份的暴论)

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Your comments will be submitted to a human moderator and will only be shown publicly after approval. The moderator reserves the full right to not approve any comment without reason. Please be civil.