1. 增加 ape-common-websocket 模块
2. 引入相关依赖
<!-- websocket相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.4.2</version>
</dependency>
3. 定义WebSocketConfig配置类
说明: 只要使用WebSocket,就需要创建一个WebSocketConfig配置类,这样就可以自动启动一个WebSocket服务!
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
4. WebSocketCheckConfig鉴权配置类
说明: 业务场景中可能不能什么用户都可以进行访问WebSocket,所以需要进行鉴权和配置相关信息的功能!
@Component
public class WebSocketServerConfig extends ServerEndpointConfig.Configurator {
@Override
public boolean checkOrigin(String originHeaderValue) { //实现鉴权效果,要重写方法
//获取websocket的request对象
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//获取http的request对象
HttpServletRequest request = servletRequestAttributes.getRequest();
//校验逻辑
return true; //校验通过
}
@Override
public void modifyHandshake(ServerEndpointConfig sec,
HandshakeRequest request, HandshakeResponse response) {
Map<String, List<String>> parameterMap = request.getParameterMap(); //获取WebSocket的所有请求参数
List<String> erpList = parameterMap.get("erp"); //获取前端传递的erp相关参数信息(作为,每个连接的唯一标识 url?erp=xx)
if(!CollectionUtils.isEmpty(erpList)) {
//使用ServerEndpointConfig来进行相应的参数传递(此处将erp相关参数信息作为用户参数传递)
sec.getUserProperties().put("erp", erpList.get(0)); //get(0), 一般请求参数只对应一个值
}
}
}
5. 业务模块使用
@Component
@Slf4j
@ServerEndpoint(value = "/ssm/socket", configurator = WebSocketServerConfig.class)
//它将这个类声明为一个WebSocket端点,指定了连接的URL路径为 ws://localhost:8080 + "/ssm/socket"
// configurator提供配置 WebSocket 端点的自定义配置器
public class ssmWebSocket {
/**
* 注意:标注 @ServerEndpoint 注解的类对象是多例的,即每个连接都会创建一个新的对象
*
* @OnOpen:当 WebSocket 连接建立时,会调用标注有 @onOpen 的方法
*
* @OnClose:当 WebSocket 连接关闭时,会调用标注有 @OnClose 的方法
*
* @OnMessage:当收到客户端发送的消息时,会调用标注有 @OnMessage 的方法
*
* @OnError:当出现错误时,会调用标注有 @OnError 的方法
*
*/
/**
* 记录当前在线的连接数
* 提供原子操作的Integer类,通过线程安全的方式操作加减。防止同时上线时,只加1。
*/
private static AtomicInteger onlineCount = new AtomicInteger(0);
/**
* 存放所有的在线客户端
* HashMap线程不安全,ConcurrentHashMap线程安全
*/
private static Map<String, ssmWebSocket> clients = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 当前会话的唯一标识key
*/
private String erp = "";
/**
* 连接建立成功调用的方法
* @param session
* @param config EndpointConfig类可获取前端传递的参数信息
* @throws IOException
*/
@OnOpen
public void onOpen(Session session, EndpointConfig config) throws IOException {
try {
Map<String, Object> userProperties = config.getUserProperties(); //获取WebSocket的所有请求参数
String erp = (String) userProperties.get("erp"); //获取前端传递的erp参数
this.erp = erp;
this.session = session;
if(clients.containsKey(this.erp)) { //当前erp已建立连接,将旧连接关闭
clients.get(this.erp).session.close();
clients.remove(this.erp); //关闭连接后,再从集合中删除
onlineCount.decrementAndGet(); //在线人数减一
}
clients.put(this.erp, this); //将新连接加入集合
onlineCount.incrementAndGet(); //在线人数加一
log.info("有新连接加入:{},当前在线人数:{}", erp, onlineCount.get());
sendMessage("连接成功", session); //告诉前端连接成功了
} catch (Exception e) {
log.error("建立链接错误{}", e.getMessage(), e);
}
}
/**
*
* @param message 给前端发送什么消息
* @param session 往哪个连接中发消息
*/
public void sendMessage(String message, Session session) throws IOException {
log.info("服务端给客户端[{}]发送消息:{}", this.erp, message);
session.getBasicRemote().sendText(message);
}
/**
* 当WebSocket连接关闭时,调用的方法
*/
@OnClose
public void onClose() throws IOException {
try {
if(clients.containsKey(this.erp)) { //如果当前erp还未关闭连接,就手动关闭
clients.get(this.erp).session.close();
clients.remove(this.erp);
onlineCount.decrementAndGet();
}
log.info("有一处连接关闭{},当前在线人数:{}", this.erp, onlineCount.get());
} catch (Exception e) {
log.error("建立链接错误{}", e.getMessage(), e);
}
}
/**
*当出现错误时,会调用的方法
*/
@OnError
public void onError(Session session, Throwable throwable) {
log.error("websocket:{},发送错误,错误原因:{}", erp, throwable.getMessage(), throwable);
try {
session.close();
} catch (Exception e) {
log.error("onError.Exception{}", e.getMessage(), e);
}
}
/**
*群发消息,为每一个客户端发送消息
*/
public void sendMessages(String message) throws IOException {
//遍历所有在线的客户端
for(Map.Entry<String, ssmWebSocket> sessionEntry : clients.entrySet()) {
String erp = sessionEntry.getKey();
ssmWebSocket webSocket = sessionEntry.getValue();
Session session = webSocket.session; //获取当前客户端的session
log.info("服务端给客户端[{}]发送消息{}", erp, message);
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("{}发送消息发生异常,异常原因{}", this.erp, message);
}
}
}
/**
* 当收到客户端发送的消息时,会调用的方法
*/
@OnMessage
public void onMessage(String message, Session session) throws IOException {
log.info("服务端收到客户端[{}]发送的消息:{}", this.erp, message);
// 模拟心跳机制:
// 前端可以通过setInterval定时任务每个15秒钟调用一次reconnect函数
// reconnect会通过socket.readyState来判断这个websocket连接是否正常
// 如果不正常就会触发定时连接,每15s钟重试一次,直到连接成功
if(message.equals("ping")) { //如果收到客户端发送的ping
this.sendMessage("pong", session); //服务端返回一个pong,来不断重连,不会超时断连
}
}
}