简介
**WebSocket是一种在单个TCP连接上进行全双工通信的协议。**WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
数据库(参考)
# 消息主体
CREATE TABLE `v_message` (
`message_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`title` varchar(300) DEFAULT NULL COMMENT '标题',
`content` varchar(3000) DEFAULT NULL COMMENT '内容',
`msg_type` tinyint(3) DEFAULT NULL COMMENT '消息类型(1公告、2通知、3私信)',
`sender_id` bigint(20) DEFAULT NULL COMMENT '发送人ID',
`notice_type` tinyint(3) DEFAULT NULL COMMENT '通知类型(发票、应收款、入库、其它...)',
`create_time` datetime DEFAULT NULL,
`deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除',
`receiver_type` tinyint(3) DEFAULT NULL COMMENT '接收者类别(1个人/2全部/角色/部门/分组)',
`group_id` bigint(20) DEFAULT NULL COMMENT '接收者组别ID(由 v_receiver_group 表维护)',
PRIMARY KEY (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='消息表';
# 接收消息
CREATE TABLE `v_msg_receive` (
`msg_receive_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`message_id` bigint(20) DEFAULT NULL COMMENT '消息ID(由 v_message 表维护)',
`receiver_id` bigint(20) DEFAULT NULL COMMENT '接收者ID',
`state` tinyint(1) DEFAULT NULL COMMENT '状态(0未读/1已读)',
`read_time` datetime DEFAULT NULL COMMENT '阅读时间',
`deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`msg_receive_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='消息接收表';
Vue
webSocketLink(){
let _this = this;
// 心跳检测
var heatCheck = {
timeout: 10000, // 心跳间隔时间
num: 3, //心跳均未响次数,超过应重连
timeoutObj: null,
serverTimeoutObj: null,
reset: function(){
clearInterval(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start: function(){
var self = this;
this.timeoutObj = setTimeout(function(){
_this.websocket.send("ping");
//console.log("ping");
self.serverTimeoutObj = setTimeout(function(){
console.log("心跳停止,断开websocket----");
_this.websocket.close();
}, 15000);
}, self.timeout);
}
}
if ('WebSocket' in window) {
_this.updateUrl("websocketServer");
_this.websocket = new WebSocket(_this.wsUrl);
}else {
alert("此浏览器不支持消息通知功能,请更换或升级浏览器!!!");
}
/* else if ('MazWebSocket' in window) {
_this.updateUrl("webSocketServer");
_this.websocket = new MazWebSocket(_this.wsUrl);
} else {
_this.updateUrl("sockjs/webSocketServer");
_this.websocket = new SockjS(_this.wsUrl);
} */
_this.websocket.onopen = function(){
console.log("websocket连接成功");
heatCheck.reset().start(); // 开启心跳检测
}
_this.websocket.onmessage = function (event) { // 接收后台发来的消息
if (event.data === 'pong'){
heatCheck.reset().start();
console.log(event.data);
}else{
console.log("收到websocket服务端消息:" + event.data);
_this.$notify.info({
title: '你有一则新消息',
message: event.data,
duration: 0,
offset: 25
});
_this.refreshData();
}
}
_this.websocket.onerror = function(){
console.log("websocket连接错误")
}
_this.websocket.onclose = function(){
console.log("websocket尝试重连.....");
_this.webSocketLink();
}
},
// 请求路径
updateUrl(urlPath){
let _this = this;
let BASE_URL = document.location.host + '/api/' + urlPath + '/' + this.navData.username+ '/' + this.navData.userId;
if (urlPath.indexOf('sockjs') != -1) {
_this.wsUrl = 'http://' + BASE_URL;
} else {
if (window.location.protocol == 'http:') {
_this.wsUrl = 'ws://' + BASE_URL;
} else {
_this.wsUrl = 'wss://' + BASE_URL;
}
}
},
Java
import lombok.extern.slf4j.Slf4j;
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.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
@ServerEndpoint(value = "/websocketServer/{username}/{userId}")
public class Websocket {
private static int onlineCount = 0; // 当前连接数
private static Map<Long, Websocket> clients = new ConcurrentHashMap<>(); // 已连接客户端
private Session session;
private Long userId;
private String username;
/**
* 连接时执行
* @param
* @param username
* @param session
* @throws IOException
*/
@OnOpen
public void onOpen( @PathParam("username") String username,@PathParam("userId") Long userId, Session session) {
this.userId = userId;
this.username = username;
this.session = session;
addOnlineCount();
clients.put(userId, this);
System.out.println("新增连接用户:"+username);
System.out.print("当前连接用户ID:");
clients.forEach((Long key, Websocket websocket)-> System.out.print(key.toString()+'、'));
}
/**
* 收到消息时执行
* @param message
* @throws IOException
*/
@OnMessage
public void onMessage(String message, Session session) {
if ("ping".equals(message)){
session.getAsyncRemote().sendText("pong");
} else {
System.out.println(message);
}
//sendMessageAll(message); // 新通知
// 先解析收到的json数据
// 根据类型决定发送对象
}
/**
* 连接错误时执行
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
/**
* 发送消息给指定用户
* @param message
* @param userIds
* @throws IOException
*/
public void sendMessageTo(String message, List<Long> userIds) throws IOException {
// session.getBasicRemote().sendText(message);
userIds.forEach(id ->{
if(clients.get(id)!=null){
clients.get(id).session.getAsyncRemote().sendText(message);
}
});
/*
for (Websocket item : clients.values()) {
if (item.username.equals(To)) {
item.session.getAsyncRemote().sendText(message);
}
}
*/
}
/**
* 发送消息给全部用户
* @param message
*/
public void sendMessageAll(String message) {
for (Websocket item : clients.values()) {
item.session.getAsyncRemote().sendText(message);
}
}
/**
* 关闭时执行
* @throws IOException
*/
@OnClose
public void onClose() throws IOException {
clients.remove(username);
subOnlineCount();
}
public static synchronized int getOnlineCount(){
return onlineCount;
}
public static synchronized void addOnlineCount() {
Websocket.onlineCount++;
log.info("新增一条连接,当前存在连接数:"+onlineCount);
}
public static synchronized void subOnlineCount() {
Websocket .onlineCount--;
log.info("减少一条连接,当前存在连接数:"+onlineCount);
}
public static synchronized Map<Long, Websocket> getClients() {
return clients;
}
}
切面
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 发送消息通知的切面
*/
@Slf4j
@Aspect
@Component
public class MessageAspect {
@Autowired
private Websocket websocket;
@Pointcut("execution(* com.threepiles.lantern.finance.service.impl.InvoiceServiceImpl.submitInvoice(..))")
public void pointcutInvoice() {}
// 发票审核后置通知
@AfterReturning("pointcutInvoice()")
public void doAfter(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
InvoiceAudit invoiceAudit = null;
if (args!=null){
invoiceAudit = (InvoiceAudit)args[0]; // 切点方法的入参
} else {
return;
}
Message message = new Message();
......
......
......
// websocket通知用户
try {
if (userIds!=null){
websocket.sendMessageTo(message.getTitle(), userIds);
}
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}