websocket实现实时消息展示
前面介绍过websocket聊天功能的实现,不清楚的可以先看下
websocket实现在线聊天
https://blog.csdn.net/qq_51076413/article/details/124644500
之前发过
websocket
的相关使用和一对一聊天的·demo·代码,这里是针对上几篇文章的补充,不明白的可以看下我以前的websocket
技术文章
这里介绍下如何实时显示好友推送过来的消息
右下角实时推送和接收通知
好友列表中实时显示最新消息
下面直接上代码
代码并未完善,比如显示未读条数、好友下线刷新好友列表等,这些功能在
websocket实现在线聊天
中都有实现,有需要的可以自己整合下
不废话上代码
后台代码展示
package cn.molu.ws.socket;
import cn.hutool.core.util.ObjUtil;
import cn.molu.ws.pojo.Message;
import cn.molu.ws.pojo.User;
import cn.molu.ws.vo.Result;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* @ApiNote: 网络连接控制器
* @Author: 陌路
* @Date: 2023/1/16 13:23
* @Tool: Created by IntelliJ IDEA.
*/
@Slf4j
@Component
@ServerEndpoint("/socket/{userId}")
public class WebSocketService {
// 存储已连接用户数据
private static final Map<String, Session> onLineUser = new ConcurrentHashMap<>();
private static final String PONG = "PONG";
private static final String PING = "PING";
/**
* 连接建立时 会调用该方法
*/
@OnOpen
public void onOpen(@PathParam("userId") String userId, Session session) {
log.info("系统消息推送。。。{} ", userId);
setonLineUser(userId, session);
}
/**
* @apiNote: 接收到客户端发送的数据时 会调用此方法
* @param: [message, session]
* @return: void
*/
@OnMessage
public void onMessage(String msg, Session session) {
log.info("来自客户端的消息:{}", msg);
if (StringUtils.isNotBlank(msg)) {
Message message = JSON.parseObject(msg, Message.class);
if (ObjUtil.isNotEmpty(message) && StringUtils.isNotBlank(message.getHeartbeat())) {
if (PING.equals(message.getHeartbeat()) && 1 == message.getType() && ObjUtil.isNotEmpty(session) && session.isOpen()) {
// 发送和接收心跳包,30s一次
message.setHeartbeat(PONG);
try {
session.getBasicRemote().sendText(JSON.toJSONString(message));
} catch (IOException e) {
log.error("同步消息发送出错",e.getMessage());
e.printStackTrace();
}
return;
}
}
}
}
/**
* 连接关闭时 调用此方法
* @throws IOException
*/
@OnClose
public void onClose(@PathParam("userId") String userId, Session session) {
log.debug("关闭连接-->{}", userId);
remove(userId);
User user = User.getUserById(userId);
user.setDate(new Date());
Message message = new Message();
message.setContent("好友" + user.getUsername() + "下线了");
message.setType(-1); // 系统消息
message.setUserId(userId);
message.setUsername(user.getUsername());
sendMessage2Everyone(message);
}
/**
* 出现错误时调用改方法
*/
@OnError
public void onError(Session session, Throwable error) {
log.info("连接出错了......{}", error);
}
/**
* @apiNote: 添加新连接用
* @param: [userId, session]
* @return: void
* @throws IOException
*/
private void setonLineUser(String userId, Session session) {
if (StringUtils.isNotBlank(userId)) {
onLineUser.put(userId, session);
log.info("有新的用户加入:{},人数:{}", userId, onLineUser.size());
User user = User.getUserById(userId);
user.setDate(new Date());
Message message = new Message();
message.setContent("好友" + user.getUsername() + "上线了");
message.setType(1); // 系统消息
message.setUserId(userId);
message.setUsername(user.getUsername());
User.setUser(user);
// 给所有在线用户推送上线通知
sendMessage2Everyone(message);
}
try {
Set<User> list = getList();
session.getBasicRemote().sendText(JSON.toJSONString(Result.ok(list)));
} catch (IOException e) {
log.error("同步消息发送出错",e.getMessage());
e.printStackTrace();
}
}
/**
* @apiNote: 获取指定已建立连接的用户
* @param: [userId]
* @return: Session
*/
public static Session getWebSocketSession(String userId) {
return (!StringUtils.isBlank(userId) && onLineUser.containsKey(userId)) ? onLineUser.get(userId) : null;
}
/**
* @apiNote: 获取所有连接用户
* @param: []
* @return: Map<String,Session>
*/
public static Map<String,Session> getAllOnlineUsers(){
return onLineUser;
}
/**
* @apiNote: 一对一发送消息
* @param: [message:toId、content]
* @return: void
*/
public static void sendMessageO2O(Message message) {
Session session = null;
if (ObjUtil.isNotEmpty(message) && StringUtils.isNotBlank(message.getToId())
&& StringUtils.isNotBlank(message.getContent())
&& StringUtils.isNotBlank(message.getUserId())) {
final String toId = message.getToId();
if (onLineUser.containsKey(toId)) {
session = onLineUser.get(toId);
}
if (ObjUtil.isNotEmpty(session) && session.isOpen()) {
// 异步消息发送
// session.getAsyncRemote().sendText(JSON.toJSONString(message));
try {
// 同步消息发送
session.getBasicRemote().sendText(JSON.toJSONString(message));
} catch (IOException e) {
log.error("同步消息发送出错",e.getMessage());
e.printStackTrace();
}
}
}
}
/**
* @apiNote: 给所有人发送消息
* @param: [message]
* @return: void
*/
public static void sendMessage2Everyone(Message message) {
if (ObjUtil.isNotEmpty(message) && onLineUser.size() > 0) {
onLineUser.entrySet().forEach(item -> {
final Session session = item.getValue();
if (ObjUtil.isNotEmpty(session) && session.isOpen()) {
message.setType(1); // 系统消息
message.setDate(new Date());
// 同步发送
// session.getBasicRemote().sendText(JSON.toJSONString(message));
try {
// 异步消息发送
session.getBasicRemote().sendText(JSON.toJSONString(message));
} catch (IOException e) {
log.error("同步消息发送出错",e.getMessage());
e.printStackTrace();
}
}
});
}
}
/**
* @apiNote: 从缓存中删除已连接的用户
* @param: [userId]
* @return: Session
*/
public static void remove(String userId) {
if (StringUtils.isNotBlank(userId) && onLineUser.containsKey(userId)) {
onLineUser.remove(userId);
}
log.debug("移除连接用户:{},当前人数:{}", userId, onLineUser.size());
}
/**
* @apiNote: 获取所有在线用户信息
* @return: Set<User>
*/
public static Set<User> getList() {
final Map<String, Session> allOnlineUsers = WebSocketService.getAllOnlineUsers();
Set<User> users = new HashSet<>();
for (Map.Entry<String, Session> entry : allOnlineUsers.entrySet()) {
final User user = User.getUserById(entry.getKey());
users.add(user);
}
return users;
}
}
说明:异步发送在并发下会出现异常错误,此时在发送消息的地方加上锁可以解决
// 异步消息发送
synchronized (session) {
session.getAsyncRemote().sendText(JSON.toJSONString(message));
}
消息处理控制器,这里可以自定义消息的过滤,拦截替换或终止发送消息
/**
* @ApiNote: 消息处理控制器
* @Author: 陌路
* @Date: 2023/1/16 15:25
* @Tool: Created by IntelliJ IDEA.
*/
@RestController
@RequestMapping("/message/*")
public class MessageController {
@GetMapping("sendMessage")
public Result sendMessage(Message message) {
message.setDate(new Date());
WebSocketService.sendMessageO2O(message);
return Result.ok("发送成功!");
}
@GetMapping("getList")
public Result getList() {
final Set<User> users = WebSocketService.getList();
return Result.ok(users);
}
}
这里是用的
map
集合存储的用户数据来模拟数据库用户,可根据自己的业务替换真实的数据库数据
/**
* @ApiNote: 用户实体类
* @Author: 陌路
* @Date: 2023/1/16 13:31
* @Tool: Created by IntelliJ IDEA.
*/
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class User extends BaseEntity{
private static final long serialVersionUID = -31513108721728277L;
/**
* 用户id
*/
private String userId;
/**
* 用户名
*/
private String username;
/**
* 用户手机号
*/
private String phone;
/**
* 用户密码
*/
private String password;
/**
* 账号
*/
private String account;
/**
* 构造用户数据 模拟数据库用户
*/
public static Map<String, User> userMap = new HashMap<String, User>();
static {
userMap.put("123456789", new User("001", "jack", "150******67", "123456","123456789"));
userMap.put("987654321", new User("002", "mary", "135******88", "123456","987654321"));
}
public static User getUserById(String userId){
for (User user : userMap.values()) {
if (user.getUserId().equals(userId)) {
return user;
}
}
return new User();
}
public static void setUser(User user) {
if (ObjUtil.isNotEmpty(user)) {
userMap.put(user.getAccount(), user);
}
}
}
前台完整代码展示
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>消息推送</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.3/jquery.js"></script>
<style type="text/css">
.main {
width: 70%;
margin: auto;
padding: 10px;
text-align: center;
}
.login-main {
display: block;
width: 60%;
margin: auto;
padding: 20px;
box-shadow: 1px 0px 5px 2px #d0d0d0;
border-radius: 10px;
}
.login-main div {
font-size: 1.3em;
font-family: cursive;
}
.login-main div p input {
border-radius: 5px;
height: 25px;
border: 1px solid #999;
outline-color: #50a7f6;
}
.login-btn {
font-size: 1.2em;
background-color: #06cd06;
color: #ffffff;
border: 1px solid #06cd06;
padding: 5px 15px;
border-radius: 8px;
}
.list-main {
display: none;
width: 100%;
}
.main-win {
width: 100%;
box-shadow: 1px 0px 5px 2px #d0d0d0;
border-radius: 10px;
display: flex;
}
.main-list, .main-chat {
border: 1px solid #999;
height: 500px;
}
.main-list {
width: 30%;
}
.main-chat {
width: 70%;
}
.chat {
width: 100%;
text-align: left;
background-color: #fafafa;
border: 1px solid #e8e8e8;
cursor: pointer;
}
.message {
font-size: 14px;
overflow: hidden;
white-space: nowrap;
}
.chat p {
margin: 3px;
}
.chat:hover {
background-color: antiquewhite;
}
.notice {
position: fixed;
bottom: 6px;
right: 6px;
box-shadow: 1px 0px 5px 2px #d0d0d0;
width: 300px;
display: none;
background-color: #fff;
}
.notice-title {
width: 100%;
height: 30px;
display: flex;
justify-content: space-between;
border-bottom: 1px solid #999;
}
.notice-title div {
padding: 5px 10px;
cursor: pointer;
}
.close {
background-color: red;
color: #fff;
}
.notice-msg {
height: 100px;
text-align: center;
font-size: 1.1em;
padding: 20px 5px;
margin: 10px;
max-height: 150px;
}
.chat-msg {
width: 100%;
height: 410px;
}
.edit-msg {
width: 100%;
}
.edit-msg textarea {
width: 99%;
height: 50px;
outline: none;
resize: none;
}
.send-btn {
width: 100%;
height: 30px;
}
.send-msg {
font-size: 1.1em;
}
.msg-left {
padding: 5px;
background-color: #50a7f6;
}
.msg-right{
padding: 5px;
background-color: skyblue;
}
.p-msg-right{
height: 30px;
text-align: right;
padding: 5px;
}
.p-msg-left{
height: 30px;
text-align: left;
padding: 5px;
}
</style>
</head>
<body>
<div class="main">
<!-- 登录框 -->
<div class="login-main">
<div>请登录</div>
<div>
<p>
<span>账号:</span><input type="text" id="account" maxlength="16" placeholder="请输入账号"/>
</p>
<p>
<span>密码:</span><input type="password" id="password" maxlength="16" placeholder="请输入密码"/>
</p>
<p>
<button class="login-btn">登录</button>
</p>
</div>
</div>
<!-- 聊天界面 -->
<div class="list-main">
<div class="main-win">
<div class="main-list"></div>
<div class="main-chat">
<div class="chat-msg">
</div>
<div class="edit-msg">
<textarea id="edit-msg-val"></textarea>
</div>
<div class="send-btn">
<button class="send-msg">发送</button>
</div>
</div>
</div>
</div>
</div>
<!-- 右下角消息通知 -->
<div class="notice">
<div class="notice-title">
<div class="notice-title-msg">消息通知</div>
<div class="close">X</div>
</div>
<div class="notice-msg">
<span class="notice-msg">好友李明上线了</span><br/>
<span class="notice-date">2023-01-16 22:04</span>
</div>
</div>
</body>
<script type="text/javascript">
let user = {}; // 当前登录用户
let userList = []; // 好友列表
let selectedUser = {}; // 当前聊天好友
let socket = {};//websocket连接
let interval = null; // 定时器
let msgChatArr = []; // 聊天记录
$(function () {
user = JSON.parse(localStorage.getItem("user")) || {};
selectedUser = JSON.parse(localStorage.getItem("selectedUser")) || {};
userList = JSON.parse(localStorage.getItem("userList")) || [];
msgChatArr = JSON.parse(localStorage.getItem("msgChatArr")) || [];
if (!user.userId) {
$(".list-main").hide();
$(".login-main").show();
} else {
$(".list-main").show();
$(".login-main").hide();
initWebsocket(user.userId)
getUserList();
}
if (selectedUser && selectedUser.userId) {
selectedUserChat(selectedUser.userId, selectedUser.username);
}
})
// 初始化连接
function initWebsocket(userId) {
if (userId && socket.readyState != 1) {
// 创建连接
socket = new WebSocket(`ws://127.0.0.1:8088/socket/${userId}`);
// 建立连接
socket.onopen = function (res) {
//建立连接,开始发送心跳包
sendPing(userId);
};
// 接收到消息
socket.onmessage = function (res) {
let data = res.data || '';
if (typeof res.data == 'string') {
data = JSON.parse(res.data);
}
console.log('onmessage.data', data);
if (data.type == 1) {
showNotice(data);
updateShowView(data);
} else {
updateShowView(data);
pushMsg(data);
}
if (data.code == 200) {
getUserList(data.data);
}
};
// 连接失败
socket.onerror = function (res) {
alert(res.msg || '连接失败!');
};
// 关闭连接
socket.onclose = function (res) {
};
} else {
}
}
// 登录按钮
$(".login-btn").click(function () {
let password = $("#password").val() || '';
let account = $("#account").val() || '';
if (!account.trim() || !password.trim()) {
return alert("请输入账号和密码!");
}
$.getJSON("/user/login", {account: account, password: password}, function (res) {
if (typeof res == 'string') {
res = JSON.parse(res);
}
if (res.code == 200) {
user = res.data || {};
console.log('user', user);
if (user.userId) {
localStorage.setItem("user", JSON.stringify(user));
initWebsocket(user.userId)
$(".list-main").show();
$(".login-main").hide();
}
} else {
alert(res.msg || '未知错误');
}
});
});
// 获取好友列表
function getUserList(data) {
if (!data) return;
if (typeof data == 'string') {
data = JSON.parse(data);
}
userList = data;
if (userList.length > 0) {
localStorage.setItem("userList", JSON.stringify(userList));
showListView(userList);
}
}
// 显示好友列表
function showListView(list) {
if (!list || list.length == 0) {
list = userList;
}
$.each(list, function (index, item) {
if(item.userId != user.userId){
let date = item.date || '';
date = date + "".substring(0, 16);
let html = `<div class="${item.userId} chat" οnclick="selectedUserChat('${item.userId}','${item.username}')">
<p>${item.username || ''}</p>
<p id="${item.userId}" class="message">${item.message || date || ''}</p>
</div>`;
if (item.username) {
$(".main-list").append(html);
}
}
});
}
// 选择好友
function selectedUserChat(id, name) {
$("." + id).css("background", "antiquewhite").siblings().css("background", "#fafafa");
selectedUser = {userId: id, username: name};
localStorage.setItem("selectedUser", JSON.stringify(selectedUser));
}
// 更新好友列表中的显示消息
function updateShowView(data) {
if (data.content && data.type != 1) {
$("#" + data.userId).text(data.content);
}
}
function pushMsg(data){
let cls = "msg-left";
if (user.userId == data.userId){
cls = "msg-right";
}
if (data.content){
let html = `<p class="p-${cls}"><span class="${cls}">${data.content}</span></p>`;
$(".chat-msg").append(html);
}
}
// 30s发送一次心跳包
function sendPing(userId) {
userId = userId || user.userId || '';
if (!userId) return;
interval = setInterval(() => {
if (socket.readyState == 1) {
socket.send(JSON.stringify({heartbeat: "PING", type: 1}));
}
}, 30 * 1000);
}
// 显示消息提醒框
function showNotice(data) {
let title = data.title || '消息通知';
let msg = data.content || '';
let userId = data.userId || '';
let date = data.date || '';
if (!msg || user.userId == userId) {
return;
}
$(".notice-title-msg").text(title);
$(".notice-msg").text(msg);
$(".notice-date").text(date);
$(".notice").show("slow");
setTimeout(() => {
$(".notice").hide("slow");
}, 5000);
}
// 关闭右下角提醒框
$(".close").click(function () {
$(".notice").hide("slow");
$(".notice-title-msg").text('');
$(".notice-msg").text('');
$(".notice-date").text('');
});
// 发送消息给后台
$(".send-msg").click(function () {
let val = $("#edit-msg-val").val();
if (!val) {
return alert("内容不要为空!");
}
if (!selectedUser || !selectedUser.userId) {
return alert("请选择好友!");
}
let param = {
userId: user.userId,
username: user.username,
toId: selectedUser.userId,
toName: selectedUser.username,
content: val,
type: 0
};
$.getJSON("/message/sendMessage", param, function (res) {
if (typeof res == 'string') {
res = JSON.parse(res);
}
if (res.code == 200) {
$("#edit-msg-val").val('');
pushMsg(param);
} else {
alert(res.msg || '未知错误!');
}
});
});
</script>
</html>
这里就是前台和后台核心代码的地方了,有需要的可以自己整理到项目使用
不懂的可以在作者主页搜索"websocket"相关文章,里面有详细说明和代码地址
.
.
.
.
完整代码目前尚未整理,待完整代码整理完成后会放到git
上
.
.