1、WebSocket的优势
我们都知道HTPP协议是基于请求响应模式,并且无状态的。HTTP通信只能由客户端发起,HTTP 协议做不到服务器主动向客户端推送信息
举例来说,我们想要查询当前的排队情况,只能是页面轮询向服务器发出请求,服务器返回查询结果。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此WebSocket 就是在这样的背景下发明的。
2、SpringBoot 整合 WebSocket
1、添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2、新建WebSocket 配置类
这个配置类检测带注解@ServerEndpoint的bean并注册它们,配置类代码如下:
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter,
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3、 OnLineWebSocket
在线编辑的OnLineWebSocket类,当前页面被用户修改后,推送页面的变更信息给前台 前台根据页面id和页面的版本,判断是否重新加载页面数据。
@Slf4j
@Component
@ServerEndpoint("/onLineWebSocket/{pageId}")
public class OnLineWebSocket {
private static Interner<String> lock = Interners.newWeakInterner();
private static final String LOCK_PREFIX = "socket_";
// 长连接的超时时间,如果10分钟没有操作,认为超时
private static final long TIMEOUT = 10 * 60 * 1000;
/**
* 长连接会话Id
*/
private String sessionId;
/**
* pageId 页面id
*/
private String pageId;
/**
* 长连接会话
*/
private Session session;
/**
* 最近一次心跳时间, 用于定时清理失效的会话连接
*/
private long heart;
/**
sessionId 与 onLineWebSocket 对应关系
一个会话,对应一个 onLineWebSocket 对象
*/
private static ConcurrentHashMap<String, OnLineWebSocket> webSocketMap = new ConcurrentHashMap<>();
/**
一个页面id,如果有多个浏览器打开,就对应多个会话,
所以页面Id,和回话id是一对多的关系
* pageId 与sessionId一对多关系,
*/
private static ConcurrentHashMap<String, Set<String>> pageSessionMap = new ConcurrentHashMap<>();
/**
* 打开连接时调用
*
* @param session 会话
* @param pageId 页面id
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "pageId") String pageId) {
if (StringUtil.isNotEmpty(pageId)) {
this.pageId = pageId;
this.session = session;
this.sessionId = session.getId();
// 每次连接时,更新心跳时间
this.heart = System.currentTimeMillis();
webSocketMap.put(this.sessionId, this);
this.addPageSession();
}
else {
throw new EcoBootException("页面id参数不能为空");
}
}
/**
* 关闭连接时调用
连接关闭时,要清除对应的关系
*/
@OnClose
public void onClose() {
this.closeSession();
this.deletePageSession();
if (StringUtil.isNotEmpty(sessionId)) {
webSocketMap.remove(sessionId);
}
}
/**
* 错误时调用
*
* @param throwable 异常
*/
@OnError
public void onError(Throwable throwable) {
System.out.println("发生错误");
throwable.printStackTrace();
}
/**
* 收到客户端信息后,根据接收人的 pageId 把消息推下去或者群发
*
* @param message json格式消息
* @throws IOException 异常
*/
@OnMessage
public void onMessage(String message) throws IOException {
if (session!=null && session.isOpen() && !StringUtil.isEmpty(message)) {
if (StringPool.STAR.equals(message)) {
//心跳测试消息
session.getBasicRemote().sendText(message);
heart = System.currentTimeMillis();
}
else {
session.getBasicRemote().sendText(message);
heart = System.currentTimeMillis();
return;
}
}
}
/**
* 给页面添加session
*/
private void addPageSession() {
String joinId = this.pageId;
synchronized (lock.intern(LOCK_PREFIX + joinId)) {
Set<String> sessionIds = pageSessionMap.computeIfAbsent(joinId, k -> new HashSet<>());
sessionIds.add(this.session.getId());
}
}
/**
* 推送消息到客户端
* @param message 消息文本
*/
public boolean pushMessage(String message) {
log.info("start to push Msg = {}",message);
try {
if (session!=null && session.isOpen()) {
log.info("call push to send");
this.session.getBasicRemote().sendText(message);
// 更新心跳时间
this.heart = System.currentTimeMillis();
log.info("push msg ok");
return true;
} else {
return false;
}
} catch (Exception e) {
log.error("pushMessage",e);
return false;
}
}
/**
* 关闭连接会话
*
*/
private void closeSession() {
if (session!=null && session.isOpen()) {
try {
session.close();
} catch (Exception e) {
log.error("closeSession", e);
}
}
}
/**
* 长连接会话是否有效
心跳时间进行判断处理
* @return boolean
*/
private boolean isValid() {
return session!=null && session.isOpen() && heart + TIMEOUT > System.currentTimeMillis();
}
/**
* 清理超时会话,10分钟之内无心跳会话,或会话已经关闭
*/
public static void clearSession() {
Iterator<Map.Entry<String, OnLineWebSocket>> iterator = webSocketMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, OnLineWebSocket> entry = iterator.next();
OnLineWebSocket ws = entry.getValue();
if(!ws.isValid()){
ws.closeSession();
ws.deletePageSession();
iterator.remove();
}
}
}
/**
* 删除页面id和会话id对应关系
*/
private void deletePageSession() {
String joinId = this.pageId;
synchronized (lock.intern(LOCK_PREFIX + joinId)) {
Set<String> sessionIds = pageSessionMap.get(joinId);
if (sessionIds != null && sessionId != null) {
sessionIds.remove(sessionId);
}
if (CollectionUtil.isEmpty(sessionIds)) {
pageSessionMap.remove(joinId);
}
}
}
/**
根据pageId,来获取会话Id,列表
*
* @param pageId 页面id
* @return Set
*/
public static Set<String> getPageSessions(String pageId) {
String joinId = pageId;
synchronized (lock.intern(LOCK_PREFIX + joinId)) {
Set<String> ids = pageSessionMap.get(joinId);
if (ids != null) {
return new HashSet<>(ids);
}
}
return null;
}
// 根据回话id,来获取 OnLineWebSocket 对象
public static OnLineWebSocket get(String sessionId){
if(!StringUtil.isEmpty(sessionId)){
return webSocketMap.get(sessionId);
}
return null;
}
/**
* 每隔10分钟进行清理
*/
@Async("scheduledPoolTaskExecutor")
@Scheduled(cron = "0 */10 * * * *")
public void taskToCleanSession() {
clearSession();
}
}
4、当页面有修改时,通知前台进行刷新
/**
* 推送消息给前台,通知刷新页面内容
*/
public void pushWebSocketMsg(String pageId) {
// 启动线程来进行消息的推送
ThreadUtil.execute(()->{
Set<String> sessions = OnLineWebSocket.getPageSessions(pageId);
// 给所有的会话进行推送
if(!CollectionUtil.isEmpty(sessions)) {
log.info("sessions size = {}",sessions.size());
PageInfo pageInfo = pageInfoService.getById(pageId);
WebSocketData webSocketData = new WebSocketData();
webSocketData.setPageId(pageId);
webSocketData.setVersion(pageInfo.getDraftVersion());
String msg = BeanUtil.toJsonString(webSocketData);
for(String id : sessions) {
log.info("session id = {}",id);
OnLineWebSocket onLineWebSocket = OnLineWebSocket.get(id);
if(onLineWebSocket != null ){
onLineWebSocket.pushMessage(msg);
}
}
}
});
}
3、测试运行
1、在前端控制台输入如下代码
const socket = new WebSocket('ws://xxxx:2022/eco-online-edit-server/onLineWebSocket/9/');
// Connection opened
socket.addEventListener('open', function (event) {
socket.send('Hello Server!');
});
// Listen for messages
socket.addEventListener('message', function (event) {
console.log('Message from server ', event.data);
});
2、效果图如下:
- 控制台端效果
- NetWork端效果