本篇文章講述如何將WebSocket
群聊模式改造為單聊模式,並詳細講解該功能的實現過程。
本Demo
是一個商城小項目,將單聊應用在了前台用戶與后台客服的在線聊天,此模式為單聊模式,代碼都是在我之前寫的websocket
群聊代碼的基礎之上改寫的,可參考之前的文章。
一,思路梳理
1,首先思考群聊的實現方式。
每當一個用戶使用websocket建立連接時,都會存放一個連接對象(在connectMap集合存放,鍵為sessionId,值為該連接對象),每次當用戶發送一條數據時,都會遍歷connectMap中所有的連接對象,然后進行廣播發送消息,接收消息也是,誰的消息都收,來之不拒,所以此為群聊。
2,再來舉一反三,思考單聊的實現方式。
單聊的話,要改變下面幾個地方。
1,存放connectMap連接對象時,值要為userId(唯一標識)。
2,使用websocket建立連接時要把自己的userId(發送人)和對方的userId(接收人)傳到后台,因為發消息時,根據userId得到連接對象,並使用連接對象發消息到對方的窗口和自己的窗口。
3,在js獲取消息的地方要判斷該消息的發送人userId是否和初始化時的接收人userId一致,如果一致的話則將消息添加到自己的窗口進行顯示,如果不一致則不顯示。
有一個地方需要提一下,比如要給A用戶發送消息,就要根據A的userId
得到連接對象,然后就會把消息發送給A用戶。
二,代碼分析
1,首先是用戶聊天框的js
代碼(html
忽略)
此為用戶點擊在線客服按鈕進入聊天框頁面就執行的js
代碼。
說明:下面接收人id寫死了為超級管理員
,我這里省事情了,因為超級管理員的賬號(也就是唯一標識)就是超級管理員
這五個字,當真正開發時就要動態獲取唯一標識了!
<script type="text/javascript">
//1,獲取連接,new WebSocket()
//獲取到路徑url的參數(用戶的手機號,這里將手機號作為了用戶的唯一標識)
var accounta=window.location.search;
var account=accounta.slice(9);
//發送人id
var sb=account;
//將發送人id參數傳給服務端
var wsUrl="ws://127.0.0.1:8082/charRoomServer";
var allUrl=wsUrl+"/"+sb;
//接收人id
var jsrid='超級管理員';
//客戶端與服務端建立連接,建立連接以后,它會發出一個ws.open事件
var ws=new WebSocket(allUrl);
//連接成功后就將接收人id發送給服務端
ws.onopen=function(){
ws.send(jsrid);
}
//客戶端收到服務端發送的消息
ws.onmessage=function(message){
var shenfen=JSON.stringify(message.data).toString();
var shenfens=shenfen.slice(shenfen.lastIndexOf("|")+1,-1);
//這里通過很low的方式拿到了發送人id [shenfens](后台拼接字符串並前台截取得到)
//判斷發送人id是否和頁面初始化時的接收人id(jsrid)是否一致,相同則說明是對方回復的消息,並將該消息添加到自己的聊天框;如果收到的發送人id與“大白機器人”或者是和自己的id一致,也要將消息添加到自己的聊天框(自己發的消息嘛)
if(jsrid==shenfens||shenfens=="大白機器人:"||shenfens==sb){
//獲取以后,在客戶端顯示
messages.innerHTML+=message.data;
}else{
//不做任何操作
}
}
//獲取某個用戶輸入的聊天內容,並發送到服務端
function getMessage(){
var inputMessage=document.getElementById("inputMessage").value;
//alert(inputMessage);
//獲取消息以后,要發送給服務端,然后廣播給所有用戶
if(typeof(inputMessage)=='undefined'){
alert("請輸入您要發送的消息!");
}else{
ws.send(inputMessage);
//輸入框消息清空
inputMessage.value="";
}
}
//當關閉頁面或者用戶退出時,會執行一個ws.close()方法
window.onbeforeunload=function(){
ws.close();
}
//按回車發送信息
document.onkeyup=function(e){
if(e.keyCode==13){
getMessage();
inputMessage.value="";
}
}
</script>
2,其次為后台系統客服聊天框的js
代碼
<script>
var name="";
$(function () {
$.post(
"/demo/getName",
function (data) {
name=data.name;
},
"json"
);
//得到未讀消息並展示到列表中(當點擊其中一個未讀消息時會得到該消息的賬號(前台用戶的userId唯一標識))
$.post(
"/demo/getweidu",
function (data) {
var temp='';
var count=0;
var account='';
for(var i=0;i<data.length;i++){
temp+='<div class="yonghu" onclick="liaotian('+data[i].account+')">\n' +
' <span id="lalala">'+data[i].account+'</span>\n' +
' <div id="'+data[i].account+'" class="weidu">'+data[i].count+'</div>\n' +
' </div>';
}
$("#nav").append(temp);
}
);
});
//這是點擊對應的未讀消息之后將未讀消息展示到客服的聊天框
var sb=null;
function liaotian(v) { //下面的websocket內容在這個方法里面,執行這個點擊事件之后觸發websocket連接
sb=v;
$("#content").empty();
$("#"+v).hide();
$.post(
"/demo/getxiaoxi",
{account:v},
function (data) {
// alert(JSON.stringify(data));
var str='';
for(var i=0;i<data.length;i++){
var time=data[i].addtime.slice(0,10);
str+=' <div class="message"><span>'+data[i].account+'</span>用戶:<span>'+time+'</span>'+data[i].message+'</div>';
}
$("#content").append(str);
},
"json"
);
//將點擊后的未讀消息改為已讀消息
$.post(
"/demo/changeYD",
{account:v},
function (data) {
console.log(data);
}
);
//發送人id(超級管理員寫死的,上面說過了)
var wsUrl="ws://127.0.0.1:8082/charRoomServer";
var allUrl=wsUrl+"/"+"超級管理員";
//客戶端與服務端建立連接,建立連接以后,它會發出一個ws.open事件
var ws=new WebSocket(allUrl);
//接收人userId
var jsrid=sb;
//連接成功后,提示瀏覽器客戶端輸入名稱
ws.onopen=function(){
ws.send(jsrid);
}
//客戶端收到服務端發送的消息
ws.onmessage=function(message){
//截取到普通用戶的手機號
var shenfen=JSON.stringify(message.data).toString();
var shenfens=shenfen.slice(shenfen.lastIndexOf("|")+1,-1);
console.log("身份:"+shenfens);
// //判斷發送人id是否和頁面初始化時的接收人id(jsrid)是否一致,相同則說明是對方回復的消息
if(jsrid==shenfens||"超級管理員"==shenfens){
//獲取以后,在客戶端顯示
content.innerHTML+=message.data;
}else{
//不做任何操作
}
}
//獲取某個用戶輸入的聊天內容,並發送到服務端
function getMessage(){
var inputMessage=document.getElementById("inputMessage").value;
//alert(inputMessage);
//獲取消息以后,要發送給服務端,然后廣播給所有用戶
if(typeof(inputMessage)=='undefined'){
alert("請輸入您要發送的消息!");
}else{
ws.send(inputMessage);
//輸入框消息清空
inputMessage.value="";
}
}
//當關閉頁面或者用戶退出時,會執行一個ws.close()方法
window.onbeforeunload=function(){
ws.close();
}
//按回車發送信息
document.onkeyup=function(e){
if(e.keyCode==13){
getMessage();
inputMessage.value="";
}
}
}
</script>
3,最后為服務端的websocket
實例
package com.qianlong.controller;
import com.qianlong.service.SelectService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
/**
* 聊天室的服務端程序
* @author Administrator
*
*/
//聲明websocket某個服務端的地址
@ServerEndpoint(value = "/charRoomServer/{param}")
@Component
public class ChatRoomServer {
@Autowired
public static SelectService selectService;
private boolean firstFlag=true;
private Session session;
private String userName;
//發送人id
private String userId;
//key代表此次客戶端的userId,value代表此次連接對象
private static final HashMap<String, Object> connectMap=new HashMap<String, Object>();
//保存所有用戶昵稱信息
//key是session的id,value是用戶昵稱
private static final HashMap<String, String> userMap=new HashMap<String, String>();
//服務端收到客戶端的連接請求,連接成功后會執行此方法
@OnOpen
public void start(@PathParam(value = "param") String param, Session session) {
this.session=session;
this.userId=param; //接收參數
connectMap.put(param,this);
}
//客戶端發來的信息,服務端接收
@OnMessage //接收人userId
public void chat(String clientMessage,Session session) {
//firstFlag為true是第一次進入,第二次進入之后設為false
ChatRoomServer client=null;
if(firstFlag) {
this.userName=clientMessage;
//將新進來的用戶保存到用戶map
userMap.put(session.getId(), userName);
try {
if("超級管理員".equals(userId)){
}else{
//構造發送給客戶端的提示信息
String message=htmlMessage("大白機器人:","親愛的"+userId+",您想了解點兒啥?");
client=(ChatRoomServer) connectMap.get(userId);
//給對應的web端發送一個文本信息
client.session.getBasicRemote().sendText(message);
}
} catch (IOException e) {
e.printStackTrace();
}
firstFlag=false;
}else {
System.err.println("clientMessage:"+userName);
//給對方發消息
String message1=htmlMessage(userId,clientMessage);
client = (ChatRoomServer) connectMap.get(userName);
if(client!=null){
try {
client.session.getBasicRemote().sendText(message1);
} catch (IOException e) {
e.printStackTrace();
}
}
//給自己窗口發消息
String message2=htmlMessage(userId,clientMessage);
client = (ChatRoomServer) connectMap.get(userId);
try {
client.session.getBasicRemote().sendText(message2);
} catch (IOException e) {
e.printStackTrace();
}
//這是將前台用戶發送的消息存數據庫並標記為未讀,和上面通信沒關系
if("超級管理員".equals(userId)){
}else{
Map map=new HashMap();
map.put("account",userId);
map.put("message",clientMessage);
map.put("addtime",new Date());
int i = selectService.chatInsert(map);
System.out.println(i);
}
}
}
/**
* 前台js的ws.close事件,會觸發后台的標注onClose的方法
*/
@OnClose
public void close() {
userMap.remove(session.getId());
connectMap.remove(userId);
}
/**
* 渲染頁面,把信息構造好標簽再發送
*/
public String htmlMessage(String userName,String message) {
StringBuffer stringBuffer=new StringBuffer();
SimpleDateFormat sf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
stringBuffer.append("<article>");
stringBuffer.append("<span>"+sf.format(new Date())+"</span>");
stringBuffer.append("<div class='avatar'>");
stringBuffer.append("<h3>"+userName+"</h3>");
stringBuffer.append("</div>");
stringBuffer.append("<div class='msg'>");
stringBuffer.append("<div class='tri'></div>");
stringBuffer.append("<div class='msg_inner'>"+message+"</div>");
stringBuffer.append("</div>");
stringBuffer.append("</article>");
//這里拼接了消息發送人的userId,在前台進行截取字符串接收發送人的userId
stringBuffer.append("|"+userName);
return stringBuffer.toString();
}
}
三,依賴注入
在websocket
中進行依賴注入service
並調用service
方法進行數據庫存儲,如果按常規的方式是走不通的。
解決方式:
在該springboot
項目中添加一個WebsocketConfig
配置類,對service
進行配置。
@Configuration
public class WebSocketConfig {
/**
* ServerEndpointExporter 用於掃描和注冊所有攜帶 ServerEndPoint 注解的實例,
* 若部署到外部容器 則無需提供此類。
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
/**
* 因 SpringBoot WebSocket 對每個客戶端連接都會創建一個 WebSocketServer(@ServerEndpoint 注解對應的對象,Bean 注入操作會被直接略過,因而手動注入一個全局變量
*/
@Autowired
public void setSelectService(SelectService selectService) {
ChatRoomServer.selectService = selectService;
}
}
然后在websocket
中注入service
。
四,效果展示
在前台商城登錄用戶18838030468賬號
點擊聯系客服
進到聊天框中(左下角的用戶名忽略,因為上面通過截取字符串得到的發送人userId,所以這里多了一個userId)
然后發送消息給客服
然后退出18838030468賬號並切換用戶13333333333賬號進行登錄,然后點擊客服,進入聊天頁面
然后發送消息給客服
這個13333333333賬號先不要退,直接登錄后台超級管理員賬號
登錄之后會提示未讀消息的信息。
點擊客服管理,進入客服聊天平台,並顯示消息未讀數。
點擊18838030468未讀消息,查看消息並進行回復。(這是給18838030468用戶發送的消息,其他用戶是接收不到的,正規開發中,要把客服發送的消息存到緩存或數據庫中,然后再在前台用戶聊天框中進行顯示聊天記錄即可,這里忽略不做)
然后再點擊13333333333未讀消息進行查看,並回復消息。
這是給13333333333用戶發送的消息,其他用戶也是收不到的。
查看13333333333用戶的聊天框內容,可以收到客服的消息,然后該用戶可以和客服實現在線單聊。
本次websocket
講解到此結束,謝謝觀賞!