• 作者:老汪软件技巧
  • 发表时间:2024-08-20 07:01
  • 浏览量:

另一方面,HTTP的transfer-encoding chunked技术允许服务器在知道整个响应内容大小之前就开始发送数据。这种技术通过将数据分割成多个块来发送,使得服务器可以动态生成响应内容,适用于AI对话系统中的流式数据处理。

结论

使用websocket做AI对话, 有杀鸡牛刀的感觉, 开发和维护成本也更高. http Chunked方式相对就更适用.

但在小程序场景下, wx.request是被微信二次定制的基础库, 原本很多在前端可用的HTTP Chunked的库, 在微信小程序上是无法使用的. 这就导致小程序内的对话, 很多人使用的都是WebSocket.

那当我接手前面小伙伴留下的项目时, 使用的也是WebSocket,他们维护了10来种弱网状态(有业务相关代码参合). 对话时经常报错, 还找不到原因, 报错就说是网络不稳定(后面检查下来, 涉及到初始化等各种代码原因, 并不是网络问题). 在几次演示事故后, 老板发话, 说这个对话三天两头出问题,必须搞好! 所以开始了本次重构.

重构开始

基于对两种方案的基本认识, 最终决定使用HTTP Chunked. 然后入坑就开始了!

坑填坑

首先判断是ArrayBuffer时, 先把它转为Uint8Array,这一步没有问题;

_前端对话框_前端聊天功能

然后重点来了: 是Uint8Array转string, 这里就会遇到上面提到的面包问题了,string就是面包,有时候返回的数据可能是3.5个字符串, 这时候转string就会失败. 当找到这个原因后, 解决起来也不复杂:发现解析失败时, 把这一段Uint8Array缓存起来(千万别舍弃,不然下一段必然失败!), 当下一段数据来到时, 两段Uint8Array合起来再尝试解析成string(3.5+.5.5=9这时候就成功了,但如果你舍弃前面的3.5, 后面的5.5必然失败), 成功后清除缓存; 如此往复(原因很简单,http能保证所有数据按顺序接收到,这也是为什么不开enableChunked时总能正常返回的原因)!

上代码

  /**
   * 返回数据转文本
   * @param res
   * @returns
   */
  const getChunkText = (data: any) => {
    // let data = res.data;
    // console.log('getSeeResData:', data)
    // 兼容处理,真机返回的的是 ArrayBuffer
    if (data instanceof ArrayBuffer) {
      data = new Uint8Array(data)
    }
    let text = data
    // Uint8Array转码
    if (typeof data != 'string') {
      // 兼容处理  微信小程序不支持TextEncoder/TextDecoder
      try {
        console.log('lastData', lastData)
        text = decodeURIComponent(escape(String.fromCharCode(...lastData, ...data)))
        lastData = new Uint8Array()
      } catch (error) {
        text = ''
        console.log('解码异常', data)
        // Uint8Array 拼接
        let swap = new Uint8Array(lastData.length + data.length)
        swap.set(lastData, 0)
        swap.set(data, lastData.length)
        // lastData = lastData.concat(data)
        lastData = swap
      }
    }
    return text
  }

HTTP Chunked在分段数据时; 会在原始数据上注入分段符data: , 这里的每一段相当于面包问题的每一袋

所以详细描述下目标二: 在收到散装面包时(怎么区分是不是散装还是完整的一袋呢), 缓存起来, 直到可以组成一袋, 在一次收到多袋时, 拆分一下.

真实处理是这样的,有点不一样的地方: 首先第一段是不处理的(因为无法判断是不是完整的), 直接缓存. 接下来,每一段数据返回时, 判断数据的最开始部分是不是分隔符data: ,如果是, 说明前面的内容是完整的(可能是一段,也可能多段,但是能袋装的),把数据拆分后返回(return)出去, 最后整个http的seccuss回调在把最后一段缓存返回出去.

上代码

 /**
   * 分段返回开始
   */
  const CHUNK_START = 'data:'
  /**
   * 分段返回中断
   */
  const SPLIT_WORD = '\ndata:'
  
    /**
   * 判断是否被拆分
   * @param text
   * @returns
   */
  const isStartString = (text: string) => {
    return text.substring(0, 5) == CHUNK_START
  }
  /**
   * 对被合并的多段请求拆分
   * @param text
   */
  const splitText = (text: string) => {
    return text
      .replaceAll(`\n\n${SPLIT_WORD}`, `\n${SPLIT_WORD}`)
      .replaceAll(`\n${SPLIT_WORD}`, `${SPLIT_WORD}`)
      .split(SPLIT_WORD)
      .filter((str) => !!str)
  }
  
    /**
   * 删除文本的开始的 data:
   * @param text
   * @returns
   */
  const removeStartText = (text: string) => {
    if (text.substring(0, CHUNK_START.length) == CHUNK_START) {
      return text.substring(CHUNK_START.length)
    }
    return text
  }
  
  /**
   * 返回数据集(返回数据)
   * @param res
   * @param onSuccess
   */
  const onChunkReceivedReturn = function (res: any) {
    let text = getChunkText(res)
    console.log('onChunkReceived', text)
    if (isStartString(text) && lastText) {
      // console.log("onSuccess", lastText);
      // onSuccess();
      let swap = lastText
      // 存储本次的数据
      lastText = text
      return splitText(removeStartText(swap))
    } else {
      lastText = lastText + text
    }
  }