Spring Cloud 项目总结
Spring Cloud + Vue 前后端分离 开发企业级在线视频课程系统
代码:https://gitee.com/hiszm/online-course
总体架构
技术选型
代码部分
admin -- web后台管理页面
business -- 核心模块
doc/db -- 数据库文件
eureka -- 注册中心
file -- 核心模块
gateway -- 网关
generator -- 代码生成器
server -- 公共模块
system -- 核心模块
web -- web首页
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
system
- 权限管理
- 角色管理
gateway
SpringCloud
的网关组件可以用gateway
或zuul
;最早使用的是zuul
,后面spring
自己出了gateway
网关主要功能:
-
限流(流量控制);
-
重试(请求失败时重试,慎用);
-
跨域(前后端不在同一个域);
-
路由(转发请求);
-
鉴权(登录校验,签名校验)等…
-
单点登录
IP-HASH
A:request.getSession()
可以访问这片空间
B:节点中,没有经历过之前的登录,Session
中没有保存登录用户信息,于是B节点会认为未登录过,会拦截掉业务请求
缺点,当其中一个节点宕机,则该节点下的用户需要重新登录;另个缺点是对SLB
不能灵活的配置流量,比如A这台机器性能好一 点,可以分配流量高一-些
服务器负载均衡(
Server Load Balancing
)
种类
-
基于DNS负载均衡
其原理就是当用户访问域名的时候,会先向 DNS 服务器去解析域名对应的 IP 地址,这个时候我们可以让 DNS 服务器根据不同地理位置的用户返回不同的 IP。比如南方的用户就返回我们在广州业务服务器的 IP,北方的用户来访问的话,我就返回北京业务服务器所在的 IP。
用户就相当于实现了按照「就近原则」将请求分流了 -
优点
使用 DNS 做负载均衡的方案,天然的优势就是配置简单,实现成本非常低,无需额外的开发和维护工作。
-
缺点
但是也有一个明显的缺点是:当配置修改后,生效不及时。这个是由于 DNS 的特性导致的,DNS 一般会有多级缓存,所以当我们修改了 DNS 配置之后,由于缓存的原因,会导致 IP 变更不及时,从而影响负载均衡的效果。
另外,使用 DNS 做负载均衡的话,大多是基于地域或者干脆直接做 IP 轮询,没有更高级的路由策略,所以这也是 DNS 方案的局限所在。 -
基于硬件负载均衡
比如大名鼎鼎的 F5 Network Big-IP,也就是我们常说的 F5,它是一个网络设备,你可以简单的理解成类似于网络交换机的东西,完全通过硬件来抗压力,性能是非常的好,每秒能处理的请求数达到百万级,即 几百万 / 秒 的负载,
当然价格也就非常非常贵了,十几万到上百万人民币都有。
- 基于软件负载均衡
软件负载均衡是指使用软件的方式来分发和均衡流量。
软件负载均衡,分为 7 层协议 和 4 层协议。
网络协议有七层,
基于第四层传输层来做流量分发的方案称为 4 层负载均衡,例如 LVS,
基于第七层应用层来做流量分发的称为 7 层负载均衡,例如 Nginx。
这两种在性能和灵活性上是有些区别的
基于 4 层的负载均衡性能要高一些,一般能达到 几十万 / 秒 的处理量,
基于 7 层的负载均衡处理量一般只在 几万 / 秒 。
基于软件的负载均衡的特点也很明显,便宜。
在正常的服务器上部署即可,无需额外采购,就是投入一点技术去优化优化即可,因此这种方式是互联网公司中用得最多的一种方式。
均衡算法
NO.1—— Random 随机
这是最简单的一种,使用随机数来决定转发到哪台机器上。
优点:简单使用,不需要额外的配置和算法。
缺点:随机数的特点是在数据量大到一定量时才能保证均衡,所以如果请求量有限的话,可能会达不到均衡负载的要求。
NO.2—— Round Robin 轮询
这个也很简单,请求到达后,依次转发,不偏不向。每个服务器的请求数量很平均。
缺点:当集群中服务器硬件配置不同、性能差别大时,无法区别对待。引出下面的算法。
NO.3—— Weighted Round Robin 加权轮询
这种算法的出现就是为了解决简单轮询策略中的不足。在实际项目中,经常会遇到这样的情况。
比如有 5 台机器,两台新买入的性能等各方面都特别好,剩下三台老古董。这时候我们设置一个权重,让新机器接收更多的请求。物尽其用、能者多劳嘛!
这种情况下,“均衡 “就比较相对了,也没必要做到百分百的平均。
NO.4—— Least Connections 最少连接
这是最符合负载均衡算法的一个。需要记录每个应用服务器正在处理的连接数,然后将新来的请求转发到最少的那台上。
NO.5—— Source Hashing 源地址散列
根据请求的来源 ip 进行 hash 计算,然后对应到一个服务器上。之后所有来自这个 ip 的请求都由同一台服务器处理。
共享session
单点登录(Single SignOn ) ,
简称为SSO,核心功能: session共享
不管是在哪一-台做的登录,登录完成后,会把登录信息保存到redis中。
当业务请求进来时,再到redis中获取登录信息,能获取到就表示已登录;未获取到就表示未登录,拦截掉请求
需要解决Session共享的场景:
1.同个应用多节点共享登录信息;
2.多个项目间共享登录信息。一般我们通常说的单点登录系统,是用来解决场景2的。
server
system
business
file
三个是本项目的核心部分,依赖于server
,本身是不会启动,是我们的公共模块
server
所有的service
层,持久层代码,都放到server
里面
generator
mybatis-generator
mybatis
持久层:负责数据持久化。即将数据存储到数据
库或硬盘等,断电也不会丢失数据。
ORM : 对象关系映射
Hibernate是全自动ORM
Mybatis : 是半自动ORM , Mybatis可以操作的花样更多,是首选的持久层框架,
file
- 文件上传
// 上传控件
<input type="file" v-on:change="uploadImage()" id="file-upload-input">
// 调用函数
uploadImage () {
let _this = this;
let formData = new window.FormData();
// key:"file"必须和后端controller参数名一致
formData.append('file', document.querySelector('#file-upload-input').files[0]);
Loading.show();
_this.$ajax.post(process.env.VUE_APP_SERVER + '/file/admin/upload', formData).then((response)=>{
Loading.hide();
let resp = response.data;
});
// 后台代码
public class UploadController {
private static final Logger LOG = LoggerFactory.getLogger(UploadController.class);
public static final String BUSINESS_NAME = "文件上传";
@RequestMapping("/upload")
public ResponseDto upload(@RequestParam MultipartFile file) throws IOException {
LOG.info("上传文件开始:{}", file);
LOG.info(file.getOriginalFilename());
LOG.info(String.valueOf(file.getSize()));
// 保存文件到本地
String fileName = file.getOriginalFilename();
String key = UuidUtil.getShortUuid();
String fullPath = "D:/file/imooc/teacher/" + key + "-" + fileName;
File dest = new File(fullPath);
file.transferTo(dest);
LOG.info(dest.getAbsolutePath());
ResponseDto responseDto = new ResponseDto();
return responseDto;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 断点续传
作为一个视频网站, -一个文件小则几十M ,大则上G ,上传 一个大文
件受网络影响很大,文件越大, 上传失败率越高
我们要完善文件上传功能,支持断点续传,当文件上传到一半,网络
断了,下次再上传时,只上传剩下的部分,这就是断点续传。
// 文件分片
let shardSize = 20 * 1024 * 1024; //以20MB为一个分片
let shardIndex = 1; //分片索引
let start = shardIndex * shardSize; //当前分片起始位置
let end = Math.min(file.size, start + shardSize); //当前分片结束位置
let fileShard = file.slice(start, end); //从文件中截取当前的分片数据
// key:"file"必须和后端controller参数名一致
formData.append('file', fileShard);
formData.append('use', _this.use);
Loading.show();
_this.$ajax.post(process.env.VUE_APP_SERVER + '/file/admin/upload', formData).then((response)=>{
Loading.hide();
let resp = response.data;
console.log("上传文件成功:", resp);
_this.afterUpload(resp);
$("#" + _this.inputId + "-input").val("");
});
},
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
@GetMapping("/merge")
public ResponseDto merge() throws Exception {
File newFile = new File(FILE_PATH + "/course/test123.mp4");
FileOutputStream outputStream = new FileOutputStream(newFile, true);//文件追加写入
FileInputStream fileInputStream = null;//分片文件
byte[] byt = new byte[10 * 1024 * 1024];
int len;
try {
// 读取第一个分片
fileInputStream = new FileInputStream(new File(FILE_PATH + "/course/Bc0SXtFn.blob"));
while ((len = fileInputStream.read(byt)) != -1) {
outputStream.write(byt, 0, len);
}
// 读取第二个分片
fileInputStream = new FileInputStream(new File(FILE_PATH + "/course/roQbPm2x.blob"));
while ((len = fileInputStream.read(byt)) != -1) {
outputStream.write(byt, 0, len);
}
} catch (IOException e) {
LOG.error("分片合并异常", e);
} finally {
try {
if (fileInputStream != null) {
fileInputStream.close();
}
outputStream.close();
LOG.info("IO流关闭");
} catch (Exception e) {
LOG.error("IO流关闭", e);
}
}
ResponseDto responseDto = new ResponseDto();
return responseDto;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
升级
将分片的记录存到数据库里面
+alter table `file` add column (`shard_index` int comment '已上传分片');
+alter table `file` add column (`shard_size` int comment '分片大小|B');
+alter table `file` add column (`shard_total` int comment '分片总数');
+alter table `file` add column (`key` varchar(32) comment '文件标识');
+alter table `file` add unique key key_unique (`key`);
- 1
- 2
- 3
- 4
- 5
使用文件生成md5签名,作为文件标识
/**
* 10进制转62进制
* @param number
* @returns {string}
* @private
*/
_10to62: function (number) {
let chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ';
let radix = chars.length;
let arr = [];
do {
let mod = number % radix;
number = (number - mod) / radix;
arr.unshift(chars[mod]);
} while (number);
return arr.join('');
}
//获取文件的属性
console.log(file);
/*
name: "test.mp4"
lastModified: 1901173357457
lastModifiedDate: Tue May 27 2099 14:49:17 GMT+0800 (中国标准时间) {}
webkitRelativePath: ""
size: 37415970
type: "video/mp4"
*/
// 生成文件标识,标识多次上传的是不是同一个文件
let key = hex_md5(file);
let key10 = parseInt(key, 16);
let key62 = Tool._10to62(key10);
console.log(key, key10, key62);
/*
d41d8cd98f00b204e9800998ecf8427e
2.8194976848941264e+38
6sfSqfOwzmik4A4icMYuUe
*/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
重构合并
public void merge(FileDto fileDto) throws Exception {
LOG.info("合并分片开始");
String path = fileDto.getPath(); //http://127.0.0.1:9000/file/f/course\6sfSqfOwzmik4A4icMYuUe.mp4
path = path.replace(FILE_DOMAIN, ""); //course\6sfSqfOwzmik4A4icMYuUe.mp4
Integer shardTotal = fileDto.getShardTotal();
File newFile = new File(FILE_PATH + path);
FileOutputStream outputStream = new FileOutputStream(newFile, true);//文件追加写入
FileInputStream fileInputStream = null;//分片文件
byte[] byt = new byte[10 * 1024 * 1024];
int len;
try {
for (int i = 0; i < shardTotal; i++) {
// 读取第i个分片
fileInputStream = new FileInputStream(new File(FILE_PATH + path + "." + (i + 1))); // course\6sfSqfOwzmik4A4icMYuUe.mp4.1
while ((len = fileInputStream.read(byt)) != -1) {
outputStream.write(byt, 0, len);
}
}
} catch (IOException e) {
LOG.error("分片合并异常", e);
} finally {
try {
if (fileInputStream != null) {
fileInputStream.close();
}
outputStream.close();
LOG.info("IO流关闭");
} catch (Exception e) {
LOG.error("IO流关闭", e);
}
}
LOG.info("合并分片结束");
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
上传完毕后,合并文件后;删除分片
System.gc();
LOG.info("删除分片开始");
for (int i = 0; i < shardTotal; i++) {
String filePath = FILE_PATH + path + "." + (i + 1);
File file = new File(filePath);
boolean result = file.delete();
LOG.info("删除{},{}", filePath, result ? "成功" : "失败");
}
LOG.info("删除分片结束");
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
断点检查分片是否继续上传?
/**
* 检查文件状态,是否已上传过?传到第几个分片?
*/
check (param) {
let _this = this;
_this.$ajax.get(process.env.VUE_APP_SERVER + '/file/admin/check/' + param.key).then((response)=>{
let resp = response.data;
if (resp.success) {
let obj = resp.content;
if (!obj) {
param.shardIndex = 1;
console.log("没有找到文件记录,从分片1开始上传");
_this.upload(param);
} else {
param.shardIndex = obj.shardIndex + 1;
console.log("找到文件记录,从分片" + param.shardIndex + "开始上传");
_this.upload(param);
}
} else {
Toast.warning("文件上传失败");
$("#" + _this.inputId + "-input").val("");
}
})
},
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
public FileDto findByKey(String key) {
return CopyUtil.copy(selectByKey(key), FileDto.class);
}
- 1
- 2
- 3
- 极速秒传
极速秒传,听起来
很高深,原理其实很简单,就是开始.上传前,先检查一下文件是否上传过了,如果已上传过,直接弹出提示,极速秒传成功。
/**
* 检查文件状态,是否已上传过?传到第几个分片?
*/
check (param) {
let _this = this;
_this.$ajax.get(process.env.VUE_APP_SERVER + '/file/admin/check/' + param.key).then((response)=>{
let resp = response.data;
if (resp.success) {
let obj = resp.content;
if (!obj) {
param.shardIndex = 1;
console.log("没有找到文件记录,从分片1开始上传");
_this.upload(param);
} else if (obj.shardIndex === obj.shardTotal) {
// 已上传分片 = 分片总数,说明已全部上传完,不需要再上传
Toast.success("文件极速秒传成功!");
_this.afterUpload(resp);
$("#" + _this.inputId + "-input").val("");
} else {
param.shardIndex = obj.shardIndex + 1;
console.log("找到文件记录,从分片" + param.shardIndex + "开始上传");
_this.upload(param);
}
} else {
Toast.warning("文件上传失败");
$("#" + _this.inputId + "-input").val("");
}
})
},
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
复制文件地址
if (fileDto != null) {
fileDto.setPath(FILE_DOMAIN + fileDto.getPath());
}
- 1
- 2
- 3
加密视频
HTTP Live Streaming(缩写是 HLS)是一个由苹果公司提出的基于 HTTP 的流媒体网络传输协议。是苹果公司 QuickTime X 和 iPhone 软件系统的一部分。它的工作原理是把整个流分成一个个小的基于 HTTP 的文件来下载,每次只下载一些。当媒体流正在播放时,客户端可以选择从许多不同的备用源中以不同的速率下载同样的资源,允许流媒体会话适应不同的数据速率。在开始一个流媒体会话时,客户端会下载一个包含元数据的 extended M3U (m3u8)playlist 文件,用于寻找可用的媒体流。 \ HLS 只请求基本的 HTTP 报文,与实时传输协议(RTP)不同,HLS 可以穿过任何允许 HTTP 数据通过的防火墙或者代理服务器。它也很容易使用内容分发网络来传输媒体流。
HLS 协议规定:
视频的封装格式是 TS。
视频的编码格式为 H264,音频编码格式为 MP3、AAC 或者 AC-3。
除了 TS 视频文件本身,还定义了用来控制播放的 m3u8 文件(文本文件)。
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-KEY:METHOD=AES-128,URI="https://ipc-camera.fast-cn.wgine.com/api/cloud/key?devId=6c4db6784fffcc3892nxmp&magic=qKr6mxwDmUbJ8EYnHCdmAnD3488CsMaj",IV=0x7b84a718bbac5e2053d64b3295ca2dce
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:10
#EXT-X-PROGRAM-DATE-TIME:2020-01-08T20:01:16.000+00:00
#EXTINF:10,
bobf339vmg9aa815lus0zTCWOElTj5jd_0.ts?token=5160f1deec8742b4c5ecd9891f5bcf934290b7d4ea3f29284a1ebce9bab2efab
#EXT-X-PROGRAM-DATE-TIME:2020-01-08T20:01:26.000+00:00
#EXTINF:10,
bobf339vmg9aa815lus0zTCWOElTj5jd_1.ts?token=9588f1926541d09bbc06db2f9ffd944fa9b2f2062e9e4186938ed64e7869b5f3
#EXT-X-PROGRAM-DATE-TIME:2020-01-08T20:01:36.000+00:00
#EXTINF:10,
#EXT-X-ENDLIST
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
2.1 EXTM3U
每个 M3U 文件第一行必须是这个 tag,起标示作用。
2.2 EXT-X-VERSION
用以标示协议版本。
2.3 EXT-X-KEY
这个标示了当前 M3U8 的解密方式。
2.4 EXT-X-MEDIA-SEQUENCE
每一个 media URI 在 PlayList 中只有唯一的序号,相邻之间序号+1, 一个 media URI 并不是必须要包含的,如果没有,默认为 0。(因为存在多个 m3u8 的情况,视频太大时减少 m3u8 大小)
2.5 EXT-X-TARGETDURATION
每一份媒体文件的时间, 以秒为单位, 这里是 10 秒一份
2.6 EXTINF
每一份媒体文件的具体数据,包括文件 url,持续时间等
2.7 EXT-X-PROGRAM-DATE-TIME
播放的绝对时间。(这里我们用来更新进度条)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
授权播放
文章来源: hiszm.blog.csdn.net,作者:孙中明,版权归原作者所有,如需转载,请联系作者。
原文链接:hiszm.blog.csdn.net/article/details/119883951
- 点赞
- 收藏
- 关注作者
评论(0)