根据菜鸟教程上的解释:
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
业务发展需要进行一个简单的在线客服和留言,最简单的办法是ajax定时器轮询,虽然访问量不大,这用方法也简单可靠,但这种方法需要数据库的支撑以及不灵活,所以选择websocket
效果图:前端是vue,后端是ssm,这里展示后端对后端,原理一样,如果是前后端分离环境,复制粘贴即可(如果是,则需要设置跨域,这里不解释)。
客服端:如果客服未在线也可以看到其聊天信息,如果不点击沟通或回复,点击自己的话默认是群聊,点击为私聊
用户端:默认客服,可以查看自己的聊天记录,如果客服没在线则可以相应给出提示,这里只是简单版本
步骤:
1.项目使用的是ssm 默认已经将spring的mvc什么的已经引入,首先引入相关jar包,在maven仓库中均可搜到
<!--websocket-->
<!-- https://mvnrepository.com/artifact/javax.websocket/javax.websocket-api -->
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>
2.从外往里配置,需要两个配置文件,webscoket走的是session共享机制,所以需要将所有请求附加session(不做解释,配好就行,两个文件位置随意,根据自己项目结构定义,只要同包即可)
第一个:
@Component
@WebListener
public class RequestListener implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
//将所有request请求都携带上httpSession
HttpSession session = ((HttpServletRequest) sre.getServletRequest()).getSession();
}
public RequestListener() {}
@Override
public void requestDestroyed(ServletRequestEvent arg0) {}
}
第二个:
public class HttpSessionConfigurator extends Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response){
HttpSession httpSession = (HttpSession)request.getHttpSession();
config.getUserProperties().put(HttpSession.class.getName(),httpSession);
}
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3.这里编写前端代码,css和js都是样式,可以根据需要引入
css:
<%--jquery-ui 让div可以随意拖动--%>
<link rel="stylesheet" href=".../static/css/jquery-ui.css"/>
<link rel="stylesheet" href=".../plugins/amaze/css/amazeui.min.css">
<link rel="stylesheet" href=".../plugins/amaze/css/admin.css">
<link rel="stylesheet" href=".../plugins/contextjs/css/context.standalone.css">
<%--layui--%>
<link rel="stylesheet" href=".../static/layui/css/layui.css">
<%--layer--%>
<link rel="stylesheet" href=".../static/layer/mobile/need/layer.css">
<link rel="stylesheet" href="http://jqueryui.com/resources/demos/style.css">
<link rel="stylesheet" href=".../static/font/css/font-awesome.min.css">
<link id="layuicss-skinlayimcss" rel="stylesheet" href="https://robot.rszhang.top/plugins/layui/css/modules/layim/layim.css?v=3.01Pro" media="all">
js:这里是layui的富文本编辑器,简单好用,就是功能较少
<%--<script src=".../plugins/jquery/jquery-2.1.4.min.js"></script>--%>
<script src=".../plugins/amaze/js/amazeui.min.js"></script>
<script src=".../plugins/amaze/js/app.js"></script>
<%--<script src=".../statics/plugins/layer/layer.js"></script>--%>
<script src=".../plugins/laypage/laypage.js"></script>
<script src=".../plugins/contextjs/js/context.js"></script>
<%--layui富文本编辑器--%>
<script type="text/javascript" charset="utf-8" src=".../static/ueditor/ueditor.config.js"></script>
<script type="text/javascript" charset="utf-8" src=".../static/ueditor/ueditor.all.min.js"> </script>
<script type="text/javascript" charset="utf-8" src=".../static/ueditor/lang/zh-cn/zh-cn.js"></script>
页面:默认jsp
用户端: to可以动态分配,但也业务需求是 1 V N 所以就设置为死的,ws地址可以设置为服务器地址,但会有很多问题(nginx配置,httpRequest等)
<html>
<head>
<title>客户端</title>
</head>
<style>
*{
font-family: 微软雅黑;
}
.body{
width: 818px;
height: 495px;
margin-top: 20px;
/*display: none;*/
background: #ffffff;
}
.title{
width: 100%;
height: 45px;
background: #DC143C;
}
.title-left{
text-align:center;
width: 640px;
height: 100%;
line-height: 45px;
float:left;
}
.title-right{
width: 176px;
height: 45px;
text-align: right;
line-height: 45px;
float: left;
}
.content{
width: 100%;
height: 450px;
}
.content-left{
float: left;
width:640px;
height: 100%;
border: 1px solid #E3E3E3;
}
.content-right{
float: left;
width:178px;
height: 100%;
border-right: 1px solid #E3E3E3;
border-bottom: 1px solid #E3E3E3;
}
.content-left-title{
width: 100%;
height: 40px;
padding: 10px;
}
.content-left-body{
width: 640px;
height: 260px;
padding: 5px;
}
.content-left-body ul li{
position: relative;
font-size: 0;
margin-bottom: 10px;
padding-left: 60px;
min-height: 68px;
}
.layim-chat-user {
position: absolute;
left: 3px;
}
.layim-chat-user img {
width: 40px;
height: 40px;
border-radius: 100%;
}
.layim-chat-user cite {
position: absolute;
left: 60px;
top: -2px;
width: 500px;
line-height: 24px;
font-size: 12px;
white-space: nowrap;
color: #999;
text-align: left;
font-style: normal;
}
.layim-chat-user cite i {
padding-left: 15px;
font-style: normal;
}
.layim-chat-user {
display: inline-block;
vertical-align: top;
font-size: 14px;
}
.layim-chat-text {
position: relative;
line-height: 22px;
margin-top: 25px;
padding: 8px 15px;
background-color: #e2e2e2;
border-radius: 3px;
color: #333;
word-break: break-all;
max-width: 462px\9;
}
.layim-chat-text, .layim-chat-user {
display: inline-block;
vertical-align: top;
font-size: 14px;
}
.layim-chat-text:after {
content: '';
position: absolute;
left: -10px;
top: 13px;
width: 0;
height: 0;
border-style: solid dashed dashed;
border-color: #e2e2e2 transparent transparent;
overflow: hidden;
border-width: 10px;
}
.content-left-body ul .layim-chat-mine {
text-align: right;
padding-left: 0;
padding-right: 60px;
}
.layim-chat-mine .layim-chat-user cite {
left: auto;
right: 60px;
text-align: right;
}
.layim-chat-mine .layim-chat-user cite i {
padding-left: 0;
padding-right: 15px;
}
.layim-chat-mine .layim-chat-text {
margin-left: 0;
text-align: left;
background-color: #5FB878;
color: #fff;
}
.layim-chat-mine .layim-chat-text:after {
left: auto;
right: -10px;
border-top-color: #5FB878;
}
.layim-chat-mine .layim-chat-user cite {
left: auto;
right: 60px;
text-align: right;
}
.content-left-input{
width: 640px;
height: 150px;
border-top: 1px solid #E3E3E3;
position:relative;
}
.input-title{
width: 640px;
height: 30px;
padding: 5px;
line-height: 15px;
}
.input-body{
width: 640px;
height: 80px;
}
.input-Send{
width: 640px;
height: 40px;
position:absolute; bottom:0;
}
.chat-view{
overflow-x: hidden; overflow-y: auto;
}
.chat-view{
overflow-x: hidden;
}
ul,li{
list-style: none;
}
textarea{outline:none;resize:none;}
.input-core{
width: 35px;
height: 25px;
margin-left: 5px;
margin-right: 5px;
float: left;
}
/*在线列表css*/
.one-big{
width: 177px; height: 50px;
background: #ffffff;
}
.two-big{
width: 78px;height: 35px; float: left;
}
.three-big{
margin-left: 5px;line-height: 35px;
}
.four-big{
width: 90px;height: 35px; float: right;
}
.five-big{
width: 60px; background: #FF6666; height: 30px; text-align: center; line-height: 15px;color: #ffffff; font-size: 12px;
}
.five-big-one{
width: 60px; background: #339933; height: 30px; text-align: center; line-height: 15px;color: #ffffff; font-size: 12px;
}
.six-big{
margin-left: 10px;
}
.ur-stitle{
width: 300px;
height: 20px;
border: 1px solid;
}
.layui-layedit {
border: none;
}
/*右上角图标*/
.picon{
color: #ffffff; margin-right: 15px; font-size: 18px;
}
/*.picon:hover{*/
/*cursor: pointer;*/
/*}*/
.fa{
cursor: pointer;
}
</style>
<script>
var layeditIndex;
var layedit;
$(function () {
$( "#draggable" ).draggable();
$( "#sortable" ).sortable({
revert: true
});
layui.use('layedit', function(){
layedit = layui.layedit;
layedit.build('demo1'); //建立编辑器
layeditIndex = layedit.build('demo1', {
height: 65, //设置编辑器高度
});
});
})
</script>
</head>
<body>
<%--最外层div--%>
<div id="draggable" class="colPer" style="width: 818px; height: 495px; margin: auto;">
<%--头部导航条--%>
<div class="title">
<%--导航条左半部分--%>
<div class="title-left">
<a style="color: #ffffff" id="state" class="stateTitle">正在与客服沟通</a>
</div><%--导航条左半部分结束--%>
<div class="title-right">
<div style="">
<span class=" picon fa fa-minus" onclick="narrow()"></span>
<span class=" picon fa fa-times-rectangle" onclick="end()"></span>
</div>
</div>
</div><%--头部导航条结束--%>
<%--主体开始--%>
<div class="content">
<%--左边--%>
<div class="content-left">
<%--预留功能区域--%>
<div class="content-left-title">
<ul id="" style="width: 100%;">
<li class="input-core" id="arr" title="抖一抖">
<a class="fa fa-heartbeat" style="font-size: 20px;" id="pot"></a>
</li>
<li class="input-core" title="刷新窗口">
<a class="fa fa-refresh" style="font-size: 20px;"></a>
</li>
<li class="input-core" title="结束会话">
<a class="fa fa-unlink" style="font-size: 20px;"></a>
</li>
<li class="input-core" title="我的位置">
<a class="fa fa-bandcamp" style="font-size: 20px;"></a>
</li>
<li class="" style=" float: right; margin-right: 5px;" title="清空屏幕">
<a class="fa fa-trash" style="font-size: 20px;" onclick="clearConsole()"></a>
</li>
</ul>
</div>
<div class="content-left-body">
<!-- 聊天区 -->
<div class="am-cf admin-main">
<div class="admin-content">
<div class="am-scrollable-vertical" id="chat-view" style="height:100%; width: 100%;">
<ul class="am-comments-list am-comments-list-flip" id="chat">
</ul>
</div>
</div>
</div>
</div>
<div class="content-left-input">
<%--<%–输入区域预留功能–%>--%>
<%--<div class="input-title">--%>
<%--</div>--%>
<div class="input-body">
<textarea id="demo1" style="display: none; border: none;"> </textarea>
</div>
<div class="input-Send">
<div style="width: 300px; height: 80px; margin-right: 15px; float: right;">
<button type="button" class="TableEditor btn btn-default" onclick="sendMessage()" style="float: right; margin-left: 15px; width: 80px; height: 30px; line-height: 15px;background: #DC143C;color: #ffffff">发送</button>
<button type="button" class="TableEditor btn btn-default" style="float: right; margin-left: 15px; width: 80px; height: 30px; line-height: 15px">关闭</button>
</div>
</div>
</div>
</div>
<%--右边--%>
<div class="content-right">
</div>
</div>
</div><%--最外层结束--%>
</body>
<script type="text/javascript">
var wsServer = null;
var ws = null;
//上传到服务器只需将代码换为服务器地址即可
//websocket是可以传参数的
wsServer = "ws://localhost:8080/ChatController/1/${userCode}";
console.log(wsServer)
ws = new WebSocket(wsServer); //创建WebSocket对象
// ws = new SockJS(wsServer); //创建WebSocket对象
//onopen建立链接
ws.onopen = function (evt) {
layer.msg("已经建立连接", { offset: 0});
};
//onmessage发送消息
ws.onmessage = function (evt) {
analysisMessage(evt.data); //解析后台传回的消息,并予以展示
};
//onerror产生异常
ws.onerror = function (evt) {
layer.msg("产生异常", { offset: 0});
};
//关闭连接时
ws.onclose = function (evt) {
layer.msg("已经关闭连接", { offset: 0});
};
/**
* 发送信息给后台
*/
function sendMessage(){
$("#demo1").val(layedit.getContent(layeditIndex));
if(ws == null){
layer.msg("连接未开启!",{offset:0, shift: 6});
return false;
}
//获取富文本内容
var message;
var mess = layedit.getContent(layeditIndex);
$("#sendto").text("");
message = mess;
//因为业务需求,将客服定为死值,这里可以动态修改to的内容,但必须和你session中的名称一致
var to = '客服';
ws.send(JSON.stringify({
message : {
type:2,
content : message,
//from: 这里是客户的信息,随意指定
from : '${userid}',
to : to, //接收人,如果没有则置空,如果有多个接收人则用,分隔
time : getDateFull()
},
type : "message"
}));
layedit.setContent(layeditIndex,"");
}
function analysisMessage(message){
message = JSON.parse(message);
if(message.type == "message"){ //会话消息
showChat(message.message);
}
}
/**
* 展示会话信息
*/
function showChat(message){
var to = message.to == null || message.to == ""? "群聊" : message.to; //获取接收人
var isSef = '${userid}' == message.from ? "layim-chat-mine" : ""; //如果是自己则显示在右边,他人信息显示在左边
var html = " <li class="+isSef+">\n" +
" <div class=\"layim-chat-user\">\n" +
" <img src=\"${pageContext.request.contextPath}/static/img/touxiang.jpg\">\n" +
" <cite>\n" +
" \n" +
" "+message.time+"<i> "+message.from+"</i>\n" +
" </cite>\n" +
" </div>\n" +
" <div class=\"layim-chat-text\">\n" +
" "+message.content+"\n" +
" </div>\n" +
" </li>";
$("#chat").append(html);
var chat = $("#chat-view");
chat.scrollTop(chat[0].scrollHeight); //让聊天区始终滚动到最下面
}
/**
* 清空聊天区
*/
function clearConsole(){
$("#chat").html("");
}
function appendZero(s){return ("00"+ s).substr((s+"").length);} //补0函数
function getDateFull(){
var date = new Date();
var currentdate = date.getFullYear() + "-" + appendZero(date.getMonth() + 1) + "-" + appendZero(date.getDate()) + " " + appendZero(date.getHours()) + ":" + appendZero(date.getMinutes()) + ":" + appendZero(date.getSeconds());
return currentdate;
}
</script>
</html>
客服端:看着比较乱,但你大多数都不用看,只需要ws.open,ws.close,还有发送信息,展示信息四个方法即可,其他都是辅助
<html>
<head>
<title>客服端</title>
<style>
*{
font-family: 微软雅黑;
}
.body{
width: 818px;
height: 495px;
margin-top: 20px;
/*display: none;*/
background: #ffffff;
}
.title{
width: 100%;
height: 45px;
background: #DC143C;
}
.title-left{
text-align:center;
width: 640px;
height: 100%;
line-height: 45px;
float:left;
}
.title-right{
width: 176px;
height: 45px;
text-align: right;
line-height: 45px;
float: left;
}
.content{
width: 100%;
height: 450px;
}
.content-left{
float: left;
width:640px;
height: 100%;
border: 1px solid #E3E3E3;
}
.content-right{
float: left;
width:340px;
height: 100%;
border-right: 1px solid #E3E3E3;
border-bottom: 1px solid #E3E3E3;
}
.content-left-title{
width: 100%;
height: 40px;
padding: 10px;
}
.content-left-body{
width: 640px;
height: 260px;
padding: 5px;
}
.content-left-body ul li{
position: relative;
font-size: 0;
margin-bottom: 10px;
padding-left: 60px;
min-height: 68px;
}
.layim-chat-user {
position: absolute;
left: 3px;
}
.layim-chat-user img {
width: 40px;
height: 40px;
border-radius: 100%;
}
.layim-chat-user cite {
position: absolute;
left: 60px;
top: -2px;
width: 500px;
line-height: 24px;
font-size: 12px;
white-space: nowrap;
color: #999;
text-align: left;
font-style: normal;
}
.layim-chat-user cite i {
padding-left: 15px;
font-style: normal;
}
.layim-chat-user {
display: inline-block;
vertical-align: top;
font-size: 14px;
}
.layim-chat-text {
position: relative;
line-height: 22px;
margin-top: 25px;
padding: 8px 15px;
background-color: #e2e2e2;
border-radius: 3px;
color: #333;
word-break: break-all;
max-width: 462px\9;
}
.layim-chat-text, .layim-chat-user {
display: inline-block;
vertical-align: top;
font-size: 14px;
}
.layim-chat-text:after {
content: '';
position: absolute;
left: -10px;
top: 13px;
width: 0;
height: 0;
border-style: solid dashed dashed;
border-color: #e2e2e2 transparent transparent;
overflow: hidden;
border-width: 10px;
}
.content-left-body ul .layim-chat-mine {
text-align: right;
padding-left: 0;
padding-right: 60px;
}
.layim-chat-mine .layim-chat-user cite {
left: auto;
right: 60px;
text-align: right;
}
.layim-chat-mine .layim-chat-user cite i {
padding-left: 0;
padding-right: 15px;
}
.layim-chat-mine .layim-chat-text {
margin-left: 0;
text-align: left;
background-color: #5FB878;
color: #fff;
}
.layim-chat-mine .layim-chat-text:after {
left: auto;
right: -10px;
border-top-color: #5FB878;
}
.layim-chat-mine .layim-chat-user cite {
left: auto;
right: 60px;
text-align: right;
}
.content-left-input{
width: 640px;
height: 150px;
border-top: 1px solid #E3E3E3;
position:relative;
}
.input-title{
width: 640px;
height: 30px;
padding: 5px;
line-height: 15px;
}
.input-body{
width: 640px;
height: 80px;
}
.input-Send{
width: 640px;
height: 40px;
position:absolute; bottom:0;
}
.chat-view{
overflow-x: hidden; overflow-y: auto;
}
.chat-view{
overflow-x: hidden;
}
ul,li{
list-style: none;
}
textarea{outline:none;resize:none;}
.input-core{
width: 35px;
height: 25px;
margin-left: 5px;
margin-right: 5px;
float: left;
}
/*在线列表css*/
.one-big{
width: 339px; height: 50px;
background: #ffffff;
}
.two-big{
width: 120px;height: 20px; float: left; margin-top: 5px;
}
.three-big{
margin-left: 5px;line-height: 20px;
}
.four-big{
width: 180px;height: 35px; float: right;
}
.five-big{
width: 60px; background: #FF6666; height: 30px; text-align: center; line-height: 15px;color: #ffffff; font-size: 12px;float: right; margin-right: 3px;
}
.five-big-green{
width: 60px; background: #339933; height: 30px; text-align: center; line-height: 15px;color: #ffffff; font-size: 12px;float: right;
}
/*鼠标移动显示关闭按钮*/
.one-big:hover .five-big-close {
display: block;
}
.five-big-close{
width: 60px; background: #ff6700; height: 30px; text-align: center; line-height: 15px;color: #ffffff; font-size: 12px;float: right;margin-left: 3px;display: none;
}
.five-big-one{
width: 60px; background: #339933; height: 30px; text-align: center; line-height: 15px;color: #ffffff; font-size: 12px;
}
.six-big{
margin-left: 10px;
margin-top: 12px;
}
.ur-stitle{
width: 300px;
height: 20px;
border: 1px solid;
}
.layui-layedit {
border: none;
}
/*右上角图标*/
.picon{
color: #ffffff; margin-right: 15px; font-size: 18px;
}
/*.picon:hover{*/
/*cursor: pointer;*/
/*}*/
.fa{
cursor: pointer;
}
.newAddTrip{
display:inline;
}
.mag-top{
margin-top: 5px;
}
</style>
<script>
var layeditIndex;
var layedit;
$(function () {
$( "#draggable" ).draggable();
$( "#sortable" ).sortable({
revert: true
});
$( "#sortableNotMess" ).sortable({
revert: true
});
layui.use('layedit', function(){
layedit = layui.layedit;
layedit.build('demo1'); //建立编辑器
layeditIndex = layedit.build('demo1', {
height: 65, //设置编辑器高度
});
});
})
</script>
</head>
<body>
<%--最外层div--%>
<div id="draggable" class="colPer" style="width: 980px; height: 495px; margin: auto;">
<%--头部导航条--%>
<div class="title">
<%--导航条左半部分--%>
<div class="title-left">
<a style="color: #ffffff" id="state" class="stateTitle">群聊</a>
</div><%--导航条左半部分结束--%>
<%--<div class="title-right">--%>
<%--<div style="">--%>
<%--<span class=" picon fa fa-minus" onclick="narrow()"></span>--%>
<%--<span class=" picon fa fa-times-rectangle" onclick="end()"></span>--%>
<%--</div>--%>
<%--</div>--%>
</div><%--头部导航条结束--%>
<%--主体开始--%>
<div class="content">
<%--左边--%>
<div class="content-left">
<%--预留功能区域--%>
<div class="content-left-title">
<ul id="" style="width: 100%;">
<li class="input-core" id="arr" title="检查链接">
<a class="fa fa-heartbeat" style="font-size: 20px;" id="pot" onclick="checkConnection()"></a>
</li>
<li class="input-core" title="重新连接">
<a class="fa fa-refresh" style="font-size: 20px;" onclick="getConnection()"></a>
</li>
<li class="input-core" title="结束会话">
<a class="fa fa-unlink" style="font-size: 20px;" onclick="closeConnection()"></a>
</li>
<li class="input-core" title="我的位置">
<a class="fa fa-bandcamp" style="font-size: 20px;"></a>
</li>
<li class="" style=" float: right; margin-right: 5px;" title="清空屏幕">
<a class="fa fa-trash" style="font-size: 20px;" onclick="clearConsole()"></a>
</li>
</ul>
</div>
<div class="content-left-body">
<!-- 聊天区 -->
<div class="am-cf admin-main">
<div class="admin-content">
<div class="am-scrollable-vertical" id="chat-view" style="height:100%; width: 100%;">
<ul class="am-comments-list am-comments-list-flip" id="chat">
</ul>
</div>
</div>
</div>
</div>
<div class="content-left-input">
<%--<%–输入区域预留功能–%>--%>
<%--<div class="input-title">--%>
<%--</div>--%>
<div class="input-body">
<textarea id="demo1" style="display: none; border: none;"> </textarea>
</div>
<div class="input-Send">
<div style="width: 300px; height: 80px; margin-right: 15px; float: right;">
<button type="button" class="TableEditor btn btn-default" onclick="sendMessage()" style="float: right; margin-left: 15px; width: 80px; height: 30px; line-height: 15px;background: #DC143C;color: #ffffff">发送</button>
<button type="button" class="TableEditor btn btn-default" onclick="javascript:location.reload();" style="float: right; margin-left: 15px; width: 80px; height: 30px; line-height: 15px">刷新</button>
</div>
</div>
</div>
</div>
<%--右边--%>
<div class="content-right">
<div>
<%--在线列表--%>
<div style="text-align: center; width: 100%;height: 40px;line-height: 40px; background: #E3E3E3;">
<p style="color:#000000;font-size: 14px;">在线列表</p>
</div>
<%--在线人员--%>
<div class=" am-panel-default">
<p>当前在线人数<span id="onlinenum"></span></p>
<ul class="am-list am-list-static am-list-striped" id="sortable">
<li class="one-big">
<div class="two-big">
<span class="three-big">@</span>
</div>
<div class="four-big">
<button type="button" class="TableEditor btn btn-default five-big" onclick="group()">全体成员</button>
<div class="newAddTrip"> <span class="layui-badge">6</span></div>
</div>
</li>
</ul>
</div>
<%--未查看消息--%>
<div class=" am-panel-default">
<p>未查看消息<span id="notMess"></span></p>
<ul class="am-list am-list-static am-list-striped" id="sortableNotMess">
</ul>
</div>
</div>
</div>
</div>
</div><%--最外层结束--%>
</body>
<style>
</style>
<script type="text/javascript">
//私聊按钮
var privateChatButton;
//小绿点
var smallGreenDot;
//新增提示
var newAddTrip;
//群聊 or 单聊
var messWhat = "群聊";
//要关闭的客户名称
var closeUser;
//点击为群聊
function group(){
$(".stateTitle").text("群聊");
messWhat="群聊";
}
$(function () {
context.init({preventDoubleContext: false});
context.settings({compress: true});
context.attach('#chat-view', [
{header: '操作菜单',},
{text: '清理', action: clearConsole},
{divider: true},
{
text: '选项', subMenu: [
{header: '连接选项'},
{text: '检查', action: checkConnection},
{text: '连接', action: getConnection},
{text: '断开', action: closeConnection}
]
},
{
text: '销毁菜单', action: function (e) {
e.preventDefault();
context.destroy('#chat-view');
}
}
]);
});
if("${message}"){
layer.msg('${message}', {
offset: 0
});
}
if("${error}"){
layer.msg('${error}', {
offset: 0,
shift: 6
});
}
var wsServer = null;
var ws = null;
wsServer = "ws://localhost:8080/ChatController/0/${userCode}";
console.log(wsServer);
ws = new WebSocket(wsServer); //创建WebSocket对象
// ws = new SockJS(wsServer); //创建WebSocket对象
//onopen建立链接
ws.onopen = function (evt) {
layer.msg("已经建立连接", { offset: 0});
};
//onmessage发送消息
ws.onmessage = function (evt) {
analysisMessage(evt.data); //解析后台传回的消息,并予以展示
};
//onerror产生异常
ws.onerror = function (evt) {
layer.msg("产生异常", { offset: 0});
};
//关闭连接时
ws.onclose = function (evt) {
layer.msg("已经关闭连接", { offset: 0});
};
/**
* 连接
*/
function getConnection(){
if(ws == null){
ws = new WebSocket(wsServer); //创建WebSocket对象
ws.onopen = function (evt) {
layer.msg("成功建立连接!", { offset: 0});
};
ws.onmessage = function (evt) {
analysisMessage(evt.data); //解析后台传回的消息,并予以展示
};
ws.onerror = function (evt) {
layer.msg("产生异常", { offset: 0});
};
ws.onclose = function (evt) {
layer.msg("已经关闭连接", { offset: 0});
};
}else{
layer.msg("连接已存在!", { offset: 0, shift: 6 });
}
}
/**
* 关闭连接
*/
function closeConnection(){
if(ws != null){
ws.close();
ws = null;
$("#sortable").html(""); //清空在线列表
layer.msg("已经关闭连接", { offset: 0});
}else{
layer.msg("未开启连接", { offset: 0, shift: 6 });
}
}
/**
* 检查连接
*/
function checkConnection(){
if(ws != null){
layer.msg(ws.readyState == 0? "连接异常":"连接正常", { offset: 0});
}else{
layer.msg("连接未开启!", { offset: 0, shift: 6 });
}
}
/**
* 发送信息给后台
*/
function sendMessage(){
$("#demo1").val(layedit.getContent(layeditIndex));
if(ws == null){
layer.msg("连接未开启!",{offset:0, shift: 6});
return false;
}
//获取富文本内容
var message;
var mess = layedit.getContent(layeditIndex);
$("#sendto").text("");
if(messWhat=="群聊"){
message = mess;
}
if(messWhat=="单聊"){
message = mess;
}
var to = messWhat == "群聊"? "": $(".stateTitle").text();
ws.send(JSON.stringify({
message : {
type:1,
content : message,
from : '${userid}',
to : to, //接收人,如果没有则置空,如果有多个接收人则用,分隔
time : getDateFull()
},
type : "message"
}));
layedit.setContent(layeditIndex,"");
}
function analysisMessage(message){
message = JSON.parse(message);
if(message.type == "message"){ //会话消息
showChat(message.message);
}
if(message.type == "notice"){ //提示消息
showNotice(message.message);
}
if(message.list != null && message.list != undefined){ //在线列表
showOnline(message.list,message.notMess);
}
}
/**
* 展示提示信息
*/
function showNotice(notice){
$("#chat").append("<div class='pert'><p class=\"am-text-success\" style=\"text-align:center\"><span class=\"am-icon-bell\"></span> "+notice+"</p></div>");
var chat = $("#chat-view");
// $(".pert").fadeOut(3000);
chat.scrollTop(chat[0].scrollHeight); //让聊天区始终滚动到最下面
$("#onlinenum").text($("#sortable li").length-1);
}
//在线列表
var mychats = [];
//消息列表
var userMessage = [];
/**
* 展示会话信息
*/
function showChat(message){
var thisIndex = message.from;
var thisTitle = $(".stateTitle").text();
var userid = '${userid}';
if(thisIndex!=thisTitle && userid!=message.from){
if(mychats.length==0){
mychats[0] = thisIndex;
}else{
if(mychats.indexOf(thisIndex)<0) {
mychats[mychats.length] = thisIndex;
}
}
}else{
var isSef = '${userid}' == message.from ? "layim-chat-mine" : ""; //如果是自己则显示在右边,他人信息显示在左边
var html = " <li class="+isSef+">\n" +
" <div class=\"layim-chat-user\">\n" +
" <img src=\"${pageContext.request.contextPath}/static/img/touxiang.jpg\">\n" +
" <cite>\n" +
" \n" +
" "+message.time+"<i> "+message.from+"</i>\n" +
" </cite>\n" +
" </div>\n" +
" <div class=\"layim-chat-text\">\n" +
" "+message.content+"\n" +
" </div>\n" +
" </li>";
$("#chat").append(html);
var chat = $("#chat-view");
chat.scrollTop(chat[0].scrollHeight); //让聊天区始终滚动到最下面
}
var to = message.to == null || message.to == ""? "群聊" : message.to; //获取接收人
}
/**
* 展示在线列表
*/
function showOnline(list,notMess){
console.log(notMess)
//在线列表
if(list.length>0){
shenfen = "沟通";
smallGreenDot="";
$("#sortable").html(""); //清空在线列表
$.each(list, function(index, item){
privateChatButton="<button type=\"button\" class=\"TableEditor btn btn-default five-big\" onclick=\"addChat('"+item+"')\">"+shenfen+"</button>";
var li=
" <li class=\"one-big\">\n" +
" <div class=\"two-big\">\n" +
" <span class=\"three-big\">"+item+"</span>\n" +
" </div>\n" +
" <div class=\"four-big\">\n" +
""+privateChatButton+""+
""+smallGreenDot+""+
" <div class=\"newAddTrip\"><span class=\"layui-badge-dot layui-bg-green six-big\"></span>\n</div>\n" +
" </div>\n" +
" </li>";
$("#sortable").append(li);
})
}else{
var li=
" <li class=\"one-big\">\n" +
" <div class=\"two-big\">\n" +
" <span class=\"three-big\">无</span>\n" +
" </div>\n" +
" <div class=\"four-big\">\n" +
" <div class=\"newAddTrip\"></div>\n" +
" </div>\n" +
" </li>";
$("#sortable").append(li);
}
if(notMess.length>0){
$("#sortableNotMess").html(""); //清空未读消息列表
var title = $(".stateTitle").html();
var offline = "回复";
$.each(notMess, function(index, item1){
if(title==item1){
return true;
}else{
var arbutton="<button type=\"button\" class=\"TableEditor btn btn-default five-big-green\" onclick=\"addChat('"+item1.userA+"')\">"+offline+"</button>";
var closeButton = "<button type=\"button\" class=\"TableEditor btn btn-default five-big-close\" onclick=\"closeButton('"+item1.userA+"')\">关闭</button>";
// privateChatButton="<button type=\"button\" class=\"TableEditor btn btn-default five-big\" onclick=\"addChat('"+item+"')\">"+shenfen+"</button>";
var li=
" <li class=\"one-big\">\n" +
" <div class=\"two-big\">\n" +
" <span class=\"three-big endClose \">"+item1.userA+"</span>\n" +
" </div>\n" +
" <div class=\"four-big\">\n" +
""+arbutton+""+
""+closeButton+""+
""+smallGreenDot+""+
" <div class=\"newAddTrip\"><span class=\"layui-badge mag-top\">"+item1.counts+"</span></div>\n" +
" </div>\n" +
" </li>";
$("#sortableNotMess").append(li);
}
})
}else{
$("#sortableNotMess").html(""); //清空未读消息列表
var li=
" <li class=\"one-big\">\n" +
" <div class=\"two-big\">\n" +
" <span class=\"three-big\">无</span>\n" +
" </div>\n" +
" <div class=\"four-big\">\n" +
" <div class=\"newAddTrip\"></div>\n" +
" </div>\n" +
" </li>";
$("#sortableNotMess").append(li);
}
$("#onlinenum").text($("#sortable li").length); //获取在线人数
}
/**
* 添加接收人
*/
function addChat(user){
$(".stateTitle").text("");
var urr;
var sendto = $(".stateTitle");
messWhat="单聊";
var receive = sendto.text() == "群聊" ? "" : sendto.text();
// $(".state").text(user);
clearConsole();
if(receive.indexOf(user) == -1){ //排除重复
urr = receive+user;
sendto.text(urr);
}
for(var i=0;i<mychats.length;i++){
if(mychats[i]==urr){
var index = mychats.indexOf(mychats[i]);
mychats.splice(index);
break;
}
}
}
$(function () {
// $(".five-big-closa").click(function(){
// alert("a")
// $(this).closest('li').remove();
// })
$(document).on("click", ".five-big-close", function () {
<%--更改信息状态--%>
var arr = confirm("确认已查看信息,并删除吗?");
if(arr){
//这里去请求你的后台,删除聊天记录(如果你的聊天记录需要保存)
$(this).closest('li').remove();
}else{
}
});
})
/**
* 清空聊天区
*/
function clearConsole(){
$("#chat").html("");
}
function appendZero(s){return ("00"+ s).substr((s+"").length);} //补0函数
function getDateFull(){
var date = new Date();
var currentdate = date.getFullYear() + "-" + appendZero(date.getMonth() + 1) + "-" + appendZero(date.getDate()) + " " + appendZero(date.getHours()) + ":" + appendZero(date.getMinutes()) + ":" + appendZero(date.getSeconds());
return currentdate;
}
</script>
</html>
3.后台:最核心的部分
//userType 0为服务器端 1为客户端 userCode为用户id
@ServerEndpoint(value = "/ChatController/{userType}/{userCode}", configurator = HttpSessionConfigurator.class)
public class ChatController extends BaseController {
//坑点:自动注入是找不到service的,需要手动在spring.xml中注入,然后引入
private OnlineService onlineService = (OnlineService) ContextLoader.getCurrentWebApplicationContext().getBean("OnlineService");
private ChatService chatService = (ChatService) ContextLoader.getCurrentWebApplicationContext().getBean("ChatService");
private static int onlineCount = 0; //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static CopyOnWriteArraySet<ChatController> webSocketSet = new CopyOnWriteArraySet<ChatController>();
private Session session; //与某个客户端的连接会话,需要通过它来给客户端发送数据
private String userid; //用户名
private HttpSession httpSession; //request的session
private static List list = new ArrayList<>(); //在线列表,记录用户名称
private static List byone = new ArrayList<>(); //状态为 1的用户
private static Map routetab = new HashMap<>(); //用户名和websocket的session绑定的路由表
private static List notMess = new ArrayList(); //定义list 当有客户端传过来消息时 查询数据库,查出所有未查看的消息列表
/**
* 连接建立成功调用的方法
* @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
@OnOpen
public void onOpen(@PathParam(value = "userType")String userType,@PathParam(value = "userCode") String userCode, Session session, EndpointConfig config){
System.out.println("输出session"+session);
PageData pd = new PageData();
this.session = session;
webSocketSet.add(this); //加入set中
this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
this.userid=(String) httpSession.getAttribute("userid"); //获取当前用户
userid=userCode;
// boolean isExist = list.contains(userid);
// System.out.println("输出boolean:"+isExist);
list.add(userid); //将用户名加入在线列表
addOnlineCount(userid); //在线数加1;
routetab.put(userid, session);
//如果此用户是
try {
//查询所有客服暂未查看的信息
List<Chat> list = chatService.selChatNotMess(pd);
//如果有没看的,赋值给notMess
if(list.size()!=0){
notMess.clear();
for(Chat chat1:list){
notMess.add(chat1);
}
}else{
notMess.clear();
}
System.out.println("输出userCode:"+userCode);
if(userType.equals("1")){
System.out.println("是用户登录");
}
//如果是客服登录,将其状态改为已登录
if(userType.equals("0")){
System.out.println("是客服上线");
pd.put("onlineStatus",1);
pd.put("onlineName",userCode);
int is = onlineService.updOnlineStatusTwo(pd);
System.out.println("客服上线:"+is);
}
} catch (Exception e) {
e.printStackTrace();
}
String message = getMessage("[" + userid + "]加入,当前在线人数"+getOnlineCount()+"位", "notice", list,notMess);
broadcast(message); //广播
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(@PathParam(value = "userType")String userType,@PathParam(value = "userCode") String userCode){
webSocketSet.remove(this); //从set中删除
subOnlineCount(userid); //在线数减1
list.remove(userid); //从在线列表移除这个用户
routetab.remove(userid);
String message = getMessage("[" + userid +"]离开了聊天,当前在线人数"+getOnlineCount()+"位", "notice", list,notMess);
PageData pd = new PageData();
//如果是客服下线,则将其状态改为已下线
if(userType.equals("0")){
System.out.println("是客服下线");
pd.put("onlineStatus",0);
pd.put("onlineName",userCode);
try {
int is = onlineService.updOnlineStatusTwo(pd);
} catch (Exception e) {
e.printStackTrace();
}
}
//如果是
if(userType.equals("1")){
notMess.remove(userCode);
}
broadcast(message); //广播
}
/**
* 接收客户端的message,判断是否有接收人而选择进行广播还是指定发送
* @param _message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String _message) {
JSONObject chat = JSON.parseObject(_message);
JSONObject message = JSON.parseObject(chat.get("message").toString());
if(message.get("to") == null || message.get("to").equals("")){ //如果to为空,则广播;如果不为空,则对指定的用户发送消息
broadcast(_message);
}else{
try {
String [] userlist = message.get("to").toString().split(",");
OrderNumUtil orderNumUtil = new OrderNumUtil();
PageData pd = new PageData();
//编号
pd.put("seNum",orderNumUtil.OrderNum());
//消息类型 1 客服发送 2 用户发送
// pd.put("messageType",message.get("type"));
int type =(Integer) message.get("type");
if(type==1){
//关系用户A
pd.put("userA",userlist[0]);
//关系用户B
pd.put("userB",message.get("from"));
}
if(type==2){
//关系用户A
pd.put("userA",message.get("from"));
//关系用户B
pd.put("userB",userlist[0]);
pd.put("onlineName",message.get("from"));
Online online=onlineService.selBooleanRecepiton(pd);
if(online!=null){
}
}
pd.put("messageType",message.get("type"));
//发送者者id
pd.put("userId",message.get("from"));
//接受者id
pd.put("friendId",userlist[0]);
//消息内容
pd.put("messageContent",message.get("content"));
//消息种类
pd.put("messageKind",1);
//消息发送时间
pd.put("sendTime",message.get("time"));
//消息状态
pd.put("status",1);
int is = chatService.insChat(pd);
singleSend(_message, (Session) routetab.get(message.get("from"))); //发送给自己,这个别忘了
for(String user : userlist){
if(!user.equals(message.get("from"))){
singleSend(_message, (Session) routetab.get(user)); //分别发送给每个指定用户
}
}
//查询所有客服暂未查看的信息
List<Chat> list = chatService.selChatNotMess(pd);
//如果有没看的,赋值给notMess
if(list.size()!=0){
notMess.clear();
for(Chat chat1:list){
notMess.add(chat1);
}
}else{
notMess.clear();
}
} catch (Exception e) {
e.printStackTrace();
}
String messages = getMessage("用户:[ "+ message.get("from")+" ]发送了一条信息,请注意查看", "notice", list,notMess);
broadcast(messages); //广播
}
}
/**
* 发生错误时调用
* @param error
*/
@OnError
public void one rror(Throwable error){
error.printStackTrace();
}
/**
* 广播消息
* @param message
*/
public void broadcast(String message){
for(ChatController chat: webSocketSet){
try {
chat.session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
continue;
}
}
}
/**
* 对特定用户发送消息
* @param message
* @param session
*/
public void singleSend(String message, Session session){
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 组装返回给前台的消息
* @param message 交互信息
* @param type 信息类型
* @param list 在线列表
* @param notMess 客服未读消息列表
* @return
*/
public String getMessage(String message, String type, List list,List notMess){
JSONObject member = new JSONObject();
member.put("message", message);
member.put("type", type);
member.put("list", list);
member.put("notMess",notMess);
return member.toString();
}
public int getOnlineCount() {
return onlineCount;
}
public void addOnlineCount(String userid) {
ChatController.onlineCount++;
PageData pd = new PageData();
try {
pd.put("onlineId",userid);
pd.put("onlineName","bb");
pd.put("onlineYs","cc");
pd.put("onlineRreply","dd");
// int is = onlineService.insOnline(pd);
} catch (Exception e) {
e.printStackTrace();
}
}
public void subOnlineCount(String userid) {
ChatController.onlineCount--;
PageData pd = new PageData();
pd.put("onlineId",userid);
try {
// int is = onlineService.delOnline(pd);
} catch (Exception e) {
e.printStackTrace();
}
}