webSocket信息推送 实现客服聊天功能 群发和点对点
程序员文章站
2022-05-21 14:22:48
...
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
客服系统开发需要用到点对点聊天,如果是不断访问接口的形式获取数据,性能消耗大
用WebSocket通信技术,这样的话只需要连接一次,信息即可互相推送
解决思路
1.当用户或者客服进入聊天界面时,发起ws请求
“ws://127.0.0.1:8888/websocket/”+id
id为唯一标识符,辨别连接用户
2.发送信息 根据id发送信息
3.接收信息 前端接收信息包含接收人id,如果接收人是当前聊天对话框的用户,就把数据渲染到对话框里
步骤
1.引入pox
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier>
</dependency>
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
@Component
@ServerEndpoint("/websocket/{id}")
public class WebSocket {
private Session session;
private static CopyOnWriteArraySet<WebSocket> webSockets = new CopyOnWriteArraySet<>();
private static Map<String, Session> sessionPool = new HashMap<String, Session>();
@OnOpen
public void onOpen(Session session, @PathParam(value = "id") String id) {
this.session = session;
webSockets.add(this);
sessionPool.put(id, session);
System.out.println(id + "【websocket消息】有新的连接,总数为:" + webSockets.size());
}
@OnClose
public void onClose() {
webSockets.remove(this);
System.out.println("【websocket消息】连接断开,总数为:" + webSockets.size());
}
@OnMessage
public void onMessage(String message) {
System.out.println("【websocket消息】收到客户端消息:" + message);
}
public void sendAllMessage(String message) {
for (WebSocket webSocket : webSockets) {
System.out.println("【websocket消息】广播消息:" + message);
try {
webSocket.session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void sendOneMessage(String id, String message) {
System.out.println("【websocket消息】单点消息:" + message);
Session session = sessionPool.get(id);
if (session != null) {
try {
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
发送
String msg="";
String todeuser=""+touser;
System.out.println("todeuser"+todeuser);
JSONObject json;
//1.将Bean转换为Json格式
json = JSONObject.fromObject(chatVo);
//2.将Json格式转换为String类型
msg = json.toString();
System.out.println("Bean转换为Json:" + msg);
webSocket.sendOneMessage(todeuser,msg);
chatVo
package com.gdkm.vo;
import lombok.Data;
@Data
public class ChatVo {
/**
* 发送者
*/
private Integer userId;
/**
* id
*/
private Integer cId;
/**
* 头像
*/
private String userIocn;
/**
* 内容
*/
private String cltext;
/**
* 时间
*/
private Data createtime;
/**
* 0是未读
*/
private String clstate;
/**
* 0是自己 1是对方
*/
private String tpye;
}
我的前端页面
<template>
<div class="chat">
<el-card class="box-card box-cards">
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>在线客服</el-breadcrumb-item>
</el-breadcrumb>
</el-card>
<div class="box-card box-cards">
<el-row class="con">
<el-col :span="24">
<div class="righthead">
<h4>{{userNickname}}</h4>
</div>
<div class="context" id="liao">
<el-row class="itemchat" v-for="(item, index) in chatlogList" :key="index" >
<el-avatar class="chattouxiang" :class="item.tpye==0?'right':'left'" :size="40" :src="item.userIocn"></el-avatar>
<div class="send" :class="item.tpye==0?'right':'left'">
<span>{{item.cltext}}
</span>
<div class="arrow" :class="item.tpye==0?'sendright':'sendleft'"/>
</div>
<!-- <span :class="item.tpye==0?'right':'left'" class="clstate">{{item.clstate==0?'未读':'已读'}}-->
<!-- </span>-->
</el-row>
</div>
<div class="rightbottom">
<textarea v-model="textarea" @keydown.13="tosendOneWebSocket()" ></textarea>
</div>
</el-col>
</el-row>
</div>
<log ref="child" v-show="this.userNickname!=''"/>
</div>
</template>
<script>
import {appuser,chatlog,sendOneWebSocket,upchatlog} from "../../network/chat";
import {userInfo} from "../../network/me";
import Log from "./chatComps/Log";
export default {
name: "Chat",
data(){
return {
touser:1,
textarea:'',
appuser:[],
chatlogList:[],
userNickname:'客服',
}
},
beforeCreate() {
},
components:{
Log
},
methods:{
tosendOneWebSocket(){
let text=this.textarea
let id=this.touser
sendOneWebSocket(id,text).then(res=>{
console.log(res)
})
const log = {
"userIocn": this.$store.state.user.userIcon,
"cltext": this.textarea,
"clstate": "1",
"tpye": "0"
}
this.chatlogList.push(log)
this.textarea=''
},
tochatlog(touser){
chatlog(touser).then(res=>{
console.log(res)
this.chatlogList= res.data
})
},
toappuser(){
appuser().then(res=>{
console.log(res)
this.appuser=res.data
})
},
initWebSocket() {
// 连接错误
this.websocket.onerror = this.setErrorMessage;
// 连接成功
this.websocket.onopen = this.setOnopenMessage;
// 收到消息的回调
this.websocket.onmessage = this.setOnmessageMessage;
// 连接关闭的回调
this.websocket.onclose = this.setOncloseMessage;
// 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = this.closeWebSocket;
},
setErrorMessage() {
// eslint-disable-next-line no-console
console.log(
"WebSocket连接发生错误 状态码:" + this.websocket.readyState
);
},
setOnopenMessage() {
// eslint-disable-next-line no-console
console.log("WebSocket连接成功 状态码:" + this.websocket.readyState);
},
setOnmessageMessage(event) {
let data=eval("("+event.data+")")
upchatlog(data.CId)
this.chatlogList.push(data);
},
setOncloseMessage() {
// eslint-disable-next-line no-console
console.log("WebSocket连接关闭 状态码:" + this.websocket.readyState);
},
closeWebSocket() {
this.websocket.close();
},
},
updated(){
let ele= document.getElementById("liao");
ele.scrollTop = ele.scrollHeight;
},
mounted() {
window.onbeforeunload = this.closeWebSocket;
this.websocket = new WebSocket(
"ws://127.0.0.1:8888/linux/websocket/"+this.$store.state.user.userId
)
this.initWebSocket();
this.tochatlog(1);
userInfo(1).then(res=>{
const user=res.data
this.userNickname=user.userNickname
})
this.$refs.child.parentMsg(this.touser)
},
beforeRouteEnter:(to,from,next)=>{
next(vm => {
if (vm.$store.state.user.roleId!=1){
vm.$message.error('权限不通过');
next('/home');
}
})
}
}
</script>
<style scoped>
.chat{
}
.clstate{
font-size: 11px;margin-top: 20px;margin-left: 10px;margin-right: 10px
}
textarea{
width: 100%;
height: 100%;
resize:none;
border: none;
outline: none;
}
.items{
margin-left:5px ;
}
.item{
height: 70px;
background-color: #2E2E2E;
border: 0;
margin-top: 1px;
line-height: 70px;
color: white;
border-radius: 5px;
}
.itemtouxiang{
float: left;
margin:15px 10px 0px 15px;
}
.touxiang{
margin-top: 15px;
margin-left: 15px;
margin-right: 10px;
float: left;
}
.con{
width: 100%;
}
.box-cards {
width: 65%;
margin: 20px auto;
}
.lefthead{
height: 80px;
background-color: #2E2E2E;
border-bottom: 1px white solid;
line-height: 80px;
color: white;
}
::-webkit-scrollbar {
width: 0 !important;
}
.leftbottom{
height: 530px;
background-color: #3D3D3D;
overflow: auto;
}
.righthead{
height: 60px;
border-bottom: 1px #F5F5F5 solid;
background-color: white;
line-height: 60px;
padding-left: 20px;
}
.context{
height: 360px;
border-bottom: 1px #E8E8E8 solid;
overflow: auto;
background-color: #F5F5F5;
padding: 20px 20px;
}
.rightbottom{
height: 130px;
padding: 10px 10px;
background-color: #FFFFFF;
}
.send {
position:relative;
background:#3399FF;
border-radius:5px; /* 圆角 */
padding:10px 10px;
line-height: 20px;
max-width: 60%;
}
.send .arrow {
position:absolute;
top:9px;
width:0;
height:0;
font-size:0;
border:solid 8px;
}
.send .sendright{
right:-15px; /* 圆角的位置需要细心调试哦 */
border-color:#F5F5F5 #F5F5F5 #F5F5F5 #3399FF;
}
.send .sendleft{
left:-15px; /* 圆角的位置需要细心调试哦 */
border-color:#F5F5F5 #3399FF #F5F5F5 #F5F5F5;
}
.send span{
font-size: 15px;
word-break:break-word;
}
.chattouxiang{
margin-left: 20px;
margin-right: 20px;
}
.right{
float: right;
}
.left{
float: left;
}
.itemchat{
margin-top: 10px;
}
</style>
<template>
<div class="chat">
<el-card class="box-card box-cards">
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>客服中心</el-breadcrumb-item>
</el-breadcrumb>
</el-card>
<div class="box-card box-cards">
<el-row class="con">
<el-col :span="7">
<div class="lefthead">
<el-avatar class="touxiang" :size="50" :src="this.$store.state.user.userIcon"></el-avatar>
<h3>{{this.$store.state.user.userNickname}}</h3>
</div>
<div class="leftbottom">
<div class="item" v-for="(item, index) in appuser" :key="index" @click="tochatlog(item.userId,item.userNickname)"
v-show="item.userId != $store.state.user.userId"
>
<!-- this.$router.push('/user/'+item.userId)-->
<div @click="touserInfo(item.userId)">
<el-avatar class="itemtouxiang" :size="40" :src="item.userIcon" ></el-avatar>
</div>
<h5>{{item.userNickname}}<el-badge is-dot class="items" v-if="item.xinxi"></el-badge></h5>
</div>
</div>
</el-col>
<el-col :span="17" v-if="showright">
<div class="righthead">
<h4>{{userNickname}}</h4>
</div>
<div class="context" id="liao2">
<el-row class="itemchat" v-for="(item, index) in chatlogList" :key="index" >
<el-avatar class="chattouxiang" :class="item.tpye==0?'right':'left'" :size="40" :src="item.userIocn"></el-avatar>
<div class="send" :class="item.tpye==0?'right':'left'">
<span>{{item.cltext}}
</span>
<div class="arrow" :class="item.tpye==0?'sendright':'sendleft'"/>
</div>
<!-- <span :class="item.tpye==0?'right':'left'" class="clstate" v-show="item.tpye==0" >{{item.clstate==0?'未读':'已读'}}-->
<!-- </span>-->
</el-row>
</div>
<div class="rightbottom">
<textarea v-model="textarea" @keydown.13="tosendOneWebSocket()" ></textarea>
</div>
</el-col>
</el-row>
</div>
<log ref="child" v-show="this.userNickname!=''"/>
</div>
</template>
<script>
import {appuser,chatlog,sendOneWebSocket,upchatlog} from "../../network/chat";
import Log from "./chatComps/Log";
export default {
name: "Chat",
data(){
return {
textarea:'',
appuser:[],
chatlogList:[],
userNickname:'',
showright:true,
touser:0,
}
},
updated(){
let ele= document.getElementById("liao2");
ele.scrollTop = ele.scrollHeight;
},
computed:{
logshow(){
return false
}
},
components:{
Log
},
beforeCreate() {
},
methods:{
touserInfo(userId){
this.$router.push('/user/'+userId)
},
tosendOneWebSocket(){
let text=this.textarea
let id=this.touser
sendOneWebSocket(id,text).then(res=>{
console.log(res)
})
const log = {
"userIocn": this.$store.state.user.userIcon,
"cltext": this.textarea,
"clstate": "1",
"tpye": "0"
}
this.chatlogList.push(log)
this.textarea=''
},
//websocket
initWebSocket() {
// 连接错误
this.websocket.onerror = this.setErrorMessage;
// 连接成功
this.websocket.onopen = this.setOnopenMessage;
// 收到消息的回调
this.websocket.onmessage = this.setOnmessageMessage;
// 连接关闭的回调
this.websocket.onclose = this.setOncloseMessage;
// 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = this.closeWebSocket;
},
setErrorMessage() {
// eslint-disable-next-line no-console
console.log(
"WebSocket连接发生错误 状态码:" + this.websocket.readyState
);
},
setOnopenMessage() {
// eslint-disable-next-line no-console
console.log("WebSocket连接成功 状态码:" + this.websocket.readyState);
},
setOnmessageMessage(event) {
let data=eval("("+event.data+")")
console.log(data)
if(data.userId == this.touser){
upchatlog(data.CId)
this.chatlogList.push(data);
}
},
setOncloseMessage() {
// eslint-disable-next-line no-console
console.log("WebSocket连接关闭 状态码:" + this.websocket.readyState);
},
closeWebSocket() {
this.websocket.close();
},
tochatlog(touser,userNickname){
this.logshow=true
this.touser=touser
chatlog(touser).then(res=>{
this.chatlogList= res.data
this.userNickname=userNickname
this.showright=true
})
// setInterval(() => {
// this.tochatlog();
// }, 300);
this.$refs.child.parentMsg(this.touser)
},
toappuser(){
appuser().then(res=>{
this.appuser=res.data
})
},
send(){
console.log("发送")
const log = {
"userIocn": "http://gdkmlzh.cn-gd.ufileos.com/img%2F5e3af915-83c8-4f23-9953-25ccb46363c1.jpg?UCloudPublicKey=TOKEN_0c9d0118-e892-4784-8702-34bf5b992604&Signature=8p462HhcXyhM%2FJmhhnygrZotLjw%3D&Expires=1891772663",
"cltext": this.textarea,
"clstate": "0",
"tpye": "1"
}
this.chatlogList.push(log)
this.textarea=''
},
},
mounted() {
window.onbeforeunload = this.closeWebSocket;
this.websocket = new WebSocket(
"ws://127.0.0.1:8888/linux/websocket/"+this.$store.state.user.userId
)
this.initWebSocket();
this.toappuser()
setInterval(() => {
this.toappuser();
}, 3000);
},
beforeRouteEnter:(to,from,next)=>{
next(vm => {
if (vm.$store.state.user.roleId!=2){
vm.$message.error('权限不通过');
next('/home');
}
})
},
}
</script>
<style scoped>
.page{
margin:10px;
text-align: center;
}
.chat{
}
.clstate{
font-size: 11px;margin-top: 20px;margin-left: 10px;margin-right: 10px
}
textarea{
width: 100%;
height: 100%;
resize:none;
border: none;
outline: none;
}
.items{
margin-left:5px ;
}
.item{
height: 70px;
background-color: #2E2E2E;
border: 0;
margin-top: 1px;
line-height: 70px;
color: white;
border-radius: 5px;
}
.itemtouxiang{
float: left;
margin:15px 10px 0px 15px;
}
.touxiang{
margin-top: 15px;
margin-left: 15px;
margin-right: 10px;
float: left;
}
.con{
width: 100%;
}
.box-cards {
width: 65%;
margin: 20px auto;
}
.lefthead{
height: 80px;
background-color: #2E2E2E;
border-bottom: 1px white solid;
line-height: 80px;
color: white;
}
::-webkit-scrollbar {
width: 0 !important;
}
.leftbottom{
height: 530px;
background-color: #3D3D3D;
overflow: auto;
}
.righthead{
height: 60px;
border-bottom: 1px #F5F5F5 solid;
background-color: white;
line-height: 60px;
padding-left: 20px;
}
.context{
height: 360px;
border-bottom: 1px #E8E8E8 solid;
overflow: auto;
background-color: #F5F5F5;
padding: 20px 20px;
}
.rightbottom{
height: 130px;
padding: 10px 10px;
background-color: #FFFFFF;
}
.send {
position:relative;
background:#3399FF;
border-radius:5px; /* 圆角 */
padding:10px 10px;
line-height: 20px;
max-width: 60%;
}
.send .arrow {
position:absolute;
top:9px;
width:0;
height:0;
font-size:0;
border:solid 8px;
}
.send .sendright{
right:-16px; /* 圆角的位置需要细心调试哦 */
border-color:#F5F5F5 #F5F5F5 #F5F5F5 #3399FF;
}
.send .sendleft{
left:-16px; /* 圆角的位置需要细心调试哦 */
border-color:#F5F5F5 #3399FF #F5F5F5 #F5F5F5;
}
.send span{
font-size: 15px;
word-break:break-word;
}
.chattouxiang{
margin-left: 20px;
margin-right: 20px;
}
.right{
float: right;
}
.left{
float: left;
}
.itemchat{
margin-top: 10px;
}
.box-cards2 {
width: 65%;
margin: 20px auto;
}
.context2{
border-bottom: 1px #E8E8E8 solid;
background-color: #F5F5F5;
padding: 20px 20px;
}
</style>
上一篇: 比特币客户端Electrum使用介绍
下一篇: WebRTC之P2P