WebSocketWebSocket 是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工通信(允许服务器主动发送信息给客户端)。
Springboot中整合WebSocket
pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.21</version>
</dependency>
配置类
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
/**
* 开启WebSocket支持
*/
@Configuration
public class WebSocketConfig implements ServletContextInitializer {
/**
* 这个bean的注册,用于扫描带有@ServerEndpoint的注解成为websocket,如果你使用外置的tomcat就不需要该配置文件
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
}
}
webSocket服务类
import cn.hutool.json.JSONUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
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.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.stream.Collectors;
@ServerEndpoint("/websocket/{id}")
@Component
@Slf4j
public class MessagePushSever {
// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private ConnectedUserWrapper connectedUserWrapper;
@Data
@AllArgsConstructor
public static class ConnectedUserWrapper{
private Session session;
private String id;
}
// session集合,存放对应的session
private static ConcurrentHashMap<String, ConnectedUserWrapper> sessionPool = new ConcurrentHashMap<>();
// concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。
private static CopyOnWriteArraySet<MessagePushSever> webSocketSet = new CopyOnWriteArraySet<>();
/**
* 建立WebSocket连接
*
* @param session
*/
@OnOpen
public void onOpen(Session session,@PathParam("id")String id) {
System.out.println("on Open!");
try {
try {
ConnectedUserWrapper userWrapper = sessionPool.get(id);
// connectedUserWrapper不为空,已经有人登录相同账号,则销毁并移除
if (userWrapper != null) {
sendKidOfflineMessage(userWrapper.getSession());
userWrapper.getSession().close();
}
} catch (IOException e) {
log.error("重复登录异常,错误信息:" + e.getMessage(), e);
}
// 建立连接
ConnectedUserWrapper connectedUserWrapper = new ConnectedUserWrapper(session, id);
sessionPool.put(id, connectedUserWrapper);
this.connectedUserWrapper = connectedUserWrapper;
webSocketSet.add(this);
log.info("建立连接完成,当前在线人数为:{}", webSocketSet.size());
}catch (Exception e){
e.printStackTrace();
try {
session.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
/**
* 发送当前用户被挤掉线消息
* @param session
* @throws IOException
*/
private void sendKidOfflineMessage(Session session) throws IOException {
if(session!=null){
MessageWrapper<String> partsFeedbackMessageWrapper = new MessageWrapper<>("error", "抱歉,您的账号已在其他位置登录,如果不是您操作,请修改密码!");
session.getBasicRemote().sendText(JSONUtil.toJsonStr(partsFeedbackMessageWrapper));
}
}
/**
* 发送错误消息
* @param session
*/
private void sendNotVerifyAndClose(Session session){
try {
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 发生错误
*
* @param e
*/
@OnError
public void onError(Throwable e) {
log.error("出现错误:"+e.getMessage(),e);
}
/**
* 连接关闭
*/
@OnClose
public void onClose() {
webSocketSet.remove(this);
if(this.connectedUserWrapper!=null && this.connectedUserWrapper.getId()!=null){
//避免挤掉他人时,上面已经移除掉了,导致把刚登陆的账号也移除掉了,
String id = this.connectedUserWrapper.getId();
sessionPool.remove(id);
log.info("下线用户:{},剩余在线人数:{}人",id,webSocketSet.size());
}
}
/**
* 接收客户端消息
*
* @param message 接收的消息
*/
@OnMessage
public void onMessage(String message) {
if(this.connectedUserWrapper!=null && this.connectedUserWrapper.getId()!=null) {
log.info("收到用户[{}]发来的消息::{}",this.connectedUserWrapper.getId(), message);
}
}
public static List<String> getOnlineUser(){
return sessionPool.values().stream().map(ConnectedUserWrapper::getId).collect(Collectors.toList());
}
public static ConcurrentHashMap<String, ConnectedUserWrapper> getOnlineUserMap(){
return sessionPool;
}
/**
* 推送消息到指定用户
*
* @param userId 用户ID
* @param message 发送的消息
*/
public static void sendMessageByUser(String userId, String message) {
log.info("用户ID:" + userId + ",推送内容:" + message);
ConnectedUserWrapper connectedUserWrapper = sessionPool.get(userId);
if(connectedUserWrapper!=null) {
try {
connectedUserWrapper.getSession().getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("推送消息到指定用户发生错误:" + e.getMessage(), e);
}
}
}
/**
* 群发消息
*
* @param message 发送的消息
*/
public static void sendAllMessage(String message) {
log.info("发送消息:{}", message);
for (MessagePushSever webSocket : webSocketSet) {
try {
webSocket.connectedUserWrapper.getSession().getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("群发消息发生错误:" + e.getMessage(), e);
}
}
}
}
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket Test</title>
<script src="./config.js"></script>
</head>
<body>
<h1>WebSocket Test</h1>
<input type="text" id="messageInput" placeholder="Enter your message">
<button onclick="sendMessage()">Send</button>
<script>
var ws = new WebSocket("ws://localhost:8090/websocket/a");
ws.onopen = function() {
console.log("WebSocket connection established");
};
ws.onmessage = function(event) {
document.getElementById("response").innerText = event.data;
};
ws.onclose = function() {
console.log("WebSocket connection closed");
};
function sendMessage() {
var message = document.getElementById("messageInput").value;
ws.send(message);
}
</script>
</body>
</html>
处理不同的消息
修改前端
定义一个事件注册器 config.js 文件
/**
* 发布订阅
*/
class EventDispatcher {
list = {}
constructor() {
this.list = {}
}
/**
* 注册事件
* @param event
* @param callback
*/
on = (event, callback) => {
if (!this.list[event]) {
this.list[event] = []
}
this.list[event].push(callback)
};
/**
* 发布消息
* @param event
* @param args
*/
emit = (event, ...args) => {
if (!this.list[event]) {
console.log('事件未注册', event, args);
return;
}
this.list[event].forEach((callback) => {
callback.apply(this, args)
});
};
}
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket Test</title>
<script src="./config.js"></script>
</head>
<body>
<h1>WebSocket Test</h1>
<input type="text" id="messageInput" placeholder="Enter your message">
<button onclick="sendMessage()">Send</button>
<p id="response"></p>
<script>
let dispatcher = new EventDispatcher();
// 注册 test1 事件
dispatcher.on("test1", (data) => {
console.log("test1", data)
})
// 注册 test2 事件
dispatcher.on("test2", (data) => {
console.log("test2", data)
})
var ws = new WebSocket("ws://localhost:8090/websocket/a");
ws.onopen = function() {
console.log("WebSocket connection established");
};
ws.onmessage = function(event) {
// 接收的消息类型为 {type:"", data:""}
let data = JSON.parse(event.data);
document.getElementById("response").innerText = data.data;
// 触发消息事件
dispatcher.emit(data.type, data)
};
ws.onclose = function() {
console.log("WebSocket connection closed");
};
</script>
</body>
</html>
服务发送消息
1. 定义一个消息实体类 用于接收和发送消息
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;
@Data
@ToString
@AllArgsConstructor
public class MessageWrapper<T>{
// 使用 type 区分消息
private String type;
private T data;
}
2. 调用上述 sendMessageByUser() 方法进行发生消息
MessageWrapper<String> partsFeedbackMessageWrapper = new MessageWrapper<>("test1", "发送test1类型的消息");
sendMessageByUser(id, JSONUtil.toJsonStr(partsFeedbackMessageWrapper));
partsFeedbackMessageWrapper = new MessageWrapper<>("test2", "发送test2类型的消息");
sendMessageByUser(id, JSONUtil.toJsonStr(partsFeedbackMessageWrapper));
修改服务端
前端发送消息 使用 type 定义不同的消息类型
function sendMessage() {
var message = document.getElementById("messageInput").value;
let json = {type: 'test1', data: "向test1发送消息:" + message};
ws.send(JSON.stringify(json));
json = {type: 'test2', data: "向test2发送消息:" + message};
ws.send(JSON.stringify(json));
json = {type: 'test3', data: "向test3发送消息:" + message};
ws.send(JSON.stringify(json));
json = {type: 'test4', data: "向test4发送消息:" + message};
ws.send(JSON.stringify(json));
}
1. 使用 bean 名称处理不同的消息
/**
* 接收客户端消息
*
* @param message 接收的消息
*/
@OnMessage
public void onMessage(String message) {
if(this.connectedUserWrapper!=null && this.connectedUserWrapper.getId()!=null) {
log.info("收到用户[{}]发来的消息::{}",this.connectedUserWrapper.getId(), message);
MessageWrapper messageWrapper = JSONUtil.toBean(message, MessageWrapper.class);
ListenerBean listenerBean = SpringUtil.getBean(messageWrapper.getType(), ListenerBean.class);
listenerBean.receive(messageWrapper);
}
}
// 定义一个 接口
public interface ListenerBean {
void receive(MessageWrapper messageWrapper);
}
// 不同的消息类型实现该接口 使用 不同的 bean 名称
/**
消息类型 1
*/
@Slf4j
@Service("test1")
public class Test1Listener implements ListenerBean {
@Override
public void receive(MessageWrapper messageWrapper) {
log.info("test1接收消息:{}", messageWrapper);
}
}
/**
消息类型 2
*/
@Slf4j
@Service("test2")
public class Test2Listener implements ListenerBean{
@Override
public void receive(MessageWrapper messageWrapper) {
log.info("test2接收消息:{}", messageWrapper);
}
}
2. 使用 ApplicationEventPublisher 事件监听处理不同的消息
// 1. 在 MessagePushSever 中 注入事件转发器
private static ApplicationEventPublisher applicationEventPublisher;
@Autowired
public void setSysUserService(ApplicationEventPublisher applicationEventPublisher){
MessagePushSever.applicationEventPublisher = applicationEventPublisher;
}
2. 修改 消息接收
/**
* 接收客户端消息
*
* @param message 接收的消息
*/
@OnMessage
public void onMessage(String message) {
if(this.connectedUserWrapper!=null && this.connectedUserWrapper.getId()!=null) {
log.info("收到用户[{}]发来的消息::{}",this.connectedUserWrapper.getId(), message);
MessageWrapper messageWrapper = JSONUtil.toBean(message, MessageWrapper.class);
MessagePushSever.applicationEventPublisher.publishEvent(messageWrapper);
}
}
3. 消息监听器 使用注解 @EventListener() 使用参数 condition 来监听特定消息
/**
消息监听
*/
@Slf4j
@Component
public class Test3Listener {
@EventListener(condition = "#messageWrapper.type == 'test3'")
public void receive(MessageWrapper messageWrapper) {
log.info("test3接收消息:{}", messageWrapper);
}
}
/**
消息监听
*/
@Slf4j
@Component
public class Test4Listener {
@EventListener(condition = "#messageWrapper.type == 'test4'")
public void receive(MessageWrapper messageWrapper) {
log.info("test4接收消息:{}", messageWrapper);
}
}
服务端完整代码
@ServerEndpoint("/websocket/{id}")
@Component
@Slf4j
public class MessagePushSever {
// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private ConnectedUserWrapper connectedUserWrapper;
@Data
@AllArgsConstructor
public static class ConnectedUserWrapper{
private Session session;
private String id;
}
private static ApplicationEventPublisher applicationEventPublisher;
@Autowired
public void setSysUserService(ApplicationEventPublisher applicationEventPublisher){
MessagePushSever.applicationEventPublisher = applicationEventPublisher;
}
// session集合,存放对应的session
private static ConcurrentHashMap<String, ConnectedUserWrapper> sessionPool = new ConcurrentHashMap<>();
// concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。
private static CopyOnWriteArraySet<MessagePushSever> webSocketSet = new CopyOnWriteArraySet<>();
/**
* 建立WebSocket连接
*
* @param session
*/
@OnOpen
public void onOpen(Session session,@PathParam("id")String id) {
System.out.println("on Open!");
try {
try {
ConnectedUserWrapper userWrapper = sessionPool.get(id);
// connectedUserWrapper不为空,已经有人登录相同账号,则销毁并移除
if (userWrapper != null) {
sendKidOfflineMessage(userWrapper.getSession());
userWrapper.getSession().close();
}
} catch (IOException e) {
log.error("重复登录异常,错误信息:" + e.getMessage(), e);
}
// 建立连接
ConnectedUserWrapper connectedUserWrapper = new ConnectedUserWrapper(session, id);
sessionPool.put(id, connectedUserWrapper);
this.connectedUserWrapper = connectedUserWrapper;
webSocketSet.add(this);
log.info("建立连接完成,当前在线人数为:{}", webSocketSet.size());
}catch (Exception e){
e.printStackTrace();
try {
session.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
/**
* 发送当前用户被挤掉线消息
* @param session
* @throws IOException
*/
private void sendKidOfflineMessage(Session session) throws IOException {
if(session!=null){
MessageWrapper<String> partsFeedbackMessageWrapper = new MessageWrapper<>("error", "抱歉,您的账号已在其他位置登录,如果不是您操作,请修改密码!");
session.getBasicRemote().sendText(JSONUtil.toJsonStr(partsFeedbackMessageWrapper));
}
}
/**
* 发送错误消息
* @param session
*/
private void sendNotVerifyAndClose(Session session){
try {
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 发生错误
*
* @param e
*/
@OnError
public void onError(Throwable e) {
log.error("出现错误:"+e.getMessage(),e);
}
/**
* 连接关闭
*/
@OnClose
public void onClose() {
webSocketSet.remove(this);
if(this.connectedUserWrapper!=null && this.connectedUserWrapper.getId()!=null){
//避免挤掉他人时,上面已经移除掉了,导致把刚登陆的账号也移除掉了,
String id = this.connectedUserWrapper.getId();
sessionPool.remove(id);
log.info("下线用户:{},剩余在线人数:{}人",id,webSocketSet.size());
}
}
/**
* 接收客户端消息
*
* @param message 接收的消息
*/
@OnMessage
public void onMessage(String message) {
if(this.connectedUserWrapper!=null && this.connectedUserWrapper.getId()!=null) {
log.info("收到用户[{}]发来的消息::{}",this.connectedUserWrapper.getId(), message);
MessageWrapper messageWrapper = JSONUtil.toBean(message, MessageWrapper.class);
MessagePushSever.applicationEventPublisher.publishEvent(messageWrapper);
}
}
public static List<String> getOnlineUser(){
return sessionPool.values().stream().map(ConnectedUserWrapper::getId).collect(Collectors.toList());
}
public static ConcurrentHashMap<String, ConnectedUserWrapper> getOnlineUserMap(){
return sessionPool;
}
/**
* 推送消息到指定用户
*
* @param userId 用户ID
* @param message 发送的消息
*/
public static void sendMessageByUser(String userId, String message) {
log.info("用户ID:" + userId + ",推送内容:" + message);
ConnectedUserWrapper connectedUserWrapper = sessionPool.get(userId);
if(connectedUserWrapper!=null) {
try {
connectedUserWrapper.getSession().getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("推送消息到指定用户发生错误:" + e.getMessage(), e);
}
}
}
/**
* 群发消息
*
* @param message 发送的消息
*/
public static void sendAllMessage(String message) {
log.info("发送消息:{}", message);
for (MessagePushSever webSocket : webSocketSet) {
try {
webSocket.connectedUserWrapper.getSession().getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("群发消息发生错误:" + e.getMessage(), e);
}
}
}
}
参考:
SpringBoot整合WebSocket(session共享实现)_websocket session-优快云博客