LiteHub之文件下载与视频播放

文件下载

前端请求

箭头函数

//这个箭头函数可以形象理解为,x流入(=>)x*x,
//自然而然=>前面的就是传入参数,=>表示函数体
x => x * x//相当于
function (x) {return x * x;
}//如果参数不是一个,就需要用括号()括起来:
(x, y) => x * x + y * y

本项目的请求下载前端代码为:

 function downloadFile(resourceId, filename, progressBar, statusText) {fetch('/resource/download', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ resourceId }) //通过post方式将要下载的文件路径发送给后端}).then(response => {if (!response.ok) {throw new Error('下载失败');}const contentLength = response.headers.get('Content-Length');const total = contentLength ? parseInt(contentLength, 10) : 0;//返回内容长度const reader = response.body.getReader(); //这个可以逐块提供bodyconst chunks = [];let received = 0;const pump = () => reader.read().then(({ done, value }) => {if (done) {//如果读取完成,整个文件已下载const blob = new Blob(chunks);//将所有小段chunks转换成一个完成的blob(binary large object)const url = window.URL.createObjectURL(blob);//浏览器创建一个临时的URL地址来获取这个数据//如blob:http://localhost/17dfc4b1-df34-4a93-a6a7-6df9f1e85e0cconst a = document.createElement('a');a.href = url;a.download = filename;document.body.appendChild(a);a.click();//模拟点击浏览器的下载行为document.body.removeChild(a);window.URL.revokeObjectURL(url);//避免内存泄露progressBar.style.width = '100%';statusText.textContent = '下载完成';return;}chunks.push(value);received += value.length;//更新下载进度if (total > 0) {const percent = Math.floor((received / total) * 100);progressBar.style.width = percent + '%';progressBar.textContent = percent + '%';statusText.textContent = `下载中 ${percent}%`;} else {statusText.textContent = `下载中(未知大小)`;}//递归调用 pump(继续读取下一段)return pump();});return pump();}).catch(error => {console.error('下载出错:', error);progressBar.style.backgroundColor = 'red';statusText.textContent = '下载失败';});}//类比
// 后端:用水龙头一点点把水流出来
// 前端:接水并灌到瓶子里(Blob)
// createObjectURL:给这瓶水贴个标签(blob URL)
// 点击下载:把瓶子交给你下载
// revokeObjectURL:把标签撕掉,清理内存

对于pump函数的理解,结合箭头函数和promise

  1. reader.read()
    ○ 返回一个 Promise<{ done: boolean, value: Uint8Array }>。
    ○ done: true 表示读取完了;
    ○ value 是当前读取的一段数据(Uint8Array 格式)。
  2. 箭头函数 () => reader.read().then(…)
    ○ 这是一个返回 Promise 的函数。
    ○ done: true 表示读取完了;
    ○ value 是当前读取的一段数据(Uint8Array 格式)。
  3. 箭头函数 () => reader.read().then(({ done, value }) => { return dump()}
    ■ ()=>reader.read(),无参数传入,执行reader.read(),返回reader.read()执行的结果{done,value}。
    ■ .then({ done, value })通过上一步接收这两个数据,然后通过这两个执行相应内容;
    ■ 如果done为false,表示还没执行完成,chunks.push(value):把这一段加入缓存 ,更新进度条, 递归调用自身,继续下一段读取 (return pump())。

后端响应

 FileUtil file(filePath); 
if (!file.isValid()) //判断请求的文件是否有效
{LOG_WARN << filePath << "not exist.";resp->setStatusLine(req.getVersion(), http::HttpResponse::k404NotFound, "Not Found");resp->setContentType("text/plain");std::string resp_info="File not found";resp->setContentLength(resp_info.size());resp->setBody(resp_info);
}
//设置相应头
resp->setStatusLine(req.getVersion(), http::HttpResponse::k200Ok, "OK");
resp->setCloseConnection(false);
resp->setContentType("application/octet-stream");std::string filename = std::filesystem::path(filePath).filename().string();
LOG_INFO<<"filename:"<<filename;
resp->addHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
//设置响应格式为文件类型,并添加文件的路径
resp->setContentLength(file.size());
resp->setisFileResponse(filePath);

设计亮点

HttpResponse.h头文件中

public:bool isFileResponse() const {return isFileResponse_;}std::string getFilePath() {return filePath_;}void setisFileResponse(const std::string& path){isFileResponse_ = true;filePath_ = path;}
private:
bool                               isFileResponse_; //判断是否是文件,如果是,采用流式发送
std::string                        filePath_;

在httpserver的请求函数中判断,如果是文件类型,就调用tcpconnection先将响应头发送出去,然后将消息体分小块发送,这里设置的是8kb;如果不是文件类型,直接将整个响应发送出去
HttpServer::onRequest函数中

// 给response设置一个成员,判断是否请求的是文件,如果是文件设置为true,并且存在文件位置在这里send出去。
if (!response.isFileResponse())
{	//不是文件类型muduo::net::Buffer buf;response.appendToBuffer(&buf);conn->send(&buf);
}
else
{// 1. 构造响应头muduo::net::Buffer headerBuf;response.appendToBuffer(&headerBuf);  // 只添加状态行和头部,不包含 bodyconn->send(&headerBuf);  // 先发 header// 2. 发送文件内容(分块)const std::string filePath = response.getFilePath();std::ifstream file(filePath, std::ios::binary);// 以二进制模式打开文件if (file) {const size_t bufferSize = 8192; 			// 8KB 缓冲区char buffer[bufferSize];                  // 栈上分配缓冲区while (file) {                            // 循环直到文件读取结束或出错file.read(buffer, bufferSize);        // 读取最多 bufferSize 字节到 bufferstd::streamsize bytesRead = file.gcount(); // 实际读取的字节数if (bytesRead > 0) {conn->send(muduo::StringPiece(buffer, bytesRead));// 发送数据块}}} else {// 文件打不开,补偿错误提示muduo::net::Buffer errBuf;errBuf.append("HTTP/1.1 500 Internal Server Error\r\n\r\nFile open failed");conn->send(&errBuf);}
}

之所以是在httpserver上分块发送数据流,是为了保证代码较好的层次性,httpserver负责管理多个tcp连接,包括发送消息和接收消息等。

视频播放

      // 从请求中获取 Range 头,例如 "bytes=1000-2000"std::string rangeHeader = req.getHeader("Range");LOG_INFO << "Range Header: " << rangeHeader;// 默认起始字节 start=0,结束字节 end=文件大小-1,表示完整文件std::streamsize start = 0, end = fileSize - 1;// 标记是否是分块响应bool isPartial = false;if (!rangeHeader.empty()) {// 如果客户端带了 Range,则标记为分块传输isPartial = true;long s = 0, e = -1;// 使用 sscanf 解析格式 bytes=<start>-<end>// 注意:用户可能只写了起始,没有写结束,所以要判断 sscanf 返回值int n = sscanf(rangeHeader.c_str(), "bytes=%ld-%ld", &s, &e);start = s;if (n == 1 || e == -1) {// 如果只解析到 1 个数,或者结束为 -1,则表示读到文件末尾end = fileSize - 1;} else {// 解析到两个数,且结束不能超过文件大小end = std::min((std::streamsize)e, fileSize - 1);}// 合法性检查:start 必须小于等于 end 且小于文件大小if (start > end || start >= fileSize) {// 如果不合法,返回 416 状态码(Requested Range Not Satisfiable)resp->setStatusLine(req.getVersion(), http::HttpResponse::k416RequestedRangeNotSatisfiable, "Requested Range Not Satisfiable");char rangeValue[64];// Content-Range 必须带 "*/总大小"snprintf(rangeValue, sizeof(rangeValue), "bytes */%ld", fileSize);resp->addHeader("Content-Range", rangeValue);resp->setCloseConnection(true);resp->setContentType("text/plain");resp->setBody("Invalid Range");return;}}// 计算需要读取的 chunkSizestd::streamsize chunkSize = end - start + 1;std::vector<char> buffer(chunkSize);// 如果需要分块,最好这里限制一下 chunkSize,防止内存过大// 定位到要读的起始位置file.seekg(start, std::ios::beg);// 从文件读出 chunkSize 大小的数据到 bufferfile.read(buffer.data(), chunkSize);// === 构造响应 ===if (isPartial) {resp->setStatusLine(req.getVersion(), http::HttpResponse::k206PartialContent, "Partial Content");char rangeHeaderValue[128];snprintf(rangeHeaderValue, sizeof(rangeHeaderValue),"bytes %ld-%ld/%ld", start, end, fileSize);resp->addHeader("Content-Range", rangeHeaderValue);} else {resp->setStatusLine(req.getVersion(), http::HttpResponse::k200Ok, "OK");}resp->addHeader("Accept-Ranges", "bytes");// 无论是否分块,都要告知支持分块resp->setContentType("video/mp4");         // 设置内容类型为 mp4 视频resp->setContentLength(buffer.size());     // 设置 Content-Lengthresp->setBody(std::string(buffer.begin(), buffer.end()));  // 把读取的文件块设置到响应体}

后端涉及对请求体中的range字段进行解析,判断range字段的合法性,随后根据range字段请求内容决定是返回部分内容还是全部内容。
请求所有内容:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
依次拖动播放进度条,range字段发生改变,格式为–字段,这里是请求从某一时刻到视频结束。
请求部分内容:
在这里插入图片描述
这里请求的是从字节6000-18000大小的数据,返回的响应为
在这里插入图片描述
这里的响应头字段为206 partial content,表示响应返回的只是视频的一部分数据。


range的合法性校验
这里我手动指定range的范围为6000-18000000000000,实际是超出了请求视频的最大范围,看看最后返回的什么。使用curl(这里因为是测试,所以去掉了权限的判定,实际上运行的时候使用curl是不可行的)
在这里插入图片描述
可以看到这里返回的是文件的最大大小。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/88350.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/88350.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

QT5使用cmakelists引入Qt5Xlsx库并使用

1、首先需要已经有了Qt5Xlsx的头文件和库&#xff0c;并拷贝到程序exe路径下&#xff08;以xxx.exe/3rdparty/qtxlsx路径为例&#xff0c;Qt5Xlsx版本为0.3.0&#xff09;&#xff1b; 2、cmakelist中&#xff1a; # 设置 QtXlsx 路径 set(QTXLSX_ROOT_DIR ${CMAKE_CURRENT_SOU…

醋酸镨:闪亮的稀土宝藏,掀开科技应用新篇章

一、什么是醋酸镨醋酸镨是一种镨的有机盐&#xff0c;镨是稀土金属元素之一。作为一种重要的稀土化合物&#xff0c;醋酸镨通常以水合物的形式存在&#xff0c;呈现淡黄色或无色结晶。镨元素本身因其独特的物理化学特性&#xff0c;在工业和科技领域有着广泛应用&#xff0c;而…

深入解析JVM内存结构与垃圾回收机制

java是强类型高级语言JVM&#xff08;Java Virtual Machine&#xff0c;Java虚拟机&#xff09;是Java平台的核心组件&#xff0c;它是一个虚拟的计算机&#xff0c;能够执行Java字节码&#xff08;bytecode&#xff09;。1、区域划分JVM对Java内存的管理也是分区分块进行&…

Java 流程控制详解:从顺序执行到跳转语句,掌握程序逻辑设计

作为一名Java开发工程师&#xff0c;你一定知道&#xff0c;流程控制&#xff08;Flow Control&#xff09; 是编写任何程序的核心。它决定了代码的执行路径、分支走向和循环次数。本文将带你系统梳理 Java中的所有常用流程控制结构&#xff0c;包括&#xff1a;顺序结构分支结…

面试150 环形链表

思路 采用双指针法,slow指针每次走一步,fast指针每次走两步&#xff0c;如果相遇的情况下&#xff0c;slow指针回到开始的位置,此时快慢指针各走一步&#xff0c;当相遇的时候也就是说明链表中有环。 # Definition for singly-linked list. # class ListNode: # def __init…

AI技术正在深度重构全球产业格局,其影响已超越工具属性,演变为推动行业变革的核心引擎。

一、AI如何重塑AI的工作与行业&#xff08;AI助手领域&#xff09;能力升级理解与生成&#xff1a;基于LLM&#xff08;大语言模型&#xff09;&#xff0c;AI能处理开放式问题、撰写报告、翻译代码&#xff0c;替代部分人类知识工作。个性化交互&#xff1a;通过用户历史对话分…

Kafka的无消息丢失配置怎么实现

那 Kafka 到底在什么情况下才能保证消息不丢失呢&#xff1f; Kafka 只对“已提交”的消息&#xff08;committed message&#xff09;做有限度的持久化保证。 第一个核心要素是“已提交的消息”。什么是已提交的消息&#xff1f;当 Kafka 的若干个 Broker 成 功地接收到一条…

集成CommitLInt+ESLint+Prettier+StyleLint+LintStaged

代码可读性低代码 代码规范落地难代码格式难统一代码质量低下 配置 ESLint ESLint 是一个用来识别 ECMAScript 并且按照规则给出报告的代码检测工具&#xff0c;使用它可以避免低级错误和统一代码的风格。它拥有以下功能&#xff1a; 查出 JavaScript 代码语法问题。根据配置…

寻找两个正序数组的中位数(C++)

给定两个大小分别为 m 和 n 的正序&#xff08;从小到大&#xff09;数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。算法的时间复杂度应该为 O(log (mn)) 。示例 1&#xff1a;输入&#xff1a;nums1 [1,3], nums2 [2] 输出&#xff1a;2.00000 解释&#x…

Expected Sarsa 算法的数学原理

&#x1f31f; 一、Expected Sarsa 算法的数学原理 1. 什么是 Expected Sarsa&#xff1f; Expected Sarsa 是一种基于 时序差分&#xff08;Temporal Difference, TD&#xff09;学习 的强化学习算法&#xff0c;用于估计 动作值函数 ( q_{\pi}(s, a) )。它是 Sarsa 算法的一种…

Vue的watch和React的useEffect

参考文章&#xff1a;https://zhuanlan.zhihu.com/p/686329898

idea中合并git分支

1.把本地dev代码合并到本地master代码在提交代码之前&#xff0c;先确保dev和master都拉取了最新的代码都进行了Git->pull了这时候确保Local的第一个分支是master分支&#xff0c;然后选择dev分支 ,鼠标右键-》Merge dev into master这时候会提示 有合并到本地master最新的代…

《Spring 中上下文传递的那些事儿》Part 7:异步任务上下文丢失问题详解

&#x1f4dd; Part 7&#xff1a;异步任务上下文丢失问题详解 在现代 Java 应用中&#xff0c;异步编程已经成为提升性能、解耦业务逻辑的重要手段。无论是使用 CompletableFuture、线程池&#xff08;ExecutorService&#xff09;、定时任务&#xff08;ScheduledExecutorSe…

大语言模型驱动智能语音应答:技术演进与架构革新

在智能客服、电话银行等场景中&#xff0c;用户时常遇到这样的困境&#xff1a;“请描述您的问题...抱歉没听清&#xff0c;请重试...正在为您转接人工”。传统语音应答&#xff08;IVR&#xff09;系统受限于规则引擎与浅层语义理解&#xff0c;难以应对复杂多变的自然语言表达…

【Linux】内存管理

要求&#xff1a;1、编写程序&#xff0c;实现如下功能。&#xff08;1&#xff09;随机生成 1000000 个 0~1 之间的数&#xff1b;&#xff08;2&#xff09;统计分析这些数据&#xff0c;计算均值、方差和分布情况&#xff0c;分布情况按0.01 的步长进行统计&#xff1b;&…

苍穹外卖—day1

文章目录前言一、接口文档导入与生成二、前端环境搭建三、后端环境搭建1. 了解项目结构2. 环境搭建常见问题总结前言 &#xff08;简要说明笔记的目的&#xff1a;记录搭建过程、关键配置和结构理解&#xff09; 一、接口文档导入与生成 Apifox 导入 使用工具&#xff1a;https…

基于微信小程序的在线疫苗预约小程序源码+论文

基于微信小程序的在线疫苗预约系统源码论文代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339; 分享万套开题报告任务书答辩PPT模板 作者完整代码目录供你选择&#xff1a; 《SpringBoot网站项目》800套 《SSM网站项目》1200套 《小程序项目》600套…

Windows 11 安装过程中跳过微软账户创建本地账户

背景 在 Windows 11 的安装和设置过程中&#xff0c;Microsoft 账号登录是默认的认证方式。然而&#xff0c;在某些情况下&#xff0c;可能需要绕过此步骤以创建本地账户。 微软在 2025 年 3 月推送的 Windows 11 预览版&#xff08;Build 26120.3653 和 Build 26200.5516&am…

利用DBeaver实现异构数据库数据定时任务同步

1、背景 本需求需要实现抽取KingBaseEs数据库的某几张表数据&#xff0c;定时同步到MySQL中 2、工具准备 2.1 DBeaverEE25.1(必须要企业版&#xff0c;如果用社区版没有定时任务功能) https://dbeaver.io/download/ 2.2 KingBaseEs数据库及驱动 https://www.kingbase.com…

【TCP/IP】1. 概述

1. 概述1. 概述1.1 因特网及技术催生新时代1.1.1 信息化时代1.1.2 关键技术1.1.3 国家战略1.2 网络互联的动机和技术1.2.1 网络互联的动机1.2.2 网络互联技术1.3 因特网的形成和发展1.3.1 国际因特网发展轨迹1.3.2 中国互联网发展1.4 有关因特网的组织机构1.5 请求注解&#xf…