一、websocket 介绍
websocket
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。使用WebSocket,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。属于服务器推送技术的一种。
二、效果示意图
三、功能实现
要看的懂下面的代码,首先你必须学会springboot和vue的相关知识。
话不多说,直接上代码。
后端代码
maven坐标
<!-- webSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
WebSocket配置类
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
pojo实体类:负责封装发送消息的实体类
package com.jia.pojo.chat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* @author 侠客
* Date 2022/8/15 20:56
* Description: 服务器发给浏览器的webSocket数据
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain=true)
public class MsgResult {
private boolean systemMsgFlag;
private String from;
private Object sendMsg;
}
封装消息工具类:负责判断系统或者群聊消息,并转换成json格式
package com.jia.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jia.pojo.chat.MsgResult;
/**
* @author 侠客
* Date 2022/8/15 20:58
*/
public class MessageUtils {
/**
* 封装响应的消息,系统消息的发送fromName为null;
* 封装好的响应如下,例如
* 系统消息: {“systemMsgFlag”: true, "fromName": null, "message": ["Name1", "Name2"]}.
* 非系统消息 {“systemMsgFlag”: false, "fromName": "YYJ", "message": “你在哪里呀?”}.
* @param systemMsgFlag 是否是系统消息
* @param from 发送方名称
* @param message 发送的消息内容
* @return java.lang.String json字符串
*/
public static String getMessage(boolean systemMsgFlag, String from, Object message) {
MsgResult resultMessage = new MsgResult();
resultMessage.setSystemMsgFlag(systemMsgFlag);
resultMessage.setSendMsg(message);
if (!systemMsgFlag) {
resultMessage.setFrom(from);
}
ObjectMapper objectMapper = new ObjectMapper();
String repStr = null;
try {
repStr = objectMapper.writeValueAsString(resultMessage);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return repStr;
}
}
核心来了,ChatEndPoint类,就是websocket业务核心类了
package com.jia.webSocket;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jia.utils.MessageUtils;
import lombok.extern.slf4j.Slf4j;
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.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author 侠客
* Date 2022/8/15 21:01
* Description webSocket核心业务类(用户间私信业务)
*/
@ServerEndpoint("/chat/{name}")
@Component
@Slf4j
public class ChatEndPoint {
public static Map<String, ChatEndPoint> onlineUsers = new ConcurrentHashMap<>();
private Session session;
private String name;
/**
* 获取所有用户姓名列表
* @return 用户姓名list
*/
public Set<String> getAllUser() {
return ChatEndPoint.onlineUsers.keySet();
}
/**
* 广播发送消息
* @param message 发送消息
*/
private void broadcastMsgToAllOnlineUsers(String message) {
Set<String> names = onlineUsers.keySet();
for (String name : names) {
ChatEndPoint chatEndpoint = onlineUsers.get(name);
RemoteEndpoint.Basic basicRemote = chatEndpoint.session.getBasicRemote();
try {
log.info("广播发送消息:"+message);
basicRemote.sendText(message);
} catch (IOException e) {
log.error("broadcastMsgToAllOnlineUsers错误");
e.printStackTrace();
}
}
}
/**
* 上线通知展示
* @param message
*/
private void onlineToUsers(String message) {
Set<String> names = onlineUsers.keySet();
for (String name : names) {
ChatEndPoint chatEndpoint = onlineUsers.get(name);
RemoteEndpoint.Basic basicRemote = chatEndpoint.session.getBasicRemote();
try {
log.info("广播发送消息:"+message);
basicRemote.sendText(message);
} catch (IOException e) {
log.error("broadcastMsgToAllOnlineUsers错误");
e.printStackTrace();
}
}
}
@OnOpen
public void onOpen(Session session,@PathParam(value = "name") String name){
if (name==null) {
try {
session.close();
} catch (IOException e) {
log.error("onOpen错误");
e.printStackTrace();
}
}
else {
this.name=name;
log.info(name + "已经上线!");
this.session = session;
onlineUsers.put(name, this);
String message = MessageUtils.getMessage(true, null, getAllUser());
onlineToUsers(message);
}
}
@OnClose
public void onClose(){
log.info(this.name+"已下线!");
onlineUsers.remove(this.name);
String message = MessageUtils.getMessage(true, null, getAllUser());
broadcastMsgToAllOnlineUsers(message);
}
@OnMessage
public void onMessage(String msg){
if (msg == null) {
return;
}
ObjectMapper objectMapper = new ObjectMapper();
try {
String message= objectMapper.readValue(msg,String.class);
log.info("服务器接收到消息"+message);
//获取发送的消息
String sendMsg=MessageUtils.getMessage(false, this.name,message);
broadcastMsgToAllOnlineUsers(sendMsg);
} catch (JsonProcessingException e) {
log.error("onMessage错误");
e.printStackTrace();
}
}
@OnError
public void onError(Throwable error) throws IOException {
log.error("聊天系统错误:"+error.getMessage());
String message = MessageUtils.getMessage(true, null, getAllUser());
broadcastMsgToAllOnlineUsers(message);
}
}
vue前端代码
<template>
<div style="display: flex;justify-content: center;align-items: center;background: #404847;height: 630px;">
<div class="main">
<div class="left">
<div style="position:relative;height:100%">
<el-card style="position:relative;height:100%;overflow: auto">
<template #header>
<div class="header">
<span style="font-weight: bolder">在线博友</span>
</div>
</template>
<div v-for="o in friend" :key="o" style="font-weight: bold;margin-bottom: 5px;">{{o}}</div>
</el-card>
</div>
</div>
<div class="right">
<div class="msg">
<div class="name">
<el-card>
<span style="position:relative;margin-top: 30px;font-weight: bolder;">欢迎来到【简印】星球</span>
<span style="position: relative;float: right;font-weight: normal">
你的昵称 : <span style="font-weight: bolder">{{myName}}</span>
</span>
</el-card>
</div>
<el-card class="text">
<div v-for="o in otherMsg" :key="o" class="text item">
<template v-if="o.from!==myName">
<div style="text-align: left">{{o.from}}<br/>
<div style="background: gainsboro;position: relative;float: left;width: 50%;text-align: left;padding:5px;word-break:break-all">
<span style="position: relative;left: 0">{{o.sendMsg}}</span>
</div>
</div>
</template>
<template v-else>
<div style="text-align: right">{{o.from}}<br/>
<div style="background: cornflowerblue;position: relative;float: right;width: 50%;text-align: left;padding:5px;word-break:break-all">
<span style="position: relative;left: 0">{{o.sendMsg}}</span>
</div>
</div>
</template>
</div>
</el-card>
<!-- <div class="text">
<div v-for="o in otherMsg" :key="o" class="text item">
<template v-if="o.from!==myName">
<div style="text-align: left">{{o.from}}<br/>{{o.sendMsg}}</div>
</template>
<template v-else>
<div style="text-align: right">{{o.from}}<br/>{{o.sendMsg}}</div>
</template>
</div>
</div>-->
</div>
<div class="send">
<el-input v-model="text" :autosize="{ minRows: 5, maxRows: 10 }" type="textarea"
placeholder="Please input" @keyup.enter.native="send"
style="position:relative;height:78%;overflow: auto"/>
<el-button @click="send" type="primary" style="position:relative;float: right;right: 2%;" >发送</el-button>
</div>
</div>
</div>
</div>
</template>
<script>
import Cookie from "_vue-cookies@1.8.1@vue-cookies";
import {ElMessageBox,ElMessage} from "element-plus";
export default {
beforeRouteLeave (to, from, next) {
ElMessageBox.confirm(
'正在离开本页面,本页面内所有未保存数据都会丢失',
'警告',
{
confirmButtonText: '离开',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(() => {
this.exit();
next();
})
.catch(() => {
window.history.go(1);
ElMessage({
type: 'info',
message: '取消操作!',
})
})
},
name: "AllChat",
data(){
return{
ws:null,
myName:'我的名字',
friend:[],
text:'',
myMsg:[],
otherMsg:[],
list:{
systemMsgFlag:'',
from:'',
sendMsg:''
}
}
},
update() {
this.scrollToBottom()
},
mounted() {
this.getConnect();
this.scrollToBottom()
},
watch:{
/* '$route.path'( to , from ){
console.log( to , from )
if( to !== '/chat'){
this.exit();
}
}*/
},
methods:{
scrollToBottom(){
const container=document.getElementsByClassName("text");
container.scrollTop=container.scrollHeight;
},
getConnect(){
const tokenStr = Cookie.get("user");
this.ws = new WebSocket(`ws://124.71.59.103:8090/chat/`+tokenStr.name);
this.myName = tokenStr.name;
this.ws.onopen = this.open
this.ws.onclose = this.close
this.ws.onerror = this.error
this.ws.onmessage = this.getMessage
},
open(){
window.onunload=function(){
this.ws.close()
};
window.onbeforeunload=function(){
this.ws.close()
};
},
exit(){
this.ws.close();
},
error(data){
console.log("webSocket连接错误:",data);
this.ws.close();
},
getMessage(msg) {
this.list=JSON.parse(JSON.stringify(msg.data));
if (typeof this.list ==='string'){
this.list = JSON.parse(this.list)
}
console.log(this.list)
if (this.list.systemMsgFlag){
this.friend=this.list.sendMsg;
}else {
if (this.list.sendMsg!=null){
// if (this.list.from!==this.myName) {
this.otherMsg.push(this.list);
/* }*/
}
this.scrollToBottom();
}
},
close() {
this.ws.close();
},
send(){
const a=this.text;
if (a.match(/^\s*$/)){
ElMessage({
showClose: true,
message: '消息不可为空,发送失败!',
type: 'warning',
})
}else {
this.myMsg.push(this.text);
this.ws.send(JSON.stringify(this.text));
this.text='';
this.scrollToBottom();
}
}
}
}
</script>
<style scoped>
.main{
display:flex;
position: relative;
width: 65%;
margin: 0;
height: 95%;
}
.left{
height: 100%;
float: left;
width: 30%;
}
.right{
-webkit-flex: 1;
width: 70%;
left: 30%;
height: 100%;
}
.msg{
background: cornsilk;
height: 70%;
}
.name{
position: relative;
background: cornflowerblue;
height: 15%;
font-weight: bolder;
left: 0
}
.text{
height: 85%;
overflow:auto;
}
.send{
background: aliceblue;
height: 30%;
}
</style>