少年一贯快马扬帆,道阻且长且不转弯,要盛大要绚烂要哗然,要用理想的泰坦尼克去撞现实的冰川,要当烧赤壁的风而非借箭的草船,要为一片海就肯翻越万山。
WebSocket协议
WebScoket是一种全双工通信协议,它是以ws或都wss开头。它可以建立客户机与服务端之间的一个长链接,直到被任何一方终止。
SpringBoot中如何使用WebSocket
集成WebSocket
在SpringBoot项目里面集成WebSocket也很简单,只需要引入相应的Jar包即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
这里只列出了WebSocket相关的maven,当然你也要引入web相关的包,这里就不写出来了
引入完成后,实现WebSocket的方式也有两种
- 通过注解的方式
- 通过实现WebSocketConfigurer类的方式(本文章所讲解)
通过实现WebSocketConfigurer类的方式
需要新创建一个类实现WebSocketConfigurer并且重写里面的registerWebSocketHandlers方法
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(uploadWsHandler,"upload")
.addInterceptors(handshakeInterceptor)
.setAllowedOriginPatterns("*");
}
WebSocketHandlerRegistry类中有几个方法:
-
addHandler:这个是添加一个处理器,当有请求需要连接时就会进入这个类中处理。方法的参数二是连接Socket的Url,类似
RequestMapping("/api")里面的/api。 -
addInterceptors:这个是添加一个拦截器,比如我可以添加一个握手拦截器(HandshakeInterceptor),在握手时看看该请求是否是一个通过认证的请求
-
setAllowedOriginPatterns:设置跨域
该类上面还要标注@EnableWebSocket和@Configuration
一是因为该类是做为的一个配置类所以要标注@Configuration
二要开启WebSocket,要标注@EnableWebSocket
创建Handler(本文章的案例是上传文件)
上传文件的方式是前端发送二进制数组。所以这里就继承BinaryWebSocketHandler类。
这里还有一个TextWebSocketHandler的处理器,从名字上就可以看出它是一个处理文本的处理器。具体自行查看
@Component
@Slf4j
public class UploadWsHandler extends BinaryWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("连接建立成功");
}
@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
log.info("接收到前端发来的消息");
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
exception.printStackTrace();
log.info("连出现异常");
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
log.info("连接建立关闭");
}
}
| 方法及作用 | |
|---|---|
| afterConnectionEstablished | 建立连接后所触发的方法 |
| handleBinaryMessage | 接收到二进制消息触发的方法 |
| handleTransportError | 当连接出现异常时触发的方法 |
| afterConnectionClosed | 当连接关闭时触发的方法 |
注:该类要标注上@Component注解,可以自动注入
通过上面所有的配置,这个时候你应该是可以与服务端建立连接了,地址ws://ip:springboot配置文件中的端口号/upload,你现在可以试下是否能成功建立连接,如有问题可以在方评论区留言
本文所用的案例是上传文件,所以这里只需要修改handleBinaryMessage方法即可
FileOutputStream fos = null;
@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
//将接收到的字节数组读取出来
final byte[] array = message.getPayload().array();
//拼接好的保存文件的路径
String path = fileSavePath + File.separator + '123.png';
if(FileUtil.exist(path)){
return;
}
//通过FOS把这个文件保存到本地
fos = new FileOutputStream(path);
try{
fos.write(array);
fos.flush();
}catch (Exception e){
e.printStackTrace();
}finally {
fos.close();
}
}
测试功能
写好以后就来测试一把,前端我是用的PanJiaChen大佬开源的一个前端脚手架vue-element-admin
mounted() {
this.initWebSocket()
},
methods: {
//初始化连接
initWebSocket() {
if (typeof (WebSocket) === 'undefined') {
this.$message({
message: '您的浏览器不支持WebSocket,推荐使用Google Chrome浏览器',
type: 'warning'
})
} else {
this.socket = new WebSocket('ws://127.0.0.1:18280/upload?token=' + this.token)
//如果是传的字节数组,这里要修改成arraybuffer
this.socket.binaryType = 'arraybuffer'
this.socket.onmessage = this.webSocketOnMessage
this.socket.onopen = this.webSocketOnOpen
this.socket.onerror = this.webSocketOnError
this.socket.onclose = this.webSocketClose
}
},
webSocketOnOpen() { // 连接建立之后执行send方法发送数据
this.$message({
message: 'Socket连接建立成功',
type: 'success'
})
},
async onChange(file, fileList) {
this.socket.send(file.raw)
},
webSocketOnError(e) { // 连接建立失败重连
this.$message({
message: 'Socket连接建立失败',
type: 'warning'
})
console.log(JSON.stringify(e))
// this.initWebSocket()
},
webSocketOnMessage(e) { // 数据接收
console.log(e.data)
this.$message('来自服务端的消息:' + e.data)
},
websocketsend(Data) { // 数据发送
this.socket.send(Data)
},
webSocketClose(e) { // 关闭
console.log('断开连接', e)
}
}
上传文件的组件是用的Element的el-upload
<template>
<div class="vipmanage-container">
<el-container>
<el-main>
<el-upload
class="upload-demo"
action=""
:limit="3"
:on-change="onChange"
:file-list="fileList"
:auto-upload="false"
>
<el-button size="small" type="primary">点击上传</el-button>
</el-upload>
</el-main>
</el-container>
</div>
</template>
当打开网页后,如果Socket连接成功就会出现这个提示
点击上传文件后,文件就会上传到你在后端设置的那个路径里面了。
我这里传的是一个小文件,如果你在这个过程中出现了因为缓冲区小出现了问题。
可以在继承WebSocketConfig的那个类中设置下缓冲区或者是看下面所讲的文件分片上传
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer(){
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxBinaryMessageBufferSize(这个大小自已定义);
return container;
}
大文件上传
有的时候会上传好几个G的大文件,这种文件在上传时最好是将其分片,上传到后端后再将其合并
前端的代码将onChange修改如下
async onChange(file, fileList) {
var start = 0; var end = 0; var index = 0; const filesize = file.size
var totalPieces = Math.ceil(filesize / this.bytesPerPiece)
console.log('start:' + start)
console.log('end:' + end)
console.log('index:' + index)
console.log('filesize:' + filesize)
console.log('totalPieces:' + totalPieces)
var chumnk = await this.createFileChunk(file.raw, 5 * 1024 * 1024)
console.log('分片长度' + chumnk.length)
for (var i = 0; i < chumnk.length; i++) {
this.socket.send(chumnk[i])
}
//在前端的文件上传完成后,给后端发送一个标识,用来确认是否合并文件
var blob = new Blob(['1'])
this.socket.send(blob)
},
//分片方法
async createFileChunk(file, size) {
const fileChunkList = []
let count = 0
while (count < file.size) {
fileChunkList.push(file.slice(count, count + size))
count += size
}
return fileChunkList
}
后端也只需要修改handleBinaryMessage方未予即可
/**保存文件分片的路径*/
private List<File> filePathSet = new ArrayList<>();
@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
if(message.getPayloadLength() == 1 ){
byte[] array = message.getPayload().array();
String s = new String(array,"UTF-8");
if(StrUtil.equals(s,"1")){
List<File> filePaths = filePathSet.stream().collect(Collectors.toList());
//这里我将文件的后缀写死了,你们在修改的时候可以传过来
String returnPath = File.separator + "file" + File.separator + IdUtil.simpleUUID() + ".mp4";
String mergenPath = fileSavePath + returnPath;
if(!FileUtil.exist(mergenPath)){
FileUtil.touch(mergenPath);
}
//开始合并文件
if(Md5Util.mergeFileWithNio(filePaths.toArray(new File[filePaths.size()]),mergenPath)){
session.sendMessage(new TextMessage("{\"success\":true;\"messgae\":\"上传成功\";\"filepath\":'"+returnPath+"'}"));
}else{
session.sendMessage(new TextMessage("{\"success\":false;\"messgae\":\"上传失败\"}"));
}
return;
}
}
final byte[] array = message.getPayload().array();
String byteMd5 = Md5Util.conVertTextToMD5(array);
String path = fileSavePath + File.separator + byteMd5;
if(FileUtil.exist(path)){
filePathSet.add(new File(path));
return;
}
filePathSet.add(new File(path));
fos = new FileOutputStream(path);
try{
fos.write(array);
fos.flush();
log.info("Md5{}",byteMd5);
}catch (Exception e){
e.printStackTrace();
}finally {
fos.close();
}
}
并且新增了计算MD5和合并文件的方法
public class Md5Util {
public static final int BUF_SIZE = 1024 * 1204;
//计算Md5
public static String conVertTextToMD5(byte[] by) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(by);
byte b[] = md.digest();
int i;
StringBuffer buf = new StringBuffer("");
for (int offset = 0; offset < b.length; offset++) {
i = b[offset];
if (i < 0)
i += 256;
if (i < 16)
buf.append("0");
buf.append(Integer.toHexString(i));
}
// 32位加密
return buf.toString();
// 16位的加密
// return buf.toString().substring(8, 24);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
//合并文件
public static boolean mergeFileWithNio(File[] files, String newFilePath) {
FileChannel outChannel = null;
FileChannel inChannel = null;
try {
outChannel = new FileOutputStream(newFilePath).getChannel();
for (File file : files) {
inChannel = new FileInputStream(file).getChannel();
ByteBuffer bb = ByteBuffer.allocate(BUF_SIZE);
while (inChannel.read(bb) != -1) {
bb.flip();
outChannel.write(bb);
bb.clear();
}
inChannel.close();
}
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
try {
if (outChannel != null) {
outChannel.close();
}
if (inChannel != null) {
inChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这个功能是我在写一个会员管理系统里面用到的,如果想要看本文的具体代码可以去我Gitee上看
后端:地址
前端:地址前端是用的PanJiaChen大佬的Vue Element Admin项目修改的