- 作者:老汪软件技巧
- 发表时间: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
}
}