最近想往全乾發展, 一直在看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()
}
}
複製代碼