当前位置: 首页 > news >正文

视频分块上传Vue3+SpringBoot3+Minio

文章目录

  • 一、简化演示
      • 分块上传、合并分块
      • 断点续传
      • 秒传
  • 二、更详细的逻辑和细节问题
      • 可能存在的隐患
  • 三、代码示例
      • 前端代码
      • 后端代码

一、简化演示

分块上传、合并分块

前端将完整的视频文件分割成多份文件块,依次上传到后端,后端将其保存到文件系统。前端将文件块上传完毕后,发送合并请求,后端拿取文件块,合并后重新上传到文件系统。
在这里插入图片描述

断点续传

前端遍历文件块,每次上传之前,先询问文件块是否存在,只有不存在的情况下,才会上传。
请添加图片描述

秒传

前端分割视频文件前,先询问此视频是否已经存在,存在则不再上传,后端之间返回视频信息。前端看起来就像是被秒传了。
请添加图片描述

二、更详细的逻辑和细节问题

  • 视频文件和文件块都通过文件本身计算MD5值作为唯一标志
  • 文件系统使用Minio,只要提供buckerNamepath就可以操作文件
  • 后端合并文件块成功后会删除文件块,并以MD5值为id存入数据库
  • Minio存储文件块时,依据其md5值计算path,比如取前两个字符构建二级文件夹,文件名为md5值,无后缀。所以只需要提供文件块的md5值就可以操作文件块。
  • Minio存储完整视频文件时,依据其md5值计算path,同上,文件名为md5值,携带.mp4等后缀,所以只需要提供视频文件的md5值就可以操作视频文件。
  1. 首先,前端计算视频文件的MD5值,记为fileMd5,传递MD5值来询问后端此视频文件是否存在,后端查询数据库返回结果,如果存在,则前端触发“秒传”。
  2. 如果不存在,则将视频文件分割成文件块,循环上传,每次循环,首先计算文件块的md5值,传递md5值询问后端此文件块是否存在,后端根据md5判断文件块是否存在,如果存在,前端跳过此文件块上传,直接标记为上传成功,如果不存在,则上传至后端,后端将其保存到minio。这其实就是“分块上传,断点续传”。
  3. 最后所有分块文件都上传成功,前端发起合并请求,传递视频文件的md5值和所有文件块的md5值到后端,后端进行文件块合并、文件块的删除、合并文件的上传,将信息存储在mysql数据库,将执行结果告知前端。这就是“合并分块”

可能存在的隐患

一个视频文件的文件块没有全部上传完成就终止,此时文件块将一直保存在minio中,如果之后此视频再也没有发起过上传请求,那么这些文件块都是是一种垃圾。

可以写一个定时任务,遍历Minio没有后缀的文件块,判断其创建时间距离当前是否足够久,是则删除。

三、代码示例

前端代码

<template><div class="p-2"><el-button icon="Plus" plain type="primary" @click="handleAdd">新增</el-button><!-- 添加或修改media对话框 --><el-dialog v-model="dialog.visible" :title="dialog.title" append-to-body width="500px"><el-form ref="mediaFormRef" :model="form" :rules="rules" label-width="80px"><el-form-item label="上传视频" prop="originalName" v-show="dialog.title=='添加视频'"><el-uploadref="uploadRef":http-request="onUpload":before-upload="beforeUpload":limit="1"action="#"class="upload-demo"><template #trigger><el-button type="primary">选择视频</el-button></template><template #tip><div class="el-upload__tip">支持分块上传、端点续传</div></template></el-upload></el-form-item><el-form-item v-show="percentageShow"><el-progress :percentage="percentage" style="width: 100%"/></el-form-item></el-form></el-dialog></div>
</template><script lang="ts" name="Media" setup>
import type {UploadInstance, UploadRawFile, UploadRequestOptions, UploadUserFile} from 'element-plus'
import SparkMD5 from "spark-md5";
import {HttpStatus} from "@/enums/RespEnum";const dialog = reactive<DialogOption>({visible: false,title: ''
});
//上传视频
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadImgUrl = ref(baseUrl + "/media/media/image"); // 上传的图片服务器地址
const uploadRef = ref<UploadInstance>()
const needUpload = ref(true)
const chunkSize = 5*1024*1024;const percentage = ref(0)
const percentageShow = ref(false)/** 新增按钮操作 */
const handleAdd = () => {dialog.visible = true;dialog.title = "添加视频";percentageShow.value = false;
}//获取文件的MD5
const getFileMd5 = (file:any) => {return new Promise((resolve, reject) => {let fileReader = new FileReader()fileReader.onload = function (event) {let fileMd5 = SparkMD5.ArrayBuffer.hash(event.target.result)resolve(fileMd5)}fileReader.readAsArrayBuffer(file)})
}//在上传之前,使用视频md5判断视频是否已经存在
const beforeUpload = async (rawFile: UploadRawFile) => {needUpload.value = true;const fileMd5 = await getFileMd5(rawFile);form.value.id = fileMd5;const rsp = await getMedia(fileMd5);if(!!rsp.data && rsp.data['id'] == fileMd5){needUpload.value = false;proxy?.$modal.msgWarning("视频文件已存在,请勿重复上传。文件名为"+rsp.data['originalName'])}
}//分块上传、合并分块
const onUpload = async (options: UploadRequestOptions) => {if(!needUpload.value){//秒传percentageShow.value = true;percentage.value = 100;dialog.visible = false;return;}percentageShow.value = true;const file = options.fileconst totalChunks = Math.ceil(file.size / chunkSize);let isUploadSuccess = true;//记录分块文件是否上传成功//合并文件参数let mergeVo = {"chunksMd5": [] as string[],"videoMd5": undefined as string | undefined,"videoName": file.name,"videoSize": file.size,"remark": undefined as string | undefined}//循环切分文件,并上传分块文件for(let i=0; i<totalChunks; ++i){const start = i * chunkSize;const end = Math.min(start + chunkSize, file.size);const chunk = file.slice(start, end);//计算 chunk md5const md5 = await getFileMd5(chunk);mergeVo.chunksMd5.push(md5);// 准备FormDataconst formData = new FormData();formData.append('file', chunk);formData.append('filename', file.name);formData.append('chunkIndex', i.toString());formData.append('totalChunks', totalChunks.toString());formData.append('md5', md5);//上传当前分块try {//先判断这个分块是否已经存在const isExistRsp = await isChunkExist({"md5": formData.get("md5")});const isExist = isExistRsp.data;//不存在则上传if (!isExist){const rsp = await addChunk(formData);console.log(`Chunk ${i + 1}/${totalChunks} uploaded`, rsp.data);}else {console.log(`Chunk ${i + 1}/${totalChunks} is exist`);}percentage.value = (i)*100 / totalChunks;} catch (error) {isUploadSuccess = false;console.error(`Error uploading chunk ${i + 1}`, error);proxy?.$modal.msgError(`上传分块${i + 1}出错`);break;}}//合并分块文件if(isUploadSuccess){proxy?.$modal.msgSuccess("分块文件上传成功")mergeVo.videoMd5 = form.value.id;//beforeUpload已经计算过视频文件的md5//合并文件const rsp = await mergeChunks(mergeVo);if (rsp.code == HttpStatus.SUCCESS){//合并文件后,实际上媒资已经插入数据库。percentage.value = 100;proxy?.$modal.msgSuccess("文件合并成功")proxy?.$modal.msgSuccess("视频上传成功")}else{proxy?.$modal.msgSuccess("文件合并异常")}}else {proxy?.$modal.msgSuccess("文件未上传成功,请重试或联系管理员")}
}</script>
export const getMedia = (id: string | number): AxiosPromise<MediaVO> => {return request({url: '/media/media/' + id,method: 'get'});
};/*** 分块文件是否存在* */
export const isChunkExist = (data: any) => {return request({url: '/media/media/video/chunk',method: 'get',params: data});
};/*** 上传分块文件* */
export const addChunk = (data: any) => {return request({url: '/media/media/video/chunk',method: 'post',data: data});
};/*** 合并分块文件* */
export const mergeChunks = (data: any) => {return request({url: '/media/media/video/chunk/merge',method: 'post',data: data});
};

后端代码

@RestController
@RequestMapping("/media")
public class MediaFilesController extends BaseController {/*** 获取media详细信息** @param id 主键*/@GetMapping("/{id}")public R<MediaFilesVo> getInfo(@NotNull(message = "主键不能为空")@PathVariable String id) {return R.ok(mediaFilesService.queryById(id));}@Log(title = "视频分块文件上传")@PostMapping(value = "/video/chunk")public R<String> handleChunkUpload(@RequestParam("file") MultipartFile file,@RequestParam("md5") String md5,@RequestParam("filename") String filename,@RequestParam("chunkIndex") int chunkIndex,@RequestParam("totalChunks") int totalChunks) {if (ObjectUtil.isNull(file)) {return R.fail("上传文件不能为空");}Boolean b = mediaFilesService.handleChunkUpload(file, md5);if (b){return R.ok();}else {return R.fail();}}@Log(title = "分块文件是否已经存在")@GetMapping(value = "/video/chunk")public R<Boolean> isChunkExist(@RequestParam("md5") String md5) {return R.ok(mediaFilesService.isChunkExist(md5));}@Log(title = "合并视频文件")@PostMapping(value = "/video/chunk/merge")public R<Boolean> mergeChunks(@RequestBody MediaVideoMergeBo bo) {bo.setCompanyId(LoginHelper.getDeptId());Boolean b = mediaFilesService.mergeChunks(bo);if (b){return R.ok();}else {return R.fail();}}
}

关于如何操作Minio等文件系统,不详细写明解释。只需要知道,给Minio提供文件本身、bucketName、path即可完成上传、下载、删除等操作。具体代码不同的包都不一样。

@Service
public class MediaFilesServiceImpl implements MediaFilesService {@Autowiredprivate MediaFilesMapper mediaFilesMapper;/*** 分块文件上传* <br/>* 分块文件不存放mysql信息,同时文件名不含后缀,只有md5* @param file 文件* @param md5  md5* @return {@link Boolean}*/@Overridepublic Boolean handleChunkUpload(MultipartFile file, String md5) {//只上传至minioOssClient storage = OssFactory.instance();String path = getPathByMD5(md5, "");try {storage.upload(file.getInputStream(), path, file.getContentType(), minioProperties.getVideoBucket());} catch (IOException e) {throw new RuntimeException(e);}return true;}@Overridepublic Boolean isChunkExist(String md5) {OssClient storage = OssFactory.instance();String path = getPathByMD5(md5, "");return storage.doesFileExist(minioProperties.getVideoBucket(), path);}@Overridepublic Boolean mergeChunks(MediaVideoMergeBo bo) {OssClient storage = OssFactory.instance();String originalfileName = bo.getVideoName();String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());//创建临时文件,用来存放合并文件String tmpDir = System.getProperty("java.io.tmpdir");String tmpFileName = UUID.randomUUID().toString() + ".tmp";File tmpFile = new File(tmpDir, tmpFileName);try(FileOutputStream fOut = new FileOutputStream(tmpFile);) {//将分块文件以流的形式copy到临时文件List<String> chunksMd5 = bo.getChunksMd5();chunksMd5.forEach(chunkMd5 -> {String chunkPath = getPathByMD5(chunkMd5, "");InputStream chunkIn = storage.getObjectContent(minioProperties.getVideoBucket(), chunkPath);IoUtil.copy(chunkIn, fOut);});//合并文件上传到minioString videoMd5 = bo.getVideoMd5();String path = getPathByMD5(videoMd5, suffix);storage.upload(tmpFile, path, minioProperties.getVideoBucket());//删除分块文件chunksMd5.forEach(chunkMd5->{String chunkPath = getPathByMD5(chunkMd5, "");storage.delete(chunkPath, minioProperties.getVideoBucket());});} catch (Exception e) {throw new RuntimeException(e);}finally {if (tmpFile.exists()){tmpFile.delete();}}//上传信息到mysqlMediaFiles mediaFiles = new MediaFiles();mediaFiles.setId(bo.getVideoMd5());mediaFiles.setCompanyId(bo.getCompanyId());mediaFiles.setOriginalName(originalfileName);mediaFiles.setFileSuffix(suffix);mediaFiles.setSize(bo.getVideoSize());mediaFiles.setPath(getPathByMD5(bo.getVideoMd5(), suffix));mediaFiles.setRemark(bo.getRemark());mediaFiles.setAuditStatus(MediaStatusEnum.UNREVIEWED.getValue());return mediaFilesMapper.insert(mediaFiles) > 0;}/*** 通过md5生成文件路径* <br/>* 比如* md5 = 6c4acb01320a21ccdbec089f6a9b7ca3* <br/>* path = 6/c/md5 + suffix* @param prefix 前缀* @param suffix 后缀* @return {@link String}*/public String getPathByMD5(String md5, String suffix) {// 文件路径String path = md5.charAt(0) + "/" + md5.charAt(1) + "/" + md5;return path + suffix;}}

相关文章:

视频分块上传Vue3+SpringBoot3+Minio

文章目录 一、简化演示分块上传、合并分块断点续传秒传 二、更详细的逻辑和细节问题可能存在的隐患 三、代码示例前端代码后端代码 一、简化演示 分块上传、合并分块 前端将完整的视频文件分割成多份文件块&#xff0c;依次上传到后端&#xff0c;后端将其保存到文件系统。前…...

深入浅出 -- 系统架构之单体到分布式架构的演变

一、传统模式的技术改革 在很多年以前&#xff0c;其实没有严格意义上的前后端工程师之分&#xff0c;每个后端就是前端&#xff0c;同理&#xff0c;前端也可以是后端&#xff0c;即Ajax、jQuery技术未盛行前的年代。 起初&#xff0c;大部分前端界面很简单&#xff0c;显示的…...

每日一题 第七十期 洛谷 [蓝桥杯 2020 省 AB2] 回文日期

[蓝桥杯 2020 省 AB2] 回文日期 题目描述 2020 年春节期间&#xff0c;有一个特殊的日期引起了大家的注意&#xff1a;2020 年 2 月 2 日。因为如果将这个日期按 yyyymmdd 的格式写成一个 8 8 8 位数是 20200202&#xff0c;恰好是一个回文数。我们称这样的日期是回文日期。…...

蓝桥杯第十四届C++A组(未完)

【规律题】平方差 题目描述 给定 L, R&#xff0c;问 L ≤ x ≤ R 中有多少个数 x 满足存在整数 y,z 使得 。 输入格式 输入一行包含两个整数 L, R&#xff0c;用一个空格分隔。 输出格式 输出一行包含一个整数满足题目给定条件的 x 的数量。 样例输入 1 5 样例输出 …...

职场口才提升之道

职场口才提升之道 在职场中&#xff0c;口才的重要性不言而喻。无论是与同事沟通协作&#xff0c;还是向上级汇报工作&#xff0c;亦或是与客户洽谈业务&#xff0c;都需要具备良好的口才能力。一个出色的职场人&#xff0c;除了拥有扎实的专业技能外&#xff0c;还应具备出色…...

【算法练习】28:选择排序学习笔记

一、选择排序的算法思想 弄懂选择排序算法&#xff0c;先得知道两个概念&#xff1a;未排序序列&#xff0c;已排序序列。 原理&#xff1a;以升序为例&#xff0c;选择排序算法的思想是&#xff0c;先将整个序列当做未排序的序列&#xff0c;以序列的第一个元素开始。然后从左…...

【关于窗口移动求和的两种计算方法】

窗口移动计算方法 例子方法1方法2运行结果: 例子 在很多算法中都会涉及到窗口滑动&#xff0c;比如基于新息序列更新的自适应卡尔曼滤波器算法中便会使用到。 已知一个数列&#xff1a;OCV [1;2;3;4;5;6;7;8;9;10;11;12;13;14;15]&#xff0c;定义窗口长度为5&#xff0c;每次…...

Win10文件夹共享(有密码的安全共享)(SMB协议共享)

前言 局域网内&#xff08;无安全问题&#xff0c;比如自己家里wifi&#xff09;无密码访问&#xff0c;参考之前的操作视频 【电脑文件全平台共享、播放器推荐】手机、电视、平板播放硬盘中的音、视频资源 下面讲解公共网络如办公室网络、咖啡厅网络等等环境下带密码的安全…...

Client sent an HTTP request to an HTTPS server

背景 最近踩坑了 我发现域名&#xff1a;8000可以访问我的服务 但是域名&#xff1a;443却不行&#xff0c;这很反常 结果发现是nginx配置的问题&#xff0c;需要把http改成https&#xff01; 原因 如果你的后端服务&#xff08;运行在8000端口上&#xff09;已经配置了SS…...

Springboot传参要求

Web.java(这里定义了一个实体类交Web) public class Web{ private int Page; public int getPage() {return Page;}public void setPage(int page) {Page page;} } 1、通过编译器自带的getter、Setter传参 。只是要注意参数的名字是固定的&#xff0c;不能灵活改变。 传参的…...

数字乡村创新实践探索:科技赋能农业现代化与乡村治理体系现代化同步推进

随着信息技术的飞速发展&#xff0c;数字乡村作为乡村振兴的重要战略方向&#xff0c;正日益成为推动农业现代化和乡村治理体系现代化的关键力量。科技赋能下的数字乡村&#xff0c;不仅提高了农业生产的效率和品质&#xff0c;也为乡村治理带来了新的机遇和挑战。本文旨在探讨…...

C语言——找单身狗1

题目描述&#xff1a; 在一个整形数组中&#xff0c;只有一个数字出现一次&#xff0c;其他数组都是成对出现的&#xff0c;找出那个只出现一次的数字。 例如&#xff1a; 数组中&#xff1a;1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;5&#xff0c;4&#xff0c;3…...

Day82:服务攻防-开发组件安全Solr搜索Shiro身份Log4j日志本地CVE环境复现

目录 J2EE-组件Solr-本地demo&CVE 命令执行&#xff08;CVE-2019-17558&#xff09; 远程命令执行漏洞(CVE-2019-0193) Apache Solr 文件读取&SSRF (CVE-2021-27905) J2EE-组件Shiro-本地demo&CVE CVE_2016_4437 Shiro-550Shiro-721(RCE) CVE-2020-11989(身…...

网络协议——VRRP(虚拟路由冗余协议)原理与配置

1. VRRP概述 单网关出现故障后下联业务中断&#xff0c;配置两个及以上的网关时由于IP地址冲突&#xff0c;导致通讯时断时续甚至通信中断。VRRP组播类的网络层协议 2. 协议版本 VRRP v2: 支持认证,仅适用于IPv4网络 VRRP v3: 不支持认证&#xff0c; 适用于IPv4和IPv6两种网…...

Elasticsearch:我们如何演化处理二进制文档格式

作者&#xff1a;来自 Elastic Sean Story 从二进制文件中提取内容是一个常见的用例。一些 PDF 文件可能非常庞大 — 考虑到几 GB 甚至更多。Elastic 在处理此类文档方面已经取得了长足的进步&#xff0c;今天&#xff0c;我们很高兴地介绍我们的新工具 —— 数据提取服务&…...

第八讲 Sort Aggregate 算法

我们现在将讨论如何使用迄今为止讨论过的 DBMS 组件来执行查询。 1 查询计划【Query Plan】 我们首先来看当一个查询【Query】被解析【Parsed】后会发生什么&#xff1f; 当 SQL 查询被提供给数据库执行引擎&#xff0c;它将通过语法解析器进行检查&#xff0c;然后它会被转换…...

clickhouse MPPDB数据库--新特性使用示例

clickhouse 新特性&#xff1a; 从clickhouse 22.3至最新的版本24.3.2.23&#xff0c;clickhouse在快速发展中&#xff0c;每个版本都增加了一些新的特性&#xff0c;在数据写入、查询方面都有性能加速。 本文根据clickhouse blog中的clickhouse release blog中&#xff0c;学…...

MATLAB多级分组绘图及图例等细节处理 ; MATLAB画图横轴时间纵轴数值按照不同sensorCode分组画不同sensorCode的曲线

平时研究需要大量的绘图Excel有时候又臃肿且麻烦 尤其是当处理大量数据时可能会拖死Windows 示例代码及数据量展示 因为数据量是万级别的折线图也变成"柱状图"了, 不过还能看出大致趋势! 横轴是时间纵轴是传感器数值图例是传感器所在深度 % data readtable(C:\U…...

20240405,数据类型,运算符,程序流程结构

是我深夜爆炸&#xff0c;不能再去补救C了&#xff0c;真的来不及了&#xff0c;不能再三天打鱼两天晒网了&#xff0c;真的来不及了呜呜呜呜 我实在是不知道看什么课&#xff0c;那黑马吧……MOOC的北邮的C正在进行呜呜 #include <iostream> using namespace std; int…...

Prometheus+grafana环境搭建Nginx(docker+二进制两种方式安装)(六)

由于所有组件写一篇幅过长&#xff0c;所以每个组件分一篇方便查看&#xff0c;前五篇链接如下 Prometheusgrafana环境搭建方法及流程两种方式(docker和源码包)(一)-CSDN博客 Prometheusgrafana环境搭建rabbitmq(docker二进制两种方式安装)(二)-CSDN博客 Prometheusgrafana环…...

高防服务器能够抵御哪些网络攻击呢?

高防服务器作为一种有着高度防御能力的服务器&#xff0c;可以帮助网站应对分布式拒绝服务攻击&#xff0c;有效识别和清理一些恶意的网络流量&#xff0c;为用户提供安全且稳定的网络环境&#xff0c;那么&#xff0c;高防服务器一般都可以抵御哪些网络攻击呢&#xff1f;下面…...

均衡后的SNRSINR

本文主要摘自参考文献中的前两篇&#xff0c;相关文献中经常会出现MIMO检测后的SINR不过一直没有找到相关数学推到过程&#xff0c;其中文献[1]中给出了相关原理在此仅做记录。 1. 系统模型 复信道模型 n t n_t nt​ 根发送天线&#xff0c; n r n_r nr​ 根接收天线的 MIMO 系…...

Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信

文章目录 Linux C语言网络编程详细入门教程&#xff1a;如何一步步实现TCP服务端与客户端通信前言一、网络通信基础概念二、服务端与客户端的完整流程图解三、每一步的详细讲解和代码示例1. 创建Socket&#xff08;服务端和客户端都要&#xff09;2. 绑定本地地址和端口&#x…...

QT3D学习笔记——圆台、圆锥

类名作用Qt3DWindow3D渲染窗口容器QEntity场景中的实体&#xff08;对象或容器&#xff09;QCamera控制观察视角QPointLight点光源QConeMesh圆锥几何网格QTransform控制实体的位置/旋转/缩放QPhongMaterialPhong光照材质&#xff08;定义颜色、反光等&#xff09;QFirstPersonC…...

vulnyx Blogger writeup

信息收集 arp-scan nmap 获取userFlag 上web看看 一个默认的页面&#xff0c;gobuster扫一下目录 可以看到扫出的目录中得到了一个有价值的目录/wordpress&#xff0c;说明目标所使用的cms是wordpress&#xff0c;访问http://192.168.43.213/wordpress/然后查看源码能看到 这…...

微服务通信安全:深入解析mTLS的原理与实践

&#x1f525;「炎码工坊」技术弹药已装填&#xff01; 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、引言&#xff1a;微服务时代的通信安全挑战 随着云原生和微服务架构的普及&#xff0c;服务间的通信安全成为系统设计的核心议题。传统的单体架构中&…...

机器学习的数学基础:线性模型

线性模型 线性模型的基本形式为&#xff1a; f ( x ) ω T x b f\left(\boldsymbol{x}\right)\boldsymbol{\omega}^\text{T}\boldsymbol{x}b f(x)ωTxb 回归问题 利用最小二乘法&#xff0c;得到 ω \boldsymbol{\omega} ω和 b b b的参数估计$ \boldsymbol{\hat{\omega}}…...

Axure零基础跟我学:展开与收回

亲爱的小伙伴,如有帮助请订阅专栏!跟着老师每课一练,系统学习Axure交互设计课程! Axure产品经理精品视频课https://edu.csdn.net/course/detail/40420 课程主题:Axure菜单展开与收回 课程视频:...

【系统架构设计师-2025上半年真题】综合知识-参考答案及部分详解(回忆版)

更多内容请见: 备考系统架构设计师-专栏介绍和目录 文章目录 【第1题】【第2题】【第3题】【第4题】【第5题】【第6题】【第7题】【第8题】【第9题】【第10题】【第11题】【第12题】【第13题】【第14题】【第15题】【第16题】【第17题】【第18题】【第19题】【第20~21题】【第…...

Centos 7 服务器部署多网站

一、准备工作 安装 Apache bash sudo yum install httpd -y sudo systemctl start httpd sudo systemctl enable httpd创建网站目录 假设部署 2 个网站&#xff0c;目录结构如下&#xff1a; bash sudo mkdir -p /var/www/site1/html sudo mkdir -p /var/www/site2/html添加测试…...