springboot整合websocket(三)上传文件(引导篇)
springboot整合websocket(一)简单聊天室
springboot整合websocket(二)聊天室补充篇
springboot整合websocket(三)上传文件(引导篇)
springboot整合websocket(四)上传文件(终篇)
Gitee 地址
gitee是更早之前的版本,只有聊天室的功能,没有文件上传的功能,核心不变,具体实现上有所出入
https://gitee.com/chr_demo/web-socket
如果大家能够有写好的可以共享一下嘛,我实在不想动了(//懒死\\)
其实,上传文件通常会调用另一个接口,而不是通过websocket,但毕竟人家支持这么做,所以还是写下。
还挺麻烦的,分两章来写。
这一章我们先做个初版,会有bug,下一章会在这一章基础上修复这个bug。
毕竟真的有很多东西啊!!
一、回顾一下
上次提到有一个方法:
void onMessage(Session session, byte[] message);
这个是用来上传文件用的,先解释一下 byte[] 这个参数。
好吧~ 大家猜的没错,这个就是我们文件的二进制流。
所以说,我们要干两件事:
第一,我们js要获得文件,并转成二进制流发过来
第二,在上传之前,我们需要知道文件的名字,因为这个二进制流是一个数组,没法知道文件的名字!(实测,js不能修改这个二进制流哦)
二、先做后端吧
说一下重点
1、在 onMessage(string) 中用将文件信息存起来,并删除已有的文件。
2、在onMessage(byte[])中保存文件。
3、在onClose中将关闭原因打印一下(这就是那个bug导致的)。
@Log4j2
@Controller
@ServerEndpoint("/websocket")
public class BaseWebsocketController
{
//使用 ConcurrentHashMap, 保证线程安全, static全局共享 session
//这里之所以static,是因为这个类不是单例的!!
//他虽然有@Controller注解,但是不适用Ioc容器中拿对象,每一次请求过来都是一个新的对象
//存放 session
private final static Map<String, Session> sessions = new ConcurrentHashMap<>();
//onopen 在连接创建(用户进入聊天室)时触发
@OnOpen
public void openSession(Session session, EndpointConfig config)
{
}
//响应字符串
@OnMessage
public void onMessage(Session session, String message)
{
//使用 fastjson 解析 json 字符串
final Message data = JSONObject.parseObject(message, Message.class);
//响应的信息
final Message response = Message.builder()
.operation(data.getOperation()) //将请求的 operation 放入
.build();
//根据不同的 operation 执行不同的操作
switch (data.getOperation()) {
//进入聊天室后保存用户名
case "tip":
session.getUserProperties().put("username", data.getMsg());
sessions.put(session.getId(), session);
response.setMsg("[" + data.getMsg() + "]进入房间");
sendAll(JSONObject.toJSONString(response));
break;
//发送消息
case "msg":
final String username = (String) session.getUserProperties().get("username");
response.setMsg("[" + username + "]" + data.getMsg());
sendAll(JSONObject.toJSONString(response));
break;
case "filename":
//删除原有文件
File file = new File(SpringbootPathUtil.getResourcePath() + "/web-socket/file/" + data.getMsg());
file.delete();
//保存文件信息
session.getUserProperties().put("file", file);
response.setMsg("文件【" + data.getMsg() + "】开始上传");
sendTo(session, JSONObject.toJSONString(response));
break;
}
}
//响应字节流
@OnMessage
public void onMessage(Session session, byte[] message) throws Exception
{
final Message response = new Message();
final String username = (String) session.getUserProperties().get("username");
final File file = (File) session.getUserProperties().get("file");
log.info(file.getCanonicalPath());
if (saveFile(file, message)) {
response.setOperation("file-upload-success");
response.setMsg("[" + username + "]上传了一个文件【" + file.getName() + "】");
sendAll(JSONObject.toJSONString(response));
}
else {
response.setOperation("file-upload-fail");
response.setMsg("文件【" + file.getName() + "】上传失败");
sendTo(session, JSONObject.toJSONString(response));
}
}
//onclose 在连接断开(用户离开聊天室)时触发
@OnClose
public void closeSession(Session session, CloseReason closeReason)
{
System.out.println(closeReason.toString());
//记得移除相对应的session
sessions.remove(session.getId());
sendAll("[" + session.getId() + "]离开了房间");
}
@OnError
public void sessionError(Session session, Throwable throwable)
{
//通常有异常会关闭session
try {
session.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
private void sendAll(String message)
{
for (Session s : sessions.values()) {
sendTo(s, message);
}
}
private void sendTo(Session session, String message)
{
final RemoteEndpoint.Basic remote = session.getBasicRemote();
try {
//发送消息
remote.sendText(message);
}
catch (IOException e) {
e.printStackTrace();
}
}
private boolean saveFile(File file, byte[] message)
{
try (OutputStream os = new FileOutputStream(file)) {
os.write(message, 0, message.length);
return true;
}
catch (IOException e) {
e.printStackTrace();
return false;
}
}
}
三、页面
1、html
说一下重点
- 先通过 sendMessage(‘filename’, file.name); 将文件信息发给服务器,让服务器保存
- 服务其保存好信息后,再发送文件。
function sendFile(file){…} - 发送文件的时候,要使用 FileReader() 对象将文件转成二进制流
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="UTF-8">
<title>websocket-demo</title>
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.2.1/css/bootstrap.min.css">
</head>
<body>
<div class="container py-3">
<div class="row">
<div class="col-6">
<div>
<label for="messageArea">聊天信息:</label>
</div>
<div>
<textarea id="messageArea" readonly class="w-100" style="height: 75vh;"></textarea>
</div>
</div>
<div class="col">
<div class="my-1">
<label for="messageArea">用 户 名:</label>
</div>
<div class="my-1">
<input type="text" id="username" autocomplete="off">
</div>
<div class="my-1">
<button class="btn-info" id="joinRoomBtn">进入聊天室</button>
<button class="btn-warning" id="leaveRoomBtn">离开聊天室</button>
</div>
<hr/>
<div class="my-1">
<label for="sendMessage">输入消息:</label>
</div>
<div>
<textarea id="sendMessage" rows="5" class="w-100" style="max-height: 50vh"></textarea>
</div>
<div class="my-1">
<button class="btn-primary" id="sendBtn">发送消息</button>
</div>
<hr/>
<div class="my-1">
<input id="file" type="file" value="选择文件"/>
<button id="fileBtn" class="btn-primary">上传</button>
</div>
</div>
</div>
</div>
<script src="https://s3.pstatp.com/cdn/expire-1-M/jquery/3.3.1/jquery.min.js"></script>
<script>
let webSocket;
//ip和端口号用自己项目的
//{websocket}: 其实是刚刚那个@ServerEndpoint("/websocket")中定义的
let url = 'ws://127.0.0.1:8080/websocket';
let file;
$('#username').keyup(function (e) {
let keycode = e.which;
if (keycode == 13) {
$('#joinRoomBtn').click();
}
});
//进入聊天室
$('#joinRoomBtn').click(function () {
let username = $('#username').val();
webSocket = new WebSocket(url);
webSocket.onopen = function () {
console.log('webSocket连接创建。。。');
sendMessage('tip', username);
}
webSocket.onclose = function () {
console.log('webSocket已断开。。。');
$('#messageArea').append('websocket已断开\n');
}
webSocket.onmessage = function (event) {
//这个 data 和刚刚的 Message 一样
let data = {
operation: '',
msg: ''
};
data = JSON.parse(event.data);
switch (data.operation) {
case "tip":
$('#messageArea').append(data.msg + '\n');
break;
case "msg": //显示消息
$('#messageArea').append(data.msg + '\n');
break;
case "filename":
$('#messageArea').append(data.msg + '\n');
sendFile(file);
break;
case "file-upload-success":
$('#messageArea').append(data.msg + '\n');
break;
case "file-upload-fail":
$('#messageArea').append(data.msg + '\n');
break;
}
}
webSocket.onerror = function (event) {
console.log(event)
console.log('webSocket连接异常。。。');
}
});
//退出聊天室
$('#leaveRoomBtn').click(function () {
if (webSocket) {
//关闭连接
webSocket.close();
}
});
//发送消息
$('#sendBtn').click(function () {
var msg = $('#sendMessage').val();
if (msg.trim().length === 0) {
alert('请输入内容');
return;
}
sendMessage('msg', $('#sendMessage').val());
$('#sendMessage').val('');
});
//上传文件
// https://www.cnblogs.com/myfjd/archive/2012/03/22/2411374.html
$('#fileBtn').click(function () {
let files = [];
files = $('#file')[0].files;
if (files.length === 0) {
alert('请选择文件');
return;
}
//发送文件名
file = files[0];
sendMessage('filename', file.name);
});
//发送消息
function sendMessage(operation, msg) {
//这个 data 和刚刚的 Message 一样
let data = {
operation: operation,
msg: msg,
};
//将 data 转成 json 字符串
webSocket.send(JSON.stringify(data));
}
//发送文件
function sendFile(file){
//挖个坑先
let blob = file;
//文件读取对象
let reader = new FileReader();
//文件加载后的回调函数
reader.onload = ev => {
let buffer = ev.target.result;
webSocket.send(buffer);
}
//二进制加载文件
reader.readAsArrayBuffer(blob);
}
</script>
</body>
</html>
2、 接下来,大家就可以开心的上传文件了
这时候,有的同学就成功了,有的同学就失败了。
成功的同学选个大点的文件(>10M)
失败的同学选个小点的文件(<1M)
失败的同学会显示websocket已断开,如下图
看一下后台的报错:(检查一下onClose是不是和我上面的一样,要打印一下关闭原因哦~)
No async message support and buffer too small. Buffer size: [8,192], Message size: [2,254,618]
这个意思是说-------->
文件:我有2,254,618B,我的很大,你忍一下
Websocket:不行,我只有8192B,太大了,忍不了,放不开
所以啊,我们要将文件切分,挨个上传,一次传一点,这篇有点长,我们下篇再说
End
springboot整合websocket(一)简单聊天室
springboot整合websocket(二)聊天室补充篇
springboot整合websocket(三)上传文件(引导篇)
springboot整合websocket(四)上传文件(终篇)