人工客服聊天(WebSocket)
技术:
前端: vue、axios
后端:tomcat、servlet、redis数据库
WebSocket介绍:
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。与传统的 HTTP 请求-响应模型不同,WebSocket 允许服务器和客户端之间进行实时的、低延迟的双向数据传输。
使用场景:
- 实时聊天应用:如客服聊天、社交聊天。
- 实时通知和提醒:如推送通知、股票价格更新。
- 协作工具:如在线文档编辑、多人游戏。
- 实时数据流:如直播、在线游戏、物联网数据传输。
前端实现 WebSocket:
前端代码(用户端、客服端):
let app = new Vue({
el:'#app',
data:{
content:'',//用户发的消息
chatMessage:[], //聊天内容
protocol:'', //当前页面的协议
host:'', //服务器的主机名或 IP 地址主机和端口
endpoint:'', //端点
contextPath:'', //项目名
},
methods:{
//WebSocket 客服聊天 初始化参数 建立连接
initWebSocket() {
// 获取当前页面的协议、主机和端口
//const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const protocol = this.protocol;
//const host = window.location.host;
const host = this.host;
//考虑nginx反向代理没有端口的问题
//const host = window.location.host.includes(':') ? window.location.host : window.location.host + ':8080';
//const endpoint = '/shop_war/echo';
/* ******客服端连接用户的端点user 用户连接客服端的端点admin ********** */
/* **** 当前为客服端 用户端的话把user改为admin ***** */
const endpoint = this.endpoint+ this.contextPath +'/user';
//const endpoint = '/shop/echo';
// 动态生成 WebSocket 的 URL
// ws://localhost:8080/user
const wsUrl = `${protocol}://${host}:${endpoint}`;
// 创建 WebSocket 连接
var ws = new WebSocket(wsUrl);
ws.onopen = function () {
console.log('WebSocket 连接已经建立。');
// ws.send('Hello, server!');
};
// ***** 接收到websocket服务器端点的通知
ws.onmessage = function (event) {
console.log('收到服务器消息:', event.data);
//该刷新了
if (event.data){
// alert("获取到了服务器的消息"); 刷新数据 即获取客服端的消息
// ***** 从redis数据库中去读取聊天内容
app.get().then(result=>{
// alert("获取服务端的消息"+result.data.obj);
// 确保 this.chatMessage 已经是一个数组
if (!Array.isArray(this.chatMessage)) {
app.$data.chatMessage = [];
}
let obj = result.data.obj;//获取到聊天内容
obj.forEach(product => {
app.$data.chatMessage.push(product);
});
})
}
};
ws.onerror = function (event) {
console.error('WebSocket 连接出现错误:', event);
};
ws.onclose = function () {
console.log('WebSocket 连接已经关闭。');
};
//关闭窗口调用
window.onbeforeunload = function() {
app.$data.ws.close();
};
this.ws = ws;
},
//获取 要建立连接的 初始化参数
init(){
//用于初始化websocket
axios.post("websocket.action?op=getServerInfo").then(result=>{
if (result.data.code==0){
}else{
this.protocol = result.data.obj.protocol;
this.host = result.data.obj.host;
this.endpoint = result.data.obj.port;
this.contextPath = result.data.obj.contextPath;
//动态初始化websocket的url,并建立连接,用来提醒该刷新消息了
this.initWebSocket();
}
})
},
//发送 消息 到 服务器
send(){
let params = new URLSearchParams( );
if (this.content == null || this.content == ''){
alert("请先输入内容");
return;
}
params.append("content",this.content);
params.append("name","admin");
axios.post("websocket.action?op=setMessage",params).then(result=>{
let jm = result.data;
if (jm.code == 1){
this.content='';
this.get().then(result=>{
app.$data.chatMessage = [];
if (!Array.isArray(this.chatMessage)) {
app.$data.chatMessage = [];
}
let obj = result.data.obj;
obj.forEach(product => {
app.$data.chatMessage.push(product);
});
})
}
})
},
//获取 服务端的消息 后调用
get() {
let params = new URLSearchParams();
return axios.post("websocket.action?op=getMessage", params)
.then(response => {
// 假设服务器返回的数据结构如下:
// { data: { code: 0, obj: {...} } }
return response;
})
.catch(error => {
console.error('获取数据时发生错误:', error);
throw error;
});
},
//初始内容并渲染 即获取聊天记录 原先的聊天内容
initContent(){
this.get().then(result=>{
app.$data.chatMessage = [];
if (!Array.isArray(this.chatMessage)) {
app.$data.chatMessage = [];
}
let obj = result.data.obj;
obj.forEach(product => {
app.$data.chatMessage.push(product);
});
})
},
},
//选择挂载点为mounted vue生命周期调用
mounted:function (){
axios.all( [ this.init(),this.initContent() ] );
},
})
后端
WebSocket 服务器端点(客服端、用户端连接类):
基本的 WebSocket 生命周期事件处理(连接、消息接收、关闭、错误)以及一个方法来向客户端发送消息。
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
// ***** 客服端 的 端点 用户端的话 把 admin改成user
@ServerEndpoint("/admin")
public class adminEchoServer {
private static Session session;
@OnOpen
public void onOpen(Session session) {
System.out.println("WebSocket 连接已经建立。");
adminEchoServer.session = session;
}
@OnMessage
public void onMessage(String message, Session session) throws IOException {
System.out.println("收到客户端消息:" + message);
//session.getBasicRemote().sendText("服务器收到消息:" + message);
}
@OnClose
public void onClose() {
System.out.println("WebSocket 连接已经关闭。");
}
@OnError
public void onError(Throwable t) {
System.out.println("WebSocket 连接出现错误:" + t.getMessage());
}
// ****** 发送 通知 告诉前端需要刷新了 即前端需要去redis数据库中读取消息了
public void send(String message) throws IOException {
session.getBasicRemote().sendText(message);
}
}
聊天内容具体操作类:
import com.yc.bean.WebSocket;
import com.yc.commons.RedisHelper;
import com.yc.utils.JsonModel;
import com.yc.utils.adminEchoServer;
import com.yc.utils.userEchoServer;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
@WebServlet(value = "/websocket.action")
public class WebSocketServlet extends BaseServlet{
获取初始化信息 建立连接
protected void getServerInfo(HttpServletRequest request, HttpServletResponse response) throws IOException {
Map<String, String> serverInfo = new HashMap<>();
String protocol = request.isSecure() ? "wss" : "ws";
String host = request.getServerName();
int port = request.getServerPort();
String contextPath = request.getContextPath();
serverInfo.put("protocol", protocol);
serverInfo.put("host", host);
serverInfo.put("port", String.valueOf(port));
serverInfo.put("contextPath", contextPath);
JsonModel jm = new JsonModel();
jm.setCode(1);
jm.setObj(serverInfo);
writeJson(jm,response);
}
// 发送消息 存到 redis数据库中
public void setMessage(HttpServletRequest request, HttpServletResponse response) throws IOException {
JsonModel jm = new JsonModel();
// name 表示 用户/客服 user/admin
String name = request.getParameter("name");
String content = request.getParameter("content");
//连接redis
Jedis jedis = new Jedis("localhost", 6379);
// 当前时间戳
long currentTimestamp = System.currentTimeMillis() / 1000;
// **** 存 redis 数据库
// ****添加新成员到有序集合Sorted Set 格式: content_websocket 时间戳 用户姓名:内容
jedis.zadd("content_websocket", currentTimestamp, name+":"+content);
jm.setCode(1);
jm.setObj("发送成功");
super.writeJson(jm,response);
//**** 通知 用户端 或者 客服端 刷新消息了
if ("user".equals(name)){
userEchoServer userEchoServer = new userEchoServer();
userEchoServer.send("我是用户端,你好");
}else {
adminEchoServer adminEchoServer = new adminEchoServer();
adminEchoServer.send("我是客服端,你好");
}
}
//从redis数据库 中 获取聊天内容
protected void getMessage(HttpServletRequest request, HttpServletResponse response) throws IOException {
JsonModel jm = new JsonModel();
//连接redis
Jedis jedis = new Jedis("localhost", 6379);
// 获取有序集合的全部成员
Set<Tuple> sortedSetMembers = jedis.zrangeWithScores("content_websocket",0,-1);
// 打印输出每个成员及其分数
List<WebSocket> list = new ArrayList<>();
for (Tuple member : sortedSetMembers) {
String memberValue = member.getElement(); // 内容
Long score = (long) member.getScore(); // 时间戳
String[] parts = memberValue.split(":");
WebSocket webSocket = new WebSocket();
if (parts[0]==null || parts[1]==null){
continue;
}
webSocket.setName(parts[0]);//姓名
webSocket.setContent(parts[1]);//内容
Date date = new Date(score*1000);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String timestamp = sdf.format(date);
webSocket.setTimestamp(timestamp);//时间
list.add(webSocket);
}
jm.setCode(1);
jm.setObj(list);
super.writeJson(jm,response);
}
}
前后端交互工具类:
import com.google.gson.Gson;
import com.yc.utils.JsonModel;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
public abstract class BaseServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String op = req.getParameter("op"); //op=regFile
JsonModel jm = new JsonModel(); //用来保存要运行后的信息 并 返回到前端
try {
if (op == null || "".equals(op)) {
// out.println( "{code:0,error:'没有op参数'}" );
jm.setCode(0);
jm.setError("op参数不能为空..");
writeJson(jm,resp);
return;
}
/// 反 射
Method[] methods = this.getClass().getDeclaredMethods();//取子类中的方法
for (Method m:methods){
if ( m.getName().equals( op ) ) { // 判断有没有 regFile方法
m.invoke(this, req, resp);//激活对应函数 regFile
}
}
} catch (Exception e) {
e.printStackTrace();
jm.setCode(0);
jm.setError( e.getMessage() );
writeJson(jm,resp);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setCharacterEncoding("utf-8"); // 响应流的编码
resp.setContentType("text/html;charset=utf-8");
super.service(req, resp);
}
//*** 后端传数据到前端 关键地方 *** 以json格式传数据到前端
protected void writeJson( JsonModel jm , HttpServletResponse resp) throws IOException {
resp.setContentType("text/json;charset=utf-8");
PrintWriter out = resp.getWriter();
Gson g = new Gson();
out.println( g.toJson( jm )); ///后端 把 运行情况 以json类型传出到前端
out.flush();
out.close();
}
protected void writeObj(Object obj, HttpServletResponse resp) throws IOException {
resp.setContentType("text/json;charset=utf-8");
PrintWriter out =resp.getWriter();
//创建一个Gson对象 g,
Gson g = new Gson();
// 用于将Java对象转换为JSON格式的字符串
out.print(g.toJson( obj ));
out.flush();
out.close();
}
}
聊天内容封装类:
import lombok.Data;
@Data
public class WebSocket {
private String name; //判断是管理员还是用户 admin/user
private String timestamp;//时间戳
private String content;//聊天内容
}
前后端交互封装类:
import lombok.Data;
import java.io.Serializable;
@Data
public class JsonModel implements Serializable {
private Integer code; //响应码 : 0:表示失败 1:表示成功
private Object obj;
private String error;
}
源码链接:https://gitee.com/xinqiuuu/websocket.git