最近想往全干发展, 一直在看Node相关的东西, 恰好有点我的需求, 就动手撸了个玩具javascript
玩具基于 react + express前端
其中有个场景是这样, 前端页面须要同时提交 表单, 图片, 视频.java
天然而然的就想到了FormData.node
但express自己不支持formdata类型react
因而乎搜了一圈, 发现你们都推荐 multerexpress
找到multer文档一看... 须要提早定义好下载路径json
这不是我想要的...后端
我理想状态是在request上经过键名直接拿到buffer, 由我本身决定后续操做数组
...此时陷入僵局promise
一番思考, 突然联想到前几天看到的RTMP协议规范, 忽然蹦出了个一个想法, 不如本身造一个相似的编码格式?
通过一波艰苦的尝试后, 终于折腾出来了...
前端部分:
构造一个相似FormData的对象, 能够经过append添加键值对, remove删除, get指定的键值
最终发送时, 传输一个序列化的二进制流
后端部分:
构造一个解析器, 解析前端传输的数据, 把解析后的数据, 挂在express的request对象上
结构:
我设想中前端最终传输的结构是这样
+--------------------------------------+
| header | chunk0 | chunk1 | ... | end |
+--------------------------------------+
一个固定长度的头尾, 用于验证数据的完整性
每个键值对包装为一个 chunk
其中每个 chunk 的格式为这样
+----------------------------+
| type | key | length | body |
+----------------------------+
固定长度的帧头, 包含4个部分
其中 type 为数据类型的定义, key 为键值名, length 为值长度, body 为值内容
最终定义以下:
header 固定长度和内容 字符串 'mpd', 3字节
end 固定长度和内容 字符串 'end', 3字节
chunk头部: 固定 20 字节, 其中
type 固定长度 1字节, 其内容为数字, 0 表示常规JSON字符串, 1表示二进制大文件
key 固定长度 15字节, 其内容为字符串, 表示该数据内容的键名
length 固定长度 4字节, 其内容为数字, 表示数据内容长度
chunk尾部
body 可变长度, 其内容为数据, 由服务端根据type类型解析
一点思考
我有个纠结好久的地方, 由于固定了chunk中 key 的长度, 以UTF8编码为例, 每一个键名就只有15个单字符串长度, 但感受也够用了...
length固定4字节, 能够描述4个G的内容偏移量, 我感受是够了
...
构思完成, 开始动手
而后发现...想法很丰满... 但操做起来, 踩了无数坑...真的是想砍本身几刀, 为何非要跟本身过不去?
先不急着帖完整代码
先给老哥们看看工具函数...
const str2buffer = str => {
const encoder = new TextEncoder()
return encoder.encode(str)
}
复制代码
<Buffer 12 0a 4d>
张这个样子 const num2ByteArr = (num) => {
const rest = Math.floor(num / 256)
const last = num % 256
if (rest >= 256) {
return [...num2ByteArr(rest), last]
} else if (rest > 0) {
return [rest, last]
} else {
return [last]
}
}
复制代码
<Buffer 01 02 03>
const numFilledLow = (raw, len) => {
if (raw.length < len) {
const offset = len - raw.length
const filled = new Array(offset).fill(0).concat(raw)
return new Uint8Array(filled)
} else {
return new Uint8Array(raw)
}
}
复制代码
<Buffer 00 00 04 D2>
[4, 210]
const strFilledLow = (raw, len) => {
if (raw.length < len) {
const offset = len - raw.length
const filled = new Uint8Array(offset)
const res = new Uint8Array(len)
res.set(filled)
res.set(raw, offset)
return res
} else {
return new Uint8Array(raw)
}
}
复制代码
const concatBuffer = (...arrs) => {
let totalLen = 0
for (let arr of arrs) {
totalLen += arr.length
}
const res = new Uint8Array(totalLen)
let offset = 0
for (let arr of arrs) {
res.set(arr, offset)
offset += arr.length
}
return res
}
复制代码
const isNumber = v => typeof v === 'number' && v !== NaN
const isString = v => typeof v === 'string'
const isFile = v => v instanceof File
复制代码
这3个就不说了吧
我把这套方案的类名定为
MultipleData
发送时调用 实例的 vaules
方法, 会把数据拼接好
代码以下
import {
str2buffer,
num2ByteArr,
numFilledLow,
strFilledLow,
concatBuffer,
isNumber,
isString,
isFile,
} from './untils'
class MultipleData {
constructor() {
this.header = str2buffer('mpd')
this.end = str2buffer('end')
this.store = {}
}
append(key, value) {
if (!(isNumber(key) || isString(key))) {
throw new Error('key must be a number or string')
}
if (isFile(value)) {
const _value = await value.arrayBuffer() */
this.store[key] = new MultipleDataChunk(key, value, 1)
} else {
this.store[key] = new MultipleDataChunk(key, value, 0)
}
}
remove(key) {
delete this.store[key]
}
get(key) {
return this.store[key]
}
async values() {
const chunks = Object.values(this.store)
const buffers = []
for (let i = 0; i < chunks.length; i++) {
const chunkBuffer = await chunks[i].buffer()
buffers.push(chunkBuffer)
}
/**
* finally buffer like this
* [header | chunk0 | chunk1 | ... | end]
*/
return concatBuffer(this.header, ...buffers, this.end)
}
}
class MultipleDataChunk {
constructor(key, value, type) {
/**
* allow number & string , but force to string
*/
this._key = key.toString()
if (this._key.length > 15) {
throw new Error('key must less than 15 char')
}
this._type = type
this._value = value
}
async buffer() {
/**
* if type = 0, call JSON.stringify
* if type = 1, convert to Uint8Array directly
*/
let value;
if (this._type === 0) {
const jsonStr = JSON.stringify({ [this._key]: this._value })
value = str2buffer(jsonStr)
} else {
const filebuffer = await this._value.arrayBuffer()
value = new Uint8Array(filebuffer)
}
/**
* structure like this
* [type | key | length]
* [body]
* type Number 1byte
* key 15char 15byte
* length Number 4byte
*/
const header = new Uint8Array(20)
const buffer_key = str2buffer(this._key)
const buffer_length = num2ByteArr(value.length)
header[0] = this._type
//header.set(this._type, 0)
header.set(strFilledLow(buffer_key, 15), 1)
header.set(numFilledLow(buffer_length, 4), 16)
return concatBuffer(header, value)
}
valueOf() {
return this._value
}
}
export default MultipleData
复制代码
其中还有个小细节
由于我忽然发现 File对象竟然有了个叫 arrayBuffer
的方法
直接调用这个方法返回一个promise, 其resolve的值是这个文件转化后的ArrayBuff
不用再多一步FileReader了, 舒服
固然也由于这个缘由, 发送数据时得包一层async 或者 Promise
你觉得完了?
后端解析也是坑啊...
一样, 先看看工具函数
const buffer2num = buf => {
return Array.prototype.map.call(buf, (i, index, arr) => i * Math.pow(256, arr.length - 1 - index)).reduce((prev, cur) => prev + cur)
}
复制代码
是否是想打人?
(全世界最好的FP, 不接受反驳)
先别急着动手
他是干啥的
还记得上面那个例子吗, 把数字转化成表示byte的数组, 而后填充
最后拿到的这个玩意 <Buffer 00 00 04 D2>
服务器接收到了这玩意要还原成数字啊...
最开始, 想固然的就 buffer.map(把每一位还原成 n * 256 ^p).reduce(求和)
而后发现不对
仔细排查才发现, node的Buffer对象map返回的每一项依然是 buffer(输入 输出 类型统一, 还真是严谨的FP)
因此须要想写链式就得调用原生数组的map
最终Buffer解析器的代码
const {
buffer2num
} = require('./untils')
class MultipleDataBuffer {
constructor(buf) {
this.header = buf.slice(0, 3).toString()
if (this.header !== 'mpd') {
throw new Error ('error header')
}
let offset = 3
const res= []
while (offset < buf.length - 3) {
const nextHeader = new MultipleDataFrameHeader(buf.slice(offset, offset + 20))
const nextBody = buf.slice(offset + 20, offset + 20 + nextHeader.bodyLength)
let nextData;
if (nextHeader.type === 0) {
nextData = JSON.parse(nextBody)
} else {
nextData = {
[nextHeader.key] : nextBody
}
}
res.push(nextData)
offset = offset + 20 + nextHeader.bodyLength
}
this.data = Object.assign({}, ...res)
this.end = buf.slice(-3).toString()
if (this.end !== 'end') {
throw new Error ('error end')
}
}
}
class MultipleDataFrameHeader {
constructor(buf) {
if (buf.length != 20) {
throw new Error('error frame header length')
}
this.type = buffer2num(buf.slice(0, 1))
this.key = buf.slice(1, 16).filter(i => i != 0).toString()
this.bodyLength = buffer2num(buf.slice(16, 20))
}
}
module.exports = MultipleDataBuffer
复制代码
express中间件(固然, 这个Content-Type, 能够随便写, 只要不跟规范里的重复就行, 固然你先后端传的时候得统一)
const isMultipleData = (req, res, next) => {
const ctype = req.get('Content-Type')
if (req.method === 'POST' && ctype === 'custom/multipledata') {
const tempBuffer = [];
req.on('data', chunk => {
tempBuffer.push(chunk)
})
req.on('end', () => {
const totalBuffer = Buffer.concat(tempBuffer)
req.mpd = new MultipleDataBuffer(totalBuffer)
next()
})
} else {
next()
}
}
复制代码