跳到主要内容

2 篇博文 含有标签「express」

查看所有标签

· 阅读需 6 分钟

Source code:https://github.com/GSemir0418/file-slice-upload

针对大文件上传的业务场景,前后端采用切片上传的方案,即前端将大文件分割为固定大小的 chunk,并循环请求给后端;后端每获取一部分,就写入到服务器指定文件中,最终实现大文件上传。

1 客户端

1.1 初始化

npm init -y
yarn add vite -D

1.2 项目结构

.
├── index.html 项目首页
├── node_modules
├── package.json
├── src
│ ├── app.js 项目入口文件,主要方法都写到这里
│ └── config.js 字段映射等配置
└── yarn.lock

1.3 思路

  1. 获取上传的文件数据
// 在 oUploader 中获取到 files 数组,并取出第一项,命名为 file
const { files: [file] } = oUploader
// 结构取出上传文件的信息
const { name, size, type } = file
  1. 校验(size,type)
  2. 记录当前上传大小 uploadedSize,用于控制切片及计算进度
  3. while 循环中,使用 slice 对 file 数据进行切片
while (uploadedSize < size) {
const fileChunk = file.slice(uploadedSize, uploadedSize + CHUNK_SIZE);
}
  1. 构造 formData
function createFormData({ name, fileName, type, size, uploadedSize, chunk }) {
const postData = new FormData();
postData.append("name", name);
postData.append("fileName", fileName);
postData.append("type", type);
postData.append("size", size);
postData.append("uploadedSize", uploadedSize);
postData.append("chunk", chunk);
return postData;
}

const formData = createFormData({
name,
fileName,
type,
size,
uploadedSize,
chunk: fileChunk,
});
  1. axios 请求
// axios发送请求
try {
response = await axios.post(API, formData);
} catch (error) {
oInfo.innerHTML = INFO["UPLOAD_FAILED"] + error.message;
return;
}
  1. 每份 chunk 上传结束后,更新 uploadedSize,并同步进度条

1.4 优化思路

  1. 如果某一片上传失败了,怎么处理?
  • 重试机制:实现自动重试机制,在文件块上传失败时(catch error),可以自动尝试重新上传
  • 断点续传:记录已成功上传文件块的信息,如果上传失败,可以从中断的地方重新开始上传。也可以让用户自己决定是否重传失败的文件块。
  • 后台验证完整性:在所有文件块上传完成后,后台进行校验,确保所有文件块均正确无误地上传。如果校验失败,可以提示前端做相应处理。
  • 取消上传任务:提供一个取消机制,当用户决定取消或者在上传过程中发现某个文件块连续重试失败达到限制次数时,可以触发取消操作。取消时应清理已上传的文件块并释放资源。
  1. 如果网络波动,如何保证上传顺序?
  • 使用文件块元信息:在上传每个文件块时,除了文件块的数据外,还应该发送一个包含文件块序号的元信息,这样即使文件块的上传请求不是按序到达服务器,服务器也能根据元信息中的序号将文件块放置到正确的位置。
  • 服务器端排序:在所有文件块上传完毕后,服务器可以根据每个文件块的序号进行排序,以确保文件块在最终组装时的顺序正确。
  • 并发请求控制,详见《并发请求》一文

2 Express 服务端

2.1 初始化

npm init
yarn add express express-fileupload
yarn global add nodemon

2.2 项目结构

.
├── app.js
├── node_modules
├── package.json
├── upload_tem
│ └── 1656466982424_1.mp4.mp4
└── yarn.lock

2.3 思路

  1. 在请求体解构取出 chunk 及其他数据
const { name, fileName, uploadedSize } = req.body;
const { chunk } = req.files;
  1. 处理文件名、后缀及保存路径
  2. 根据 uploadedSize 判断新建或追加数据文件
if (uploadedSize !== "0") {
// 注意是字符串0
if (!existsSync(filePath)) {
res.send({
code: 1002,
msg: "No file exists",
});
return;
}
// append数据到文件,结束本次上传
appendFileSync(filePath, chunk.data);
res.send({
code: 200,
msg: "chunk appended",
// 将服务器静态数据文件路径发送给前端
video_url: "http://localhost:8000/" + filename,
});
return;
}
  1. 如果 uploadedSize 为 0,表示没有正在上传的数据,此时创建并写入这个文件
writeFileSync(filePath, chunk.data);
res.send({ code: 200, msg: "file created" });

附:使用到的中间件等方法

// 请求体数据处理中间件
const bodyParser = require("body-parser");
const uploader = require("express-fileupload");
// extname是获取文件后缀名的
const { extname, resolve } = require("path");
// existsSync检查文件是否存在; appendFileSync同步添加数据
const { existsSync, appendFileSync, writeFileSync } = require("fs");
// 解析并返回的请求体对象配置为任意类型
app.use(bodyParser.urlencoded({ extended: true }));
// 解析json格式的请求体
app.use(bodyParser.json());
// 请求体中上传数据的处理,返回的数据在req.files中
app.use(uploader());
// 指定静态文件url
app.use("/", express.static("upload_tem"));
// 跨域处理
app.all("*", (_, res, next) => {
res.header("Access-Control-Allow-origin", "*");
res.header("Access-Control-Allow-methods", "POST,GET");
next();
});

· 阅读需 7 分钟

https://github.com/GSemir0418/video-call-demo-express

基于 SocketIO 和 PeerJS,使用 Express 完成 Web 视频通话的 demo,

二者结合既能高效传输数据,又能正确处理实时的信令事件

其中 socket.io 主要负责在服务器和客户端之间实现实时双向通信,用于在服务器和客户端之间传递与通话相关的信令数据,比如「哪一个用户加入了通话」或者「一个用户离开了通话」

PeerJS

https://peerjs.com/

PeerJS 是一个封装了 WebRTC API 的 JavaScript 库,主要负责通过浏览器直接在客户端之间进行实时通信,可以让两个浏览器直接进行实时数据的传输,无需经过服务器。

WebRTC(Web Real-Time Communication)是一项开源项目,让网页应用和网站能在不需要中间媒介的情况下直接进行浏览器间音视频通话和数据分享,大大降低了实时通信应用的复杂性,使得开发者无需为实现复杂的实时通信架构而头疼,也让用户无需安装任何额外插件就能使用浏览器进行音视频聊天和文件分享。

PeerJS 的作用主要体现在以下几点:

  1. 直接传输视频和音频流:由于 PeerJS 封装了 WebRTC,你可以使用它将视频和音频流直接从一个浏览器传输到另一个浏览器,而无需经过服务器。这不仅能提高数据传输速度,还可以显著降低服务器带宽消耗。
  2. 高效的数据传输:PeerJS 允许你使用其数据通道API进行二进制数据的直接传输,同时,数据通道API也支持流控和拥塞控制。
  3. 点对点的通信模式:通过 PeerJS 和 WebRTC 实现的点对点(Peer-to-Peer)通信模式,可以有效避免服务端负载过大的问题,并且提高了整体的数据传输性能。

使用

  • 基本使用
npm i -g peer
peerjs --port 9000 --key peerjs --path /myapp
// 初始化 peer 实例,连接 peer 服务
const peer = new Peer(undefined, {
host: '你的服务器地址',
port: '你的服务器端口',
path: '/myapp',
key: 'peerjs',
})
// 第一个参数表示该用户 id,传入 undefined 表示自动生成用户 id
  • 可以将一个 peer 对象类比于一个用户
  • open 事件

open 事件在 Peer 对象与其对应的 Peer 服务器成功建立连接时触发。在这个事件触发之后,我们就可以获得一个在 Peer 服务器上唯一的ID,它可以被用来建立 Peer 到 Peer 的连接

事件参数 id 是由 Peer 服务器为此 Peer 对象分配的唯一 ID。在此事件触发后,我们就可以使用这个 ID 与其他 Peer 对象建立连接

  • call 事件

call 事件在 Peer.call() 调用时触发,其事件参数 call 对象,表示一次 Peer 到 Peer 的连接

当与另一个 Peer 对象建立连接并共享音视频流,可以调用 peer.call(otherPeerId, yourStream)。其中 otherPeerId 是你想要连接的其他 Peer 的 ID,yourStream 是你想要共享的音视频流

当调用了 peer.call() 后,我们可以得到一个代表这次连接的 Call 对象。在这个 Call 对象上,我们可以听取各种事件,如 streamclose 等,分别在接收到其他 Peer 的流以及连接关闭时触发

  • 可以将 call 连接类比于 socket 连接,可以使用 on 监听事件,也可以触发事件

代码

服务端开启 peerjs 服务

服务端开启 socket 服务,处理 join-roomuser-connecteddisconnected 以及 user-disconnected 事件

io.on('connection', socket => {
// 注册并监听客户端触发的 join-room 事件
socket.on('join-room', (roomId, userId) => {
// 让 socket 客户端加入这个 roomId 房间
socket.join(roomId)
// 让 socket 客户端向指定房间内(除自己之外)的其他客户端广播 user-connected 事件
socket.to(roomId).emit('user-connected', userId)

// 注册断开连接事件
socket.on('disconnect', () => {
// 向指定房间内其他成员广播 该用户断开连接
socket.to(roomId).emit('user-disconnected', userId)
})
})
})

客户端源码

// 初始化 socket 连接
const socket = io('/')
const videoGrid = document.getElementById('video-grid')
// 缓存所有连接到同一房间的其他用户
const peers = {}
// 连接 peerjs 服务器
const myPeer = new Peer(undefined, {
host: '/',
port: '3001'
})

// 视频元素:来自当前用户设备的视频流
const myVideo = document.createElement('video')
myVideo.style.border = '3px solid red'
myVideo.muted = true

// 当 peer 连接打开时,通过socket向服务器发送一个'join-room'事件
myPeer.on('open', id => {
socket.emit('join-room', ROOM_ID, id)
})

// 获取当前用户视频流
navigator.mediaDevices.getUserMedia({
video: true,
audio: true
}).then(currentStream => {
// 添加当前用户的视频流数据及 video 元素
addVideoStream(myVideo, currentStream)

// 监听其他用户的连接事件
socket.on('user-connected', (userId) => {
// 发起 peer 连接
connectToNewUser(userId, currentStream)
})

// 监听'call'事件,
myPeer.on('call', call => {
// 当收到其他用户的音视频流时,将当前用户的视频流返回给其他用户
call.answer(currentStream)
const video = document.createElement('video')
call.on('stream', otherVideoStream => {
addVideoStream(video, otherVideoStream)
})
})
})

// socket 断开连接事件
socket.on('user-disconnected', userId => {
if(peers[userId])
peers[userId].close()
})


// 添加(其他用户)视频元素
function addVideoStream(video, stream) {
video.srcObject = stream
video.addEventListener('loadedmetadata', () => {
video.play()
})
videoGrid.append(video)
}

// 连接到(其他)用户
function connectToNewUser(userId, stream) {
// 向该用户建立连接
const call = myPeer.call(userId, stream)
const video = document.createElement('video')
call.on('stream', userVideoStream => {
addVideoStream(video, userVideoStream)
})
// close 事件触发后,移除 video 元素
call.on('close', () => {
video.remove()
})
// 缓存新用户连接
peers[userId] = call
}