流式数据接收:提供StreamReceiver处理流式数据接收
Created By RV, and licensed with Creative Commons "CC BY-NC-ND 4.0"
目的与场景
Kree4X支持Streaming模式,允许RPC调用参数及返回结果是Streaming类型(例如文件),详见基础篇《Streaming:流式的调用参数,流式的调用结果》。
基础篇中,我们有意地跳过了一个细节:流式传送的终点,目标节点如何接收流式的数据?
接收流式参数,然后保存在内存中?这违背了流式传送“仅消耗有限、定量内存”的设计本意。
Kree4N中,默认使用临时文件接收流式数据。在接收完毕后,基于临时文件创建StreamReader,并返回给开发者。
在这一章,我们将讲解如何实现StreamReceiver接口,通过设置StreamReceiverFactory,扩展Kree4X接收处理流式数据的能力。
一. 概念
1. 流接收器:StreamReceiver
StreamReceiver 是流式数据的接收者。
Kree4X节点,将分帧到来的流数据,逐块交给StreamReceiver,由StreamReceiver缓存或保存数据块。
完整接收完毕,StreamReceiver.createReader()可创建并返回读取器,用来读取接收的完整数据。
2. 流接收器工厂:StreamReceiverFactory
StreamReceiverFactory,定义了下述接口方法:
create(streamIndex:number, packId:string):基于数据包内的流序号与DataPack标识,创建StreamReceiver实例- streamIndex,第几个流,从零计数。一次RPC调用,可能有多个流类型参数
- packId,数据包DataPack的id
一个KreeX节点,可持有一个 StreamReceiverFactory。
未设置StreamReceiverFactory时:
- Kree4N NodeJS节点,默认使用内置的
FileStreamReceiverFactory - Kree4B浏览器节点,在HTTPS或localhost下默认使用内置的
OPFSStreamReceiverFactory,否则默认使用MemoryStreamReceiver
3. 设置流接收器工厂
Kree4X.setStreamReceiverFactory() 即替换默认流接收器工厂。
// 一个节点一个工厂,设置即替换
nodeA.setStreamReceiverFactory(new MyReceiverFactory())
4. 块的序号:seq
StreamReceiver._writeChunk(seq, pdu) 逐块写入接收数据,系统不保证数据块按seq顺序到达。
Receiver应根据seq动态计算写入位置,支持随机写入。
seq:number:块在流内的序号,从0起递增。同一流内,块按seq排序即为完整流数据pdu:Uint8Array:块数据
5. 数据块的大小:chunkLimit
StreamReceiver._writeChunk(seq, pdu)时,除了最后一个数据块pdu,其它块都是等长的。
通过StreamReceiver.chunkLimit,可以获知这个固定的块大小,字节数。
通过比较pdu.byteLength与chunkLimit,即可知当前chunk是否是流的尾块,结合seq,可支持随机写存储时,计算偏移位置。
二. 示例代码
在下边的示例中,我们实现一个校验型流接收器:接收时实时校验块序号连续性(重复/乱序/缺失即计数违规),并在流完整时输出统计——演示如何替换 kree4n 默认文件接收器并接管流式接收。
1. 定义接收器
// 校验型流接收器:继承 StreamReceiver,实现校验与统计
class ValidatingStreamReceiver extends StreamReceiver {
constructor (streamIndex, packId) {
super(streamIndex, packId)
// 声明配额类型:'memory'(接收数据缓存在内存中)
this._quotaType = 'memory'
this._seq2Chunk = new Map()
this._prevSeq = undefined
this._violations = 0
this._totalBytes = 0
}
// 模板方法 writeChunk() 校验配额后回调这里;seq是块在流内的序号
_writeChunk (seq, pdu) {
if (this._prevSeq != null && seq <= this._prevSeq) {
// 重复或乱序块
this._violations++
return
}
if (this._prevSeq != null && seq > this._prevSeq + 1) {
// 序号跳跃:存在缺失块
this._violations++
}
this._seq2Chunk.set(seq, pdu)
this._totalBytes += pdu.byteLength
this._prevSeq = seq
}
// 已收块计数:框架据此判定流是否完整
_getReceivedCount () {
return this._seq2Chunk.size
}
// 按序号顺序产出已接收的块
createReader () {
if (!this.__reported) {
this.__reported = true
logger.info(`[validating-stream] 校验通过: chunks=${this._seq2Chunk.size} totalBytes=${this._totalBytes} violations=${this._violations}`)
}
const firstSeq = this._firstSeq ?? 0
const lastSeq = this._lastSeq ?? -1
const seq2Chunk = this._seq2Chunk
const asyncGen = (async function * () {
for (let i = firstSeq; i <= lastSeq; i++) {
const chunk = seq2Chunk.get(i)
if (chunk != null && chunk.byteLength > 0) {
yield chunk
}
}
})()
return new GenericStreamReader(asyncGen)
}
destroy () {
if (this._destroyed) {
return
}
super.destroy()
this._seq2Chunk.clear()
}
}
2. 定义工厂
// 工厂:节点级单一工厂,create() 创建校验型接收器
class ValidatingStreamReceiverFactory extends StreamReceiverFactory {
create (streamIndex, packId) {
return new ValidatingStreamReceiver(streamIndex, packId)
}
}
3. 替换默认接收器并验证
// streaming 选项同时用于配额设置:singleMaxSize 单个接收器上限,memoryMaxSize 内存配额
const nodeA = Kree4N.create('node-a', '校验接收方', {
transport: {
streaming: {
enable: true,
singleMaxSize: 10 * 1024 * 1024,
memoryMaxSize: 256 * 1024 * 1024
}
}
})
// 节点级单一工厂:直接设置即替换 kree4n 内置的 FileStreamReceiverFactory
nodeA.setStreamReceiverFactory(new ValidatingStreamReceiverFactory())
nodeA.register('stream', {
upload: async (streamReader) => {
let total = 0
for await (const chunk of streamReader) {
total += chunk.byteLength
}
return `received ${total} bytes`
}
})
nodeA.listen(`tcp://127.0.0.1:${PORT}`)
await nodeA.start()
// node-b 上传 300KB 文件(>64KB 帧缓冲,多帧分片)
const nodeB = Kree4N.create('node-b', '文件发送方', {
transport: { streaming: { enable: true } }
})
nodeB.attach(`tcp://127.0.0.1:${PORT}`)
await nodeB.start()
const result = await nodeB.service('stream').upload(new Kree4N.NodeFile(filePath))
// received 307200 bytes
三. 须强调的细节
1. Node.js 环境默认就是文件型接收器
Kree4N默认设置为 FileStreamReceiverFactory。
所以,Node.js 环境,默认将数据缓存到临时文件。
Kree4B,HTTPS或localhost时,默认设置为 OPFSStreamReceiverFactory,否则设置内置 MemoryStreamReceiver。
所以,浏览器环境,优先尝试将数据写入OPFS(Origin Private File System),回退时,使用内存缓存。
2. 一节点一工厂
一个节点只持有一个 StreamReceiverFactory。
setStreamReceiverFactory() 设置时,直接替换旧有工厂,设置即生效。
3. 配额类型 _quotaType
Kree4X,框架层内置流式数据消耗资源的配额管理及背压控制。
Receiver,须声明自己消耗的资源类型:
'memory':累计进memoryMaxSize'file':累计进fileMaxSize- 超出配额时,框架自动触发背压,进行流控
默认配额(StreamingQuota):
singleMaxSize单个流配额,10MBmemoryMaxSize总内存配额,1GBfileMaxSize总磁盘配额,1GB
可通过kree4X options.transport.streaming 调整:
Kree4N.create('node-a', '校验接收方', {
transport: {
streaming: {
enable: true,
singleMaxSize: 10 * 1024 * 1024, // 单个流上限
memoryMaxSize: 256 * 1024 * 1024, // 内存配额
fileMaxSize: 1 * 1024 * 1024 * 1024 // 文件配额
}
}
})
5. 框架已内建的类型
Transports.Streaming 已导出:
StreamReceiver,接收器接口StreamReceiverFactory,工厂类GenericStreamReader,抽象基类MemoryStreamReceiver,内存实现FileStreamReader,文件实现
四. 涉及到的API:
1. StreamReceiverFactory 基类
/**
* 流接收器工厂:为数据包内的流创建流接收器。
*/
class StreamReceiverFactory {
/**
* 创建流接收器。
* @param {number} streamIndex - 数据包内的流序号(从0开始)。
* @param {string} packId - DataPack标识。
* @returns {StreamReceiver} 流接收器实例。
* @abstract
*/
create(streamIndex: number, packId: string): StreamReceiver
}
2. StreamReceiver 抽象基类
/**
* 流接收器:接收分帧流数据,向消费方提供读取器。
*/
class StreamReceiver {
_quotaType: 'memory'|'file' // 必声明:配额类型
_writeChunk(seq: number, pdu: Uint8Array): void // 必实现:收一个数据块(模板回调)
_getReceivedCount(): number // 必实现:已收块计数(框架据此判定完整)
createReader(): StreamReader // 必实现:产出读取器
destroy(): void // 必实现:清理(先调 super.destroy())
// 框架提供,无需子类实现:
writeChunk(seq, pdu) // 模板方法:配额校验后回调 _writeChunk
setFirstSeq(seq) | setLastSeq(seq) // 段首/段尾帧到达时由框架注入
// 框架注入,勿自行赋值:
_firstSeq | _lastSeq // 段首/段尾帧的序号
}
3. 节点设置流接收器工厂
/**
* 设置流接收器工厂(节点级单一工厂,重复设置替换旧工厂)。
*
* @param {StreamReceiverFactory} streamReceiverFactory - 流接收器工厂。
* @returns {KreeX} 当前节点实例,支持链式调用。
*/
setStreamReceiverFactory(streamReceiverFactory)
/**
* 读取当前流接收器工厂。
*
* @returns {StreamReceiverFactory|undefined} 当前流接收器工厂。
*/
get streamReceiverFactory(): StreamReceiverFactory|undefined
五. 可运行代码
完整示例代码,参见:05-stream-receiver.mjs