webScoket离线消息暂存,上线发送
webScoket离线消息暂存,上线发送
用webScoket的即时聊天通讯,功能可群发单发,可对不在线用户发送消息时用户一上线立马就能收到消息,也可以查看未读数量
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>1.3.5.RELEASE</version>
</dependency>
添加websocket配置类
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
注入ServerEndpointExporter,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint。要注意,如果使用独立的servlet容器,而不是直接使用springboot的内置容器,就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理。
后端代码
package com.zl.socket;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import com.Utils.WebSocketMapUtil;
/**
* 即时通讯
* @author 史**
*
* 2018年9月17日
*/
@Component
@ServerEndpoint(value = "/websocket")
public class MyWebSocket {
private volatile static List<Session> sessions = Collections.synchronizedList(new ArrayList());
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
//设置为静态的 公用一个消息map ConcurrentMap为线程安全的map HashMap不安全
private static ConcurrentMap<String, Map<String, List<Object>>> messageMap=new ConcurrentHashMap<>();
/**
* /**
* 连接建立成功调用的方法
* @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
* @param userId 用户id
* @throws Exception
*/
@OnOpen
public void onOpen(Session session) throws Exception{
System.out.println("开始");
this.session = session;
// sessions.add((Session) this);
String key="";//当前用户id
String objectUserId="";//对象id
if("".equals(session.getQueryString()) || session.getQueryString()==null) {
key= "";
}else {
String keString=session.getQueryString();
if(keString.length()>1) {
key=keString.split(",")[0];
objectUserId=keString.split(",")[1];
}else {
key=session.getQueryString();
}
}
WebSocketMapUtil.put(key,this);
if(messageMap.get(key)!=null) {
//说明在用户没有登录的时候有人给用户发送消息
//该用户所有未收的消息
Map<String, List<Object>> lists=messageMap.get(key);
//对象用户发送的离线消息
List<Object> list= lists.get(objectUserId);
if(list!=null) {
for(int i=0;i<list.size();i++) {
//封装消息类型 消息内容+"["+发送消息的人+";"+接收消息的人","+0
String message=list.get(i)+"["+objectUserId+";"+key+","+0;
onMessage(message);
}
}
// map中key(键)的迭代器对象
//用户接收完消息后删除 避免下次继续发送
Iterator iterator = lists.keySet().iterator();
while (iterator.hasNext()) {// 循环取键值进行判断
String keys = (String) iterator.next();//键
if (objectUserId.equals(keys)) {
iterator.remove(); // 移除map中以a字符开头的键对应的键值对
// messageMap.remove(key);
}
}
}
}
/**
* 连接关闭调用的方法
* @throws Exception
*/
@OnClose
public void onClose() throws Exception{
//从map中删除
System.out.println(session.getQueryString().split(",")[0]);
WebSocketMapUtil.remove(session.getQueryString().split(",")[0]);
System.out.println("关闭");
}
/**
* 收到客户端消息后调用的方法 单发
* @param message 客户端发送过来的消息
* @param session 可选的参数
* @param tit 0 单发 1 群发
* @throws IOException
*/
@OnMessage
public void onMessage(String message){
String tit=message.substring(message.lastIndexOf(",")+1, message.length());
System.out.println("收到");
String userId=message.substring(message.lastIndexOf(";")+1,message.lastIndexOf(","));//接收消息的用户
System.out.println("发给:"+userId);
String sendUserId=message.substring(message.lastIndexOf("[")+1,message.lastIndexOf(";"));//发送消息的用户
System.err.println("发消息的用户:"+sendUserId);
message=message.substring(0, message.lastIndexOf("["));//发送的消息
System.err.println("客户端发来的信息:"+message);
try {
MyWebSocketController myWebSocket= ((MyWebSocketController) WebSocketMapUtil.get(userId));
if(myWebSocket != null){
if("0".equals(tit)) {
//单发
myWebSocket.sendMessage(message);
}else if ("1".equals(tit)) {
//调用群发方法
myWebSocket.sendMessageAll(message);
}
}else {
//不在线
System.out.println("不在线");
if(messageMap.get(userId)==null) {
//用户不在线时 第一次给他发消息
Map<String, List<Object>> maps=new HashMap<>();//该用户的所有消息
List<Object> list=new ArrayList<>();//该用户发的离线消息的集合
list.add(message);
maps.put(sendUserId, list);
messageMap.put(userId, maps );
}else {
//不在线再次发送消息
//给用户的所有消息
Map<String,List<Object>> listObject=messageMap.get(userId);
List<Object> objects=new ArrayList<>();
if(listObject.get(sendUserId)!=null) {//这个用户给收消息的这个用户发过消息
//此用户给该用户发送过离线消息(此用户给该用户发过的所有消息)
objects=listObject.get(sendUserId);
objects.add(message);//加上这次发送的消息
//maps.put(sendUserId, objects);
//替换原来的map
listObject.put(sendUserId, objects);
}else {//这个用户没给该用户发送过离线消息
objects.add(message);
listObject.put(sendUserId, objects);
}
//Map<String, List<Object>> map=new HashMap<>();
// for(Map<String, List<Object>> map : listObject) {//遍历该用户的所有未发送的集合
// if(map.get(sendUserId).size()>0) {//这个用户给他发送过离线消息
// objects=map.get(sendUserId);
// objects.add(message);
// }
// }
//maps.put(sendUserId, message);
messageMap.put(userId, listObject );
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 发生错误时调用
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error){
System.out.println("错误");
error.printStackTrace();
}
/**
* 发送消息方法。
* @param message
* @throws IOException
*/
public void sendMessage(String message) throws IOException{
// System.out.println("发消息");
this.session.getBasicRemote().sendText(message);
}
/**
* 群发消息方法。
* @param message
* @throws IOException
*/
public void sendMessageAll(String message) throws IOException{
System.out.println("群发");
for(MyWebSocketController myWebSocket : WebSocketMapUtil.getValues()){
myWebSocket.sendMessage(message);
}
}
/**
* 获取该用户未读的消息数量
* @param userId 当前用户id
* @param objectUserId 对象id
* @return
*/
public int getMessageCount(String userId,String objectUserId) {
//获取该用户所有未收的消息
Map<String, List<Object>> listMap=messageMap.get(userId);
if(listMap != null) {
List<Object> list=listMap.get(objectUserId);
if(list!=null) {
return listMap.get(objectUserId).size();
}else {
return 0;
}
}else {
return 0;
}
}
}
这里是需要用到的一个工具类
package com.Utils;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import com.caesar.controller.MyWebSocketController;
/**
* 工具类 用来存放删除获取用户
* @author 史**
*
* 2018年9月17日
*/
public class WebSocketMapUtil {
public static ConcurrentMap<String, MyWebSocketController> webSocketMap = new ConcurrentHashMap<String, MyWebSocketController>();
public static void put(String key, MyWebSocketController myWebSocket){
webSocketMap.put(key, myWebSocket);
}
public static MyWebSocketController get(String key){
return webSocketMap.get(key);
}
public static void remove(String key){
webSocketMap.remove(key);
}
public static Collection<MyWebSocketController> getValues(){
return webSocketMap.values();
}
}
前端代码
小程序的前台调用代码,html的代码调用也类似
// pages/message/chat/chat.js
//获取应用实例
const app = getApp();
/*路径 */
var apiURL = require('../../../apiURL');
Page({
/**
* 页面的初始数据
*/
data: {
receiveUserId:'',//接收消息的用户id
chatRecord:[],//聊天记录
objectuser:[],//聊天对象信息
userInfo:[],//用户信息
chatListId:'',//聊天室id
status:'',//聊天室转态
value:'',
height:'',//设备高度
scrollTop:0
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
console.log(options)
var _this = this;
// _this.getChatRecord();
//将json字符串转为json对象
//对象信息
var objectuser=JSON.parse(options.objectuser);
//聊天室id
var chatListId = options.chatListId;
if(chatListId == ''){//聊天室id为空说明不是从聊天室列表过来的 所以查询和该用户是否有聊天室
_this.isChatList(options.objectuserid, app.globalData.userId);
}else{
//查聊天室的聊天记录
_this.getChatRecord(chatListId, app.globalData.userId);
_this.setData({
chatListId: chatListId,
})
}
//查用户信息
_this.getUserInfo(app.globalData.userId);
_this.data.receiveUserId = options.objectuserid;
_this.setData({
objectuser: objectuser,
})
if (!app.globalData.communication) {
//通讯没有连接 向后台发起连接
wx.connectSocket({//传入对象id 当前用户id和对象用户id
url: apiURL.wxLinkMessage.linkMessages + app.globalData.userId + "," + options.objectuserid,
header: {
'content-type': 'application/json'
},
method: "GET"
})
}
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
//连接成功
wx.onSocketOpen(function () {
console.log('连接成功');
app.globalData.communication = true
})
//连接失败
wx.onSocketError(function (res) {
app.globalData.communication = false
console.log('WebSocket连接打开失败,请检查!')
})
//接收
wx.onSocketMessage(function (res) {
console.log("接收服务器发过来的消息")
console.log(res)
})
wx.onSocketClose(function (res) {
console.log('WebSocket 已关闭!')
})
},
/**
* 生命周期函数--监听页面显示
*/
onShow: function () {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide: function () {
// console.log("++++++++++++++++++++++++++++++")
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload: function () {
app.globalData.communication=false;
//关闭当前连接
wx.closeSocket()
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage: function () {
},
sendMessage: function (e) {
var _this=this;
// console.log(e.detail.value.message)
var message = e.detail.value.message
if (_this.Trim(message) == null || _this.Trim(message) == ''){
return wx.showToast({
title: '信息不能为空',
icon: 'none',
duration: 2000
})
}
//封装消息类型 消息内容 + "[" + 发送消息的人 + ";" + 接收消息的人","+0
//0是单发1是群发
var cotnt = message + "[" + app.globalData.userId + ";" + _this.data.receiveUserId + ',' + 0;
//保存聊天记录
_this.save(cotnt);
wx.sendSocketMessage({
// message + "[" + "1c3546026fe24ff08d28115d58063263" + ";" + '944b50fd0eb64c4c816a4f3bf3203842' + ',' + 0,
data: cotnt,
})
_this.setData({
value:''
})
if (_this.data.status == 0 || _this.data.status==''){
//修改聊天室状态
// console.log("修改")
_this.update()
}
},
/**
* 去空格
*/
Trim: function (str) {
return str.replace(/(^\s*)|(\s*$)/g, "");
},
/**
* 查询两个人的聊天记录
*/
getChatRecord: function (chatListId,sendUserId){
// console.log(sendUserId)
var _this = this;
wx.request({
url: apiURL.wxChatRecord.getChatRecord,
method: "POST",
header: {
'content-type': 'application/x-www-form-urlencoded'
},
data: { "chatListId": chatListId, "sendUserId": sendUserId },
success: function (data) {
console.log(data)
var chatList = data.data.data;
var len = 10000 * chatList.length;
if (data.data.code == 200) {
return _this.setData({
chatRecord: chatList,
scrollTop: len//定位在最下方 显示最后一条信息
})
}
wx.showToast({
title: '系统错误',
icon: 'none',
duration: 2000
})
}
})
},
// getUserInfo: function () {
// var that = this
// wx.getSetting({
// success(res) {
// if (!res.authSetting['scope.userInfo']) {
// wx.authorize({
// scope: 'scope.userInfo',
// success() {
// that.UserLogin();
// }
// })
// }
// else {
// that.UserLogin();
// }
// }
// })
// },
/**
* 查用户信息
*/
getUserInfo:function(userId){
var _this = this;
//先从缓存拿取 缓存里没有再从 数据库拿取
//console.log("查询用户信息走缓存")
var userInfo = wx.getStorageSync(userId);
// console.log(userInfo)
if (userInfo) {
//成功获取到数据绑定到页面
_this.setData({
userInfo: userInfo
})
} else {
// console.log("缓存查不到用户信息,向后台发起请求")
//查询不到向后台发起请求
//查询用户信息
wx.request({
url: apiURL.wxUser.getBaseInfo,
method: "POST",
header: {
'content-type': 'application/x-www-form-urlencoded' // post请求时需改成这个才行 否则后台接收不到参数
},
data: { "id": userId},
dataType: "json",
success: function (data) {
// console.log(data)
//获取失败弹窗
if (data.data.code != 200) {
wx.showToast({
title: '系统错误',
icon: 'none',
duration: 2000
})
} else {
// console.log("查询成功")
// console.log(data.data)
//成功获取数据 放到缓存
wx.setStorageSync(app.globalData.userId, data.data.result)
//同时将数据绑定
_this.setData({
userInfo: data.data.result
})
}
}
})
}
},
/**
* 根据双方id查聊天室是否存在
*/
isChatList: function (currentUserId, objectUserId){
var _this = this;
wx.request({
url: apiURL.wxChatList.isGetChatList,
method: "POST",
header: {
'content-type': 'application/x-www-form-urlencoded'
},
data: { "currentUserId": currentUserId, "objectUserId": objectUserId },
success: function (data) {
// console.log(data)
//查聊天室的聊天记录
_this.getChatRecord(data.data.data.id, app.globalData.userId);
_this.setData({
chatListId: data.data.data.id,
status: data.data.data.status,
})
}
})
},
/**
* 保存聊天记录
*/
save: function (cotnt){
var _this=this;
wx.request({
url: apiURL.wxChatRecord.save,
method: "POST",
header: {
'content-type': 'application/x-www-form-urlencoded'
},
data: { "message": cotnt },
success: function (res) {
// console.log(res.data.data)
var chatRecord= _this.data.chatRecord;
chatRecord.push(res.data.data)
// console.log(chatRecord)
var len = 1000 * chatRecord.length;
_this.setData({
users: res.data,
userId: app.globalData.userId,
chatRecord:chatRecord,
scrollTop: len,//使新发出的信息显示在最下面
})
}
})
},
/**
* 修改聊天室转态
*/
update: function (){
var _this=this;
wx.request({
url: apiURL.wxChatList.update,
method: "POST",
header: {
'content-type': 'application/x-www-form-urlencoded'
},
data: { "id": _this.data.chatListId, "status": 1},
success: function (res) {
// console.log(res.data)
if(res.data.code==200){
_this.setData({
status: 1
})
}
}
})
},
/**
* 跳到对象用户详情
*/
sendOtherUser(e){
console.log(e)
var _this=this;
wx.navigateTo({
url: '../../other/personDetails/personDetails?userId=' + _this.data.receiveUserId,
})
},
/**
* 查看我的详情
*/
sendUser:function(){
wx.navigateTo({
url: '../../my/my?userId=' + app.globalData.userId,
})
}
})
问题以及解决方法
1. 无法与前端建立连接
使用ServerEndpointExporter但没用使用外置tomcat容器
错误原因:ServerEndpointExporter需要外置tomcat容器运行环境,但平常我们都是使用SpringBoot内置tomcat,导致ServerEndpointExporter在运行时一直报错。
解决方案:在pom.xml文件中,排除SpringBoot自带的嵌入tomcat,添加外置的tomcat依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 移除嵌入式tomcat插件 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 添加外置的tomcat依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
2. 使用拦截器或过滤器但没有对请求放行
错误原因:使用拦截器或过滤器但没有对请求放行,特别是使用Spring Security或Shiro,很容易遗忘放行请求路径
解决方案:在相应的配置文件中放行请求路径
filterMap.put("/websocket/**", "anon");//开放webSocket路径
3. 解决在web页面中动态口令场景下的连接与断开问题
在页面维持期间,动态口令需要一直不断的向前端推送实时口令,但是
何时断开
以及是谁断开
就成了一个问题
解决方式如下:
创建一个WebSocketConnectStatusController类,
在前端发起websocket请求前,由前端生成一个特定的长度的key,
(1)进入动态口令页面,首先调用doIn接口并传入key,该key由前端保留备用;后端使用静态map来存放key,并将value设为0。
(2)建立连接,同时在session中传入上一步生成的key
@RestController
@RequestMapping("/webSocketConnectStatus")
public class WebSocketConnectStatusController {
public static Map<String, String> map = new HashMap<>();
@GetMapping("/doIn")
@ApiOperation(value = "doIn方法")
public String doIn(String key){
//System.out.println("调用了doIn---");
map.put(key,"0");
return "开始获取动态码了";
}
@GetMapping("/doOut")
@ApiOperation(value = "doOut方法")
public String doOut(String key){
//System.out.println("调用了doOut---");
map.put(key,"1");
return "改完状态了,动态码获取结束";
}
}
(3)当用户离开页面时,调用doOut接口,并传入之前保留的本次连接的key,后端将静态map中对应key的value更新为1,此时后端不再向前端推送新的口令,循环结束。
后端推送消息的具体逻辑如下:
@OnMessage
public void onMessage(String message, Session session) {
String key = session.getQueryString();//获取session中的key
try {
while ("0".equals(WebSocketConnectStatusController.map.get(key))){
Thread.sleep(1000);//每秒推送一次
sendMessage("生成你的动态码");
}
WebSocketConnectStatusController.map.remove(key);//清空本次链接的key
} catch (IOException e) {
System.out.println("IO异常");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
只有当循环结束时才能正常断开连接,否则会报IO异常
原文章地址:
https://blog.csdn.net/qq_39897814/article/details/93597408
https://blog.csdn.net/qq_38589118/article/details/129004248