SpringBoot集成Minio实现上传凭证、分片上传、秒传和断点续传

总体概述

Spring Boot整合Minio后,前端的文件上传有两种方式:
1.文件上传到后端,由后端保存到Minio
这种方式好处是完全由后端集中管理,可以很好的做到、身份验证、权限控制、文件与处理等,并且可以做一些额外的业务逻辑,比如生成缩略图、提取元数据等。
缺点也很明显:

  •  延迟时间高了,本来花费上传一次文件的时间,现在多了后端保存到Minio的时间
    
  •  后端资源占用,后端本来可以只处理业务请求,现在还要负责文件流,增加了性能压力
    
  •  单点故障,Minio即便做了集群,但是如果后端服务器故障,也会导致Minio不可用
    

所以,实际上我们不会把文件传到后端,而是直接传给Minio,其实这也符合OSS服务的使用方式。
2.文件向后端申请上传凭证,然后直接上传到Minio
为了避免Minio被攻击,我们需要结合后端,让后端生成并返回一个有时效的上传凭证,前端拿着这个凭证才能去上传,通过这种方式,我们可以做到一定程度的权限控制,本文要分享的就是这种方式。
环境准备
部署Minio环境:分布式文件服务器 ——MinIO单机模式CentOS环境部署

SpringBoot集成Minio

引入Minio依赖

在项目根目录下的pom.xml文件中配置Minio依赖

<dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>7.1.0</version>
</dependency>

配置Minio

在项目配置文件application.yml中配置Minio

minio:endpoint: http://mylocalhost:9001accessKey: miniosecretKey: minio123bucket: demo

定义属性类

@Component
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {/*** 对象存储服务的URL*/private String endpoint;/*** Access key就像用户ID,可以唯一标识你的账户*/private String accessKey;/*** Secret key是你账户的密码*/private String secretKey;/*** 默认文件桶*/private String bucket;...
}

定义Minio配置类

@Configuration
public class MinioConfig {@Beanpublic MinioClient minioClient(MinioProperties properties){try {MinioClient.Builder builder = MinioClient.builder();builder.endpoint(properties.getEndpoint());if (StringUtils.hasLength(properties.getAccessKey()) && StringUtils.hasLength(properties.getSecretKey())) {builder.credentials(properties.getAccessKey(),properties.getSecretKey());}return builder.build();} catch (Exception e) {return null;}}
}

至此,SpringBoot集成Minio完成,启动服务即可。

上传凭证

写一个接口,返回上传凭证,代码如下

@RequestMapping(value = "/presign", method = {RequestMethod.POST})
public Map<String, String> presign(@RequestBody PresignParam presignParam) {// 如果前端不指定桶,那么给一个默认的if (StringUtils.isEmpty(presignParam.getBucket())) {presignParam.setBucket("demo");}// 前端不指定文件名称,就给一个UUIDif (StringUtils.isEmpty(presignParam.getFilename())) {presignParam.setFilename(UUID.randomUUID().toString());}// 如果想要以子目录的方式保存,就在前面加上斜杠来表示//        presignParam.setFilename("/2023/" + presignParam.getFilename());// 设置凭证过期时间ZonedDateTime expirationDate = ZonedDateTime.now().plusMinutes(10);// 创建一个凭证PostPolicy policy = new PostPolicy(presignParam.getBucket(), presignParam.getFilename(), expirationDate);// 限制文件大小,单位是字节byte,也就是说可以设置如:只允许10M以内的文件上传//        policy.setContentRange(1, 10 * 1024);// 限制上传文件请求的ContentType//        policy.setContentType("image/png");try {// 生成凭证并返回final Map<String, String> map = minioClient.presignedPostPolicy(policy);for (Map.Entry<String, String> entry : map.entrySet()) {System.out.println(entry.getKey() + " = " + entry.getValue());}return map;} catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException e) {e.printStackTrace();}return null;
}

从上面的示例代码可以知道,我们还可以加一些权限认证,以判断用户是否有以下权限:

  • 上传权限
  • 可上传的文件大小
  • 可上传的文件类型

请求参数类:

public class PresignParam {// 桶名private String bucket;// 文件名private String filename;...
}

这个接口的返回结果是:

bucket: demo
x-amz-date: 20230831T042351Z
x-amz-signature: 79cc2ae0baee274d1d47cb29bdd5e99127059033503c2a02f904f0478a73ecac
key: 边城往事.mp4
x-amz-algorithm: AWS4-HMAC-SHA256
x-amz-credential: minio/20230831/us-east-1/s3/aws4_request
policy: eyJleHBpcmF0aW9uIjoiMjAyMy0wOC0zMVQwNDozMzo1MS42MzZaIiwiY29uZGl0aW9ucyI6W1siZXEiLCIkYnVja2V0IiwiZGVtbyJdLFsiZXEiLCIka2V5Iiwi5a+C5a+e55qE5a2j6IqCLm1wNCJdLFsiZXEiLCIkeC1hbXotYWxnb3JpdGhtIiwiQVdTNC1ITUFDLVNIQTI1NiJdLFsiZXEiLCIkeC1hbXotY3JlZGVudGlhbCIsIm1pbmlvLzIwMjMwODMxL3VzLWVhc3QtMS9zMy9hd3M0X3JlcXVlc3QiXSxbImVxIiwiJHgtYW16LWRhdGUiLCIyMDIzMDgzMVQwNDIzNTFaIl1dfQ==
  • bucket:表示目标桶
  • x-amz-date:时间戳
  • x-amz-signature:签名
  • key:文件名
  • x-amz-algorithm:签名算法
  • x-amz-credential:认证授权
  • policy:凭证token

前端收到后,将该凭证连同文件流一并上传到Minio服务器:

uploadFile(file, policy) {console.log("准备上传文件:")console.log("file:" + file)console.log("policy:" + policy)var formData = new FormData()formData.append('file', file)formData.append('key', policy['key'])formData.append('x-amz-algorithm', policy['x-amz-algorithm'])formData.append('x-amz-credential', policy['x-amz-credential'])formData.append('x-amz-signature', policy['x-amz-signature'])formData.append('x-amz-date', policy['x-amz-date'])formData.append('policy', policy['policy'])return new Promise(((resolve, reject) => {$.ajax({method: 'POST',url: 'http://mylocalhost:9001/' + policy['bucket'],data: formData,dataType: 'json',contentType: false, // 必须设置为 false,不设置 contentType,让浏览器自动设置processData: false, // 必须设置为 false,不对 FormData 进行序列化处理// async: false, // 设置同步,方便等下做分片上传xhr: function xhr() {//获取原生的xhr对象var xhr = $.ajaxSettings.xhr();if (xhr.upload) {//添加 progress 事件监听xhr.upload.addEventListener('progress', function (e) {//e.loaded 已上传文件字节数//e.total 文件总字节数var percentage = parseInt(e.loaded / e.total * 100)vm.uploadResult = percentage + "%" + ":" + policy['key']}, false);}return xhr;},success: function (result) {vm.uploadResult = '文件上传成功:' + policy['key']resolve(result)},error: function (e) {reject()}})}))
},

这样就完成了获取上传凭证并上传文件。

分片上传、妙传、断点续传

分片上传

原理:将大文件分割成多个较小的片段(称为分片),然后分别上传这些分片。这样可以避免一次性传输大文件导致的超时、网络不稳定等问题。每个分片可以独立上传,并且在服务器端可以根据一定的规则重新组合成完整的文件。

案例:分片上传可以用在大文件上传上,一个100M的文件可以分成10份,每份10M,一共传输10次,

优点:Minio做了集群,用Nginx转发,那么分片上传可以降低单台Minio服务器的性能压力多线程上传可以加快上传效率。

秒传

原理:在上传文件之前,先计算文件的唯一标识,通常是通过计算文件的哈希值(如 MD5)。服务器端会检查是否已经存在具有相同哈希值的文件。如果存在,则直接认为文件已上传成功,无需再次传输实际的文件内容。

案例:我们上传一个文件之前,可以用工具生成MD5字符串,就好像这样:

3cc1f3c3c2d1a29ecf60ffad4de278c7

然后拼接上文件名:

3cc1f3c3c2d1a29ecf60ffad4de278c7_边城往事.mp4

这时候去向后端申请上传凭证的时候,后端可以先去看看文件是否已存在,如果文件已存在,就不用生成凭证了,直接告诉前端该文件已经上传完毕,由此实现文件秒传。

优点

  • 节省上传时间和带宽,对于重复的文件无需再次上传
  • 降低Minio服务器压力,响应秒回,用户体验提高。

断点续传

原理:在上传过程中断后,记录已经上传的分片信息。当下次继续上传时,客户端告知服务器已经上传的部分,服务器根据这些信息从上次中断的位置继续接收分片,而不是重新开始上传。

案例:结合分片上传和秒传的原理,我们可以来做到断点续传,当我们要上传一个大文件的时候,进度到一半了,这时候网络掉线导致上传失败,网络恢复后又要重新上传,这就很崩溃。

处理方式: 大文件也可以分成一个个小文件来上传,这样即便上传到一半网络掉线,恢复上传的时候可以跳过前一半已上传的部分,接着上传后面一半。

优点:在网络不稳定或其他原因导致上传中断时,无需从头开始上传,提高上传效率。

文件合并

当我们完成分片上传后,后端还需要提供接口,来将所有分片数据合并:示例代码如下:

@GetMapping("/compose")
public void merge() {List<ComposeSource> sources = new ArrayList<>();// 分片数据放到另一个桶里面:slicesources.add(ComposeSource.builder().bucket("slice").object("0边城往事.mp4").build());sources.add(ComposeSource.builder().bucket("slice").object("1边城往事.mp4").build());sources.add(ComposeSource.builder().bucket("slice").object("2边城往事.mp4").build());final ComposeObjectArgs args = ComposeObjectArgs.builder().bucket("demo").object("边城往事.mp4").sources(sources).build();try {minioClient.composeObject(args);} catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException e) {e.printStackTrace();}
}

前端需要传的参数是:

分片桶:slice
分片数据数组:0边城往事.mp41边城往事.mp42边城往事.mp4
目标桶:demo

然后调用composeObject函数完成合并。

前端示例代码分享

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title><script src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.14/vue.js"></script><script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.js"></script><script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
</head>
<body>
<div id="app"><h1>{{title}}</h1><br><form @submit.prevent="getPolicyForm"><label>桶名<input type="text" v-model="policyParams.bucket"></label><br><label>文件名<input type="text" v-model="policyParams.filename"></label><br><button type="submit">获取上传凭证</button><br><div v-for="(val, key) in policy" :key="key">{{ key }}: <span>{{ val }}</span></div></form><br><form @submit.prevent="uploadFileForm" v-show="policy != null"><label>文件<input type="file" @change="fileChange"></label><br><br><button type="submit" v-show="file != null">上传文件</button></form>---<br><div v-show="file != null"><button @click="sliceEvent">测试文件分片上传</button>|<button @click="sliceComposeEvent">分片文件合并</button></div><br><br><br><p>{{uploadResult}}</p><ul><!--        <li v-for="item in sliceUploadResult">{{ item }}</li>--><li v-for="(item, index) in sliceUploadResult" :key="index">{{ item }}</li></ul><br></div><script>var vm = new Vue({el: "#app",data() {return {title: "Minio测试"// 请求凭证参数, policyParams: {bucket: null, filename: null}// 请求到的凭证, policy: null// 待上传文件, file: null// 上传文件参数, uploadParams: {file: null}// 分片上传参数, sliceParams: {bucket: "", filename: "", file: null}, slicePolicys: [], sliceCount: 0// 上传结果回调, uploadResult: null// 分片上传结果回调, sliceUploadResult: null};},methods: {getPolicyForm() {this.policyParams.bucket = "demo"this.policyParams.filename = "寂寞的季节.mp4"this.requestPolicy(this.policyParams)},requestPolicy(params) {return new Promise(((resolve, reject) => {$.ajax({type: "POST",url: "http://localhost:8888/presign",contentType: "application/json",data: JSON.stringify(params),// async: false,success: function (result) {console.log(result)vm.policy = result;resolve(result)},error: function (e) {reject()}});}))},fileChange(event) {const file = event.target.files[0]this.file = file},uploadFileForm() {this.uploadFile(this.file, this.policy)},uploadFile(file, policy) {console.log("准备上传文件:")console.log("file:" + file)console.log("policy:" + policy)var formData = new FormData()formData.append('file', file)formData.append('key', policy['key'])formData.append('x-amz-algorithm', policy['x-amz-algorithm'])formData.append('x-amz-credential', policy['x-amz-credential'])formData.append('x-amz-signature', policy['x-amz-signature'])formData.append('x-amz-date', policy['x-amz-date'])formData.append('policy', policy['policy'])return new Promise(((resolve, reject) => {$.ajax({method: 'POST',url: 'http://mylocalhost:9001/' + policy['bucket'],data: formData,dataType: 'json',contentType: false, // 必须设置为 false,不设置 contentType,让浏览器自动设置processData: false, // 必须设置为 false,不对 FormData 进行序列化处理// async: false, // 设置同步,方便等下做分片上传xhr: function xhr() {//获取原生的xhr对象var xhr = $.ajaxSettings.xhr();if (xhr.upload) {//添加 progress 事件监听xhr.upload.addEventListener('progress', function (e) {//e.loaded 已上传文件字节数//e.total 文件总字节数var percentage = parseInt(e.loaded / e.total * 100)vm.uploadResult = percentage + "%" + ":" + policy['key']}, false);}return xhr;},success: function (result) {vm.uploadResult = '文件上传成功:' + policy['key']resolve(result)},error: function (e) {reject()}})}))},sliceEvent() {// 获取文件var file = this.file// 设置分片大小:5MBvar chunkSize = 5 * 1024 * 1024// 计算总共有多少个分片var totalChunk = Math.ceil(file.size / chunkSize)// 数组存放所有分片var chunks = []// 遍历所有分片for (var i = 0; i < totalChunk; i++) {// 利用slice获取分片var start = i * chunkSizevar end = Math.min(file.size, start + chunkSize)var blob = file.slice(start, end)// 添加分片到数组chunks.push(blob)}console.log(totalChunk)this.sliceUploadResult = Array(totalChunk).fill(0)for (let i = 0; i < chunks.length; i++) {var file = chunks[i];this.calculateMD5(file).then((md5) => {console.log(md5);  // 输出计算出的 MD5 值}).catch((error) => {console.error(error);  // 处理错误});}return// 创建序号var index = 0;// 循环上传分片while (index < totalChunk) {console.log('------------------------------')params = {"bucket": "slice","filename": index + "寂寞的季节.mp4"}var policyPromise = this.requestPolicy(params);(function (index) {var file = chunks[index]policyPromise.then(function (result) {var filename = result['key']console.log('准备上传文件:', filename, ',序号为:', index)vm.uploadFile(file, result).then(function (result) {console.log('上传完成:' + filename)vm.sliceUploadResult[index] = ('分片文件上传成功:' + filename)})})})(index)index++}},sliceComposeEvent() {var parmas = {}$.ajax({method: 'POST',url: 'http://localhost:8888/compose',data: formData,dataType: 'json',contentType: false, // 必须设置为 false,不设置 contentType,让浏览器自动设置processData: false, // 必须设置为 false,不对 FormData 进行序列化处理// async: false, // 设置同步,方便等下做分片上传xhr: function xhr() {//获取原生的xhr对象var xhr = $.ajaxSettings.xhr();if (xhr.upload) {//添加 progress 事件监听xhr.upload.addEventListener('progress', function (e) {//e.loaded 已上传文件字节数//e.total 文件总字节数var percentage = parseInt(e.loaded / e.total * 100)vm.uploadResult = percentage + "%" + ":" + policy['key']}, false);}return xhr;},success: function (result) {vm.uploadResult = '文件上传成功:' + policy['key']resolve(result)},error: function (e) {reject()}})},calculateMD5(file) {return new Promise((resolve, reject) => {const reader = new FileReader();// 读取文件内容reader.readAsArrayBuffer(file);reader.onload = () => {const spark = new SparkMD5.ArrayBuffer();spark.append(reader.result);  // 将文件内容添加到 MD5 计算器中const md5 = spark.end();  // 计算 MD5 值resolve(md5);};reader.onerror = (error) => {reject(error);};});}},mounted() {},created() {},});</script>
</body>
</html>

至此,SpringBoot集成Minio实现上传凭证、分片上传、秒传和断点续传主要方案分享完毕,如有不解或疑问的请在评论区探讨,谢谢🙏

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

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

相关文章

『 Linux 』网络层 - IP协议 (二)

文章目录 路由NAT技术分片与组装分片的组装IP协议分片的短板 路由 通常情况路由器具备了一个非常重要的功能,即构建子网; 同时路由器需要实现跨网络通信,说明路由器必须存在两个或以上的IP地址,通常在路由器中可以看到几个接口,分别是一个WAN口和几个LAN口; WAN口IP被称为公网I…

深度学习实战图像缺陷修复

这里写目录标题 概述1. 图像缺陷修复的研究背景2. 传统图像缺陷修复方法的局限性(1) 基于纹理合成的方法(2) 基于偏微分方程&#xff08;PDE&#xff09;的方法 3. 深度学习在图像缺陷修复中的兴起(1) 深度学习的基本思路(2) 深度学习方法的优势(3) 关键技术的引入 4. 深度学习…

vue从入门到精通(七):事件处理

1&#xff0c;事件的基本使用 1.使用v-on:xxx或 xxx 绑定事件&#xff0c;其中xxx是事件名2.事件的回调需要配置在methods对象中&#xff0c;最终会在vm上3.methods中配置的所数&#xff0c;不要用箭头函数!否则this就不是vm了4.methods中配置的函数&#xff0c;都是被Vue所管…

如何在 .gitignore 中仅保留特定文件:以忽略文件夹中的所有文件为例

在日常的开发工作中&#xff0c;使用 Git 来管理项目是不可或缺的一部分。项目中的某些文件夹可能包含大量的临时文件、生成文件或不需要版本控制的文件。在这种情况下&#xff0c;我们通常会使用 .gitignore 文件来忽略这些文件夹。然而&#xff0c;有时我们可能希望在忽略整个…

【SQL实验】索引操作(菜单操作和命令操作)

【代码是自己的解答&#xff0c;并非标准答案&#xff0c;也有可能写错&#xff0c;文中可能会有不准确或待完善之处&#xff0c;恳请各位读者不吝批评指正&#xff0c;共同促进学习交流】 文件”成绩管理”导入【具体操作前几篇文章详细展示过来&#xff0c;这里跳过。还是不太…

MVC 模型:架构与原理

MVC 模型:架构与原理 MVC(Model-View-Controller)模型是一种广泛应用于软件工程的架构模式,主要用于分离应用程序的逻辑层,以提高其可维护性和可扩展性。MVC模型将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。本文将深入探讨MVC模型的…

[pdf,epub]162页《分析模式》漫谈合集01-35提供下载

《分析模式》漫谈合集01-35的pdf、epub文件&#xff0c;已上传至本号的CSDN资源。 如果CSDN资源下载有问题&#xff0c;可到umlchina.com/url/ap.html。 已排版成适合手机阅读&#xff0c;pdf的排版更好一些。 ★UMLChina为什么叒要翻译《分析模式》&#xff1f; ★[缝合故事…

【Linux学习】【Ubuntu入门】1-7 ubuntu下磁盘管理

1.准备一个U盘或者SD卡&#xff08;插上读卡器&#xff09;&#xff0c;将U盘插入主机电脑&#xff0c;右键点击属性&#xff0c;查看U盘的文件系统确保是FAT32格式 2.右键单击ubuntu右下角图标&#xff0c;将U盘与虚拟机连接 参考链接 3. Ubuntu磁盘文件&#xff1a;/dev/s…

Leetcode152. 乘积最大子数组(HOT100)

链接 代码&#xff1a; class Solution { public:int maxProduct(vector<int>& nums) {int f nums[0],g nums[0];int res nums[0];for(int i 1;i<nums.size();i){//int i 1 not int i 0 ,因为我们已经初始化好了首元素作为子数组的最大值和最小值int a n…

移远通信推出全新5G RedCap模组RG255AA系列,以更高性价比加速5G轻量化大规模商用

11月20&#xff0c;全球领先的物联网整体解决方案供应商移远通信宣布&#xff0c;正式推出其全新5G RedCap模组RG255AA系列。该系列模组支持5G NR独立组网&#xff08;SA&#xff09;和LTE Cat 4双模通信&#xff0c;具有高性能高集成度、低功耗、小尺寸、高性价比等优势&#…

【CVE-2024-9413】SCP-Firmware漏洞:安全通告

安全之安全(security)博客目录导读 目录 一、概述 二、修订历史 三、CVE根因分析 四、问题修复解决 一、概述 在SCP固件中发现了一个漏洞,如果利用该漏洞,可能会允许应用处理器(AP)在系统控制处理器(SCP)固件中导致缓冲区溢出。 CVE IDCVE-2024-9413受影响的产品SC…

数据集-目标检测系列- 花卉 玫瑰 检测数据集 rose >> DataBall

数据集-目标检测系列- 花卉 玫瑰 检测数据集 rose >> DataBall DataBall 助力快速掌握数据集的信息和使用方式&#xff0c;会员享有 百种数据集&#xff0c;持续增加中。 贵在坚持&#xff01; 数据样例项目地址&#xff1a; * 相关项目 1&#xff09;数据集可视化项…

详解Qt QTimeZone 时区类

文章目录 QTimeZone 详解前言什么是 QTimeZone&#xff1f;QTimeZone 的构造函数和常用成员函数构造函数1. 默认构造函数2. 指定时区 ID 构造函数3. 根据 UTC 偏移量构造 常用成员函数1. 获取时区 IDid 2. 检查时区是否有效isValid 3. 获取 UTC 偏移量offsetFromUtc 4. 检查是否…

Linux应用编程(C语言编译过程)

目录 1. 举例 2.预处理 2.1 预处理命令 2.2 .i文件内容解读 3.编译 4.汇编 5.链接 5.1 链接方式 5.1.1 静态链接 5.1.2 动态链接 5.1.3 混合链接 1. 举例 Linux的C语言开发&#xff0c;一般选择GCC工具链进行编译&#xff0c;通过下面的例子来演示GCC如何使用&#…

GitHub 开源项目 Puter :云端互联操作系统

每天面对着各种云盘和在线应用&#xff0c;我们常常会遇到这样的困扰。 文件分散在不同平台很难统一管理&#xff0c;付费订阅的软件越来越多&#xff0c;更不用说那些烦人的存储空间限制了。 最近在 GitHub 上发现的一个开源项目 Puter 彻底改变了我的在线办公方式。 让人惊…

Python 使用 OpenCV 将 MP4 转换为 GIF图

以下是使用 Python 和 OpenCV 将 MP4 转换为 GIF 的示例代码&#xff1a; python import cv2 import imageiodef mp4_to_gif(mp4_path, gif_path, fps10, start_timeNone, end_timeNone):"""将MP4视频转换为GIF动图。:param mp4_path: 输入MP4视频的路径。:pa…

el-table的树形结构后端返回的id没有唯一键怎么办

前端自己生成唯一键 首先尝试了表格的几个字段用-拼接成唯一键 但是仍报错 只好自己利用uuid库生成&#xff1b;

【Linux】缓冲区/磁盘inode/动静态库

目录 一、缓冲区 &#xff08;一&#xff09;概念 &#xff08;二&#xff09;刷新策略 &#xff08;三&#xff09;仿写FILE &#xff08;四&#xff09;内核缓冲区 二、磁盘 &#xff08;一&#xff09;磁盘的存储 &#xff08;二&#xff09;磁盘的抽象存储结构 &am…

SpringBoot(9)-Dubbo+Zookeeper

目录 一、了解分布式系统 二、RPC 三、Dubbo 四、SpringBootDubboZookeeper 4.1 框架搭建 4.2 实现RPC 一、了解分布式系统 分布式系统&#xff1a;由一组通过网络进行通信&#xff0c;为了完成共同的任务而协调工作的计算机节点组成的系统 二、RPC RPC&#xff1a;远程…

【Github】如何使用Git将本地项目上传到Github

【Github】如何使用Git将本地项目上传到Github 写在最前面1. 注册Github账号2. 安装Git工具配置用户名和邮箱仅为当前项目配置&#xff08;可选&#xff09; 3. 创建Github仓库4. 获取仓库地址5. 本地操作&#xff08;1&#xff09;进入项目文件夹&#xff08;2&#xff09;克隆…