WebSocket服务端注解 | 事件类型 | 事件描述 |
---|---|---|
@OnOpen | onOpen | 当打开连接后触发 |
@OnMessage | onMessage | 当接收客户端信息时触发 |
@OnClose | onClose | 当连接关闭时触发 |
@OnError | onError | 当通信异常时触发 |
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.56version>
dependency>
WebSocketConfig.class
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
Message.class
import com.alibaba.fastjson.JSON;
import java.util.List;
/**
* WebSocket 聊天消息类
*/
public class Message {
public static final String ENTER = "ENTER";
public static final String SPEAK = "SPEAK";
public static final String QUIT = "QUIT";
private String type; // 消息类型
private String fromUser; // 发送人
private String toUser; // 接收人
private String msg; // 发送消息
private int onlineCount; // 在线用户数
private List<String> list;
/*
* 聊天消息
* 没有设置接收人 toUser ,视为群聊
* 设置了接收人 toUser,视为私聊
* */
public static String jsonStr(String type, String fromUser, String toUser, String msg, int onlineCount) {
return JSON.toJSONString(new Message(type, fromUser, toUser, msg, onlineCount));
}
public Message(String type, String fromUser, String toUser, String msg, int onlineCount) {
this.type = type;
this.fromUser = fromUser;
this.toUser = toUser;
this.msg = msg;
this.onlineCount = onlineCount;
}
public static String jsonStr(String type, String fromUser, String toUser, String msg, int onlineCount, List<String> list) {
return JSON.toJSONString(new Message(type, fromUser, toUser, msg, onlineCount, list));
}
public Message(String type, String fromUser, String toUser, String msg, int onlineCount, List<String> list) {
this.type = type;
this.fromUser = fromUser;
this.toUser = toUser;
this.msg = msg;
this.onlineCount = onlineCount;
this.list = list;
}
public static String getENTER() {
return ENTER;
}
public static String getSPEAK() {
return SPEAK;
}
public static String getQUIT() {
return QUIT;
}
public String getType() {
return type;
}
public Message setType(String type) {
this.type = type;
return this;
}
public String getFromUser() {
return fromUser;
}
public Message setFromUser(String fromUser) {
this.fromUser = fromUser;
return this;
}
public String getToUser() {
return toUser;
}
public Message setToUser(String toUser) {
this.toUser = toUser;
return this;
}
public String getMsg() {
return msg;
}
public Message setMsg(String msg) {
this.msg = msg;
return this;
}
public int getOnlineCount() {
return onlineCount;
}
public Message setOnlineCount(int onlineCount) {
this.onlineCount = onlineCount;
return this;
}
public List<String> getList() {
return list;
}
public Message setList(List<String> list) {
this.list = list;
return this;
}
}
WebSocketChatServer.class
import com.alibaba.fastjson.JSON;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
@ServerEndpoint("/chat/{name}")
public class WebSocketChatServer {
/**
* 全部在线会话 PS: 基于场景考虑 这里使用线程安全的Map存储会话对象。
* 以用户姓名为key
*/
private static Map<String, Session> onlineSessions = new ConcurrentHashMap<>();
/**
* 当通信发生异常:打印错误日志
*/
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
/**
* 当客户端打开连接:1.添加会话对象 2.更新在线人数
*/
@OnOpen
public void onOpen(Session session, @PathParam("name") String name) {
onlineSessions.put(name, session);
sendMessageToAll(Message.jsonStr(Message.ENTER, "系统通知", "", "欢迎“" + name + "”加入群聊", onlineSessions.size(), listUser()));
}
/**
* 当客户端发送消息:1.获取它的用户名和消息 2.发送消息给所有人
*
* PS: 这里约定传递的消息为JSON字符串 方便传递更多参数!
*/
@OnMessage
public void onMessage(Session session, String jsonStr) {
Message message = JSON.parseObject(jsonStr, Message.class);
if (message.getToUser() != null) {
sendMessageToUser(Message.jsonStr(Message.SPEAK, message.getFromUser(), message.getToUser(), message.getMsg(), onlineSessions.size()));
} else {
sendMessageToAll(Message.jsonStr(Message.SPEAK, message.getFromUser(), message.getToUser(), message.getMsg(), onlineSessions.size()));
}
}
/**
* 当关闭连接:1.移除会话对象 2.更新在线人数
*/
@OnClose
public void onClose(Session session, @PathParam("name") String name) {
onlineSessions.remove(name);
sendMessageToAll(Message.jsonStr(Message.QUIT, "系统通知", "", "“" + name + "”退出群聊", onlineSessions.size(), listUser()));
}
/**
* 公共方法:发送信息给所有人
*/
private static void sendMessageToAll(String msg) {
onlineSessions.forEach((id, session) -> {
try {
session.getBasicRemote().sendText(msg);
} catch (IOException e) {
e.printStackTrace();
}
});
}
/**
* 单独聊天方法:发送信息给指定的人
*/
private static void sendMessageToUser(String msg) {
Message message = JSON.parseObject(msg, Message.class);
if (onlineSessions.get(message.getToUser()) != null) {
try {
onlineSessions.get(message.getToUser()).getBasicRemote().sendText(msg);
} catch (IOException e) {
e.printStackTrace();
}
} else {
try {
onlineSessions.get(message.getFromUser()).getBasicRemote().sendText(Message.jsonStr(Message.QUIT, "系统通知", message.getFromUser(), "用户不在线", onlineSessions.size()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
private List<String> listUser() {
List<String> list = new ArrayList<>();
for (Map.Entry<String, Session> s: onlineSessions.entrySet()) {
list.add(s.getKey());
}
return list;
}
}
WebSocket回调函数 | 事件类型 | 事件描述 |
---|---|---|
webSocket.onopen | onOpen | 当打开连接后触发 |
webSocket.onmessage | onMessage | 当接收客户端信息时触发 |
webSocket.onclose | onClose | 当连接关闭时触发 |
webSocket.onerror | onError | 当通信异常时触发 |
<template>
<div>
<el-row type="flex" class="row-bg" justify="center">
<el-col :md="8">
<el-card shadow="always" style="margin-top: 150px;">
<h3 class="text-center mb-5">webSocket 聊天室</h3>
<el-form ref="form" :model="form" :inline="true">
<el-form-item label="用户名">
<el-input v-model="form.input" @keyup.enter.native="login()"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" style="width: 100%" @click="login()">马上进入</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
name: "login",
data() {
return {
form: {
input: '',
},
}
},
methods: {
login() {
if (this.form.input === '') {
this.$message.warning('请填写用户名');
} else {
let admin = {
username : this.form.input,
};
this.$cookie.setCookie('user', this.form.input);
this.$router.push({path:'/index'});
}
}
}
}
</script>
<style scoped>
</style>
<template>
<div>
<el-row type="flex" justify="center">
<el-col :md="14">
<div>
<p class="mb-1 ml-2">点击退出登录</p>
<p class="mt-1"><i class="el-icon-bottom" style="margin-left: 12px;"></i></p>
</div>
<el-card :body-style="{ padding: '10px' }" style="background-color: #E8E8E8; border: 1px solid #DDDDDD; border-bottom: 0;">
<div style="height: 20px;">
<div style="width: 20px; height: 20px; background-color: #DF7065; border-radius: 50%; float: left;" @click="webSokcetClose()"></div>
<div style="width: 20px; height: 20px; background-color: #E6BB46; border-radius: 50%; float: left; margin-left: 10px;"></div>
<div style="width: 20px; height: 20px; background-color: #5BCC8B; border-radius: 50%; float: left; margin-left: 10px;"></div>
</div>
</el-card>
<el-card :body-style="{ padding: '0' }" style="border: 1px solid #DDDDDD; border-top: 0;">
<el-row>
<el-col :md="6" style=" height: 500px; border-right: 2px solid #DDDDDD; overflow: auto;">
<el-card :body-style="{ 'padding-top': '11px', 'padding-bottom': '12px', 'padding-left': '10px', 'padding-right': '12px' }" shadow="never">
<div>
<el-input
size="mini"
placeholder="请输入内容"
v-model="input1">
</el-input>
</div>
</el-card>
<div v-for="item in listUser" :key="item">
<a href="javascript:void(0);" @click="toggleChat(item)">
<el-card :body-style="{ padding: '5px 10px' }" shadow="never">
<el-row>
<el-col :span="7">
<el-image
style="width: 40px; height: 40px; border-radius: 50%;"
src="https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg"
fit="cover"></el-image>
</el-col>
<el-col :span="17">
<h4 class="m-0" style="color: #666666; font-size: 15px;">{{item}}</h4>
<p style="color: #999999; font-size: 12px; margin: 0; margin-top: 3px;" class="limitTitleDirectory">这个家伙很懒,什么也没有留下。</p>
</el-col>
</el-row>
</el-card>
</a>
</div>
</el-col>
<el-col :md="18">
<div v-show="toUser !== '游客'">
<el-card :body-style="{ padding: '15px' }" shadow="never">
<div class="text-center">
<p class="m-0">{{toUser}}</p>
</div>
</el-card>
<el-card :body-style="{ padding: '5px' }" shadow="never">
<div v-for="item in this.listUser" :key="item" v-show="toUser === item" :id="item" style="height: 327px; overflow: auto;"></div>
</el-card>
<el-input type="textarea" :rows="5" v-model="input" @keyup.enter.native="webSokcetSend()"></el-input>
</div>
<div v-show="toUser === '游客'">
<div style="text-align: center; line-height: 500px; background-color: #f3f3f3;">
<p style="margin: 0; font-size: 22px; color: #909399;">没有会话消息</p>
</div>
</div>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
name: "index",=
data() {
return {
input: '',
input1: '',
listUser: [],
webSocket : null,
fromUser: '游客',
toUser: '游客',
}
},
mounted() {
if (document.cookie === '') {
this.webSokcetClose();
this.$router.push({path:'/'});
} else {
this.fromUser = this.$cookie.getCookie('user');
}
this.webSocket = new WebSocket('ws://192.168.1.125:8855/chat/' + this.fromUser);
this.initWebSocket();
},
methods: {
initWebSocket() {
this.webSocket.onerror = this.onError; // 通讯异常
this.webSocket.onopen = this.onOpen; // 连接成功
this.webSocket.onmessage = this.onMessage; // 收到消息时回调
this.webSocket.onclose = this.onClose; // 连接关闭时回调
},
onError() {
/*
* 通讯异常
* */
console.log("通讯异常")
},
onOpen() {
/*
* 连接成功
* */
console.log("通讯开始");
},
onMessage(event) {
/*
* 收到消息时回调函数
* */
let data = JSON.parse(event.data);
// console.log(data);
if (data.list !== undefined) {
let list = data.list;
for (let i=0; i<list.length; i++) {
if (list[i] === this.fromUser) {
list.splice(i, 1);
}
}
this.listUser = list;
}
this.messageDiv(event.data);
},
onClose() {
/*
* 关闭连接时回调函数
* */
console.log("通讯关闭");
},
webSokcetSend() {
/*
* 发送消息
* */
let message = JSON.stringify({'fromUser': this.fromUser, 'toUser': this.toUser, 'msg': this.input,});
this.webSocket.send(message);
this.input = '';
this.messageDiv(message)
},
webSokcetClose() {
/*
* 关闭连接
* */
this.webSocket.close();
this.$cookie.delCookie('user');
this.$router.push({path:'/'});
},
toggleChat(toUser) {
this.toUser = toUser;
},
messageDiv(data1, type) {
let data = JSON.parse(data1);
let div = document.createElement('div');
let p1 = document.createElement('p');
let p = document.createElement('p');
p1.innerHTML = data.fromUser;
p.innerHTML = data.msg;
if (data.fromUser !== '系统通知') {
if (data.type !== undefined) {
/*
* data.type !== undefined
* 说明这是接收到消息
* 把值插到发送者的div
* */
let fromUser = document.getElementById(data.fromUser);
div.style = 'width: 370px; float: left; margin-left: 15px; margin-top: 15px;';
p1.style = 'font-size: 15px; margin-bottom: 0; margin-top: 0; float: left; font-weight: 500;';
p.style = 'padding: 10px; background-color: #F1F1F1; margin-top: 25px; margin-bottom: 10px; word-wrap : break-word;';
div.appendChild(p1);
div.appendChild(p);
fromUser.appendChild(div);
fromUser.scrollTop = fromUser.scrollHeight;
if (fromUser.style.display === 'none') {
console.log('有新的消息未查看!');
}
} else {
/*
* 说明这是发送消息
* 把值插到接收者的div
* */
let toUser = document.getElementById(data.toUser);
div.style = 'width: 370px; float: right; margin-right: 15px; margin-top: 15px;';
p1.style = 'font-size: 15px; margin-bottom: 0; margin-top: 0; float: right; font-weight: 500;';
p.style = 'padding: 10px; background-color: #9FE86C; margin-top: 25px; margin-bottom: 10px; word-wrap : break-word;';
div.appendChild(p1);
div.appendChild(p);
toUser.appendChild(div);
toUser.scrollTop = toUser.scrollHeight;
}
}
}
},
beforeRouteEnter(to, from, next) {
// 添加背景色
document.querySelector('body').setAttribute('style', 'background-color: #F9F9F9');
next()
},
beforeRouteLeave(to, from, next) {
// 去除背景色
document.querySelector('body').setAttribute('style', '');
next()
},
}
</script>
<style scoped>
.limitTitleDirectory {
width: 120px; /* 限制文本宽度 */
overflow: hidden; /* 超出的文本隐藏 */
text-overflow: ellipsis; /* 溢出的文本内容用 ... 代替 */
white-space: nowrap; /* 溢出不换行*/
}
element.style {
padding-left: 10px;
}
.el-menu-item {
font-size: 14px;
color: #303133;
padding: 0 10px;
cursor: pointer;
-webkit-transition: border-color .3s,background-color .3s,color .3s;
transition: border-color .3s,background-color .3s,color .3s;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
/*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/
::-webkit-scrollbar
{
width: 5px; /*滚动条宽度*/
height: 5px; /*滚动条高度*/
}
/*定义滚动条轨道 内阴影+圆角*/
::-webkit-scrollbar-track
{
/*-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);*/
border-radius: 10px; /*滚动条的背景区域的圆角*/
/*background-color: red;!*滚动条的背景颜色*!*/
}
/*定义滑块 内阴影+圆角*/
::-webkit-scrollbar-thumb
{
border-radius: 10px; /*滚动条的圆角*/
/*-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);*/
background-color: #b8b8bc; /*滚动条的背景颜色*/
}
</style>
前端源码: https://gitee.com/chenbz2/websocketvue
后端源码: https://gitee.com/chenbz2/websocketspring
参考博客: