在 SpringBoot 中使用原生注解简易集成 websocket 可参考:
Spring Boot 2.2 原生注解简易集成 websocket
本文将介绍使用 Spring 封装的注解简易集成 websocket
./demo-websocket-spring/pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>${hutool.version}version>
dependency>
其中 springboot 版本为 2.2.5.RELEASE
, ${hutool.version}
版本为 5.2.3
./demo-websocket-spring/src/main/resources/application.yml
## config
## server
server:
port: 8201
./demo-websocket-spring/src/main/java/com/ljq/demo/springboot/websocketspring/web/SocketSessionManager.java
package com.ljq.demo.springboot.websocketspring.web;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Description: websocket 会话管理
* @Author: junqiang.lu
* @Date: 2020/3/19
*/
@Slf4j
public class SocketSessionManager {
/**
* websocket 会话池
*/
private static ConcurrentHashMap<String, WebSocketSession> webSocketSessionMap = new ConcurrentHashMap<>();
/**
* 添加 websocket 会话
*
* @param key
* @param session
*/
public static void add(String key, WebSocketSession session) {
webSocketSessionMap.put(key, session);
}
/**
* 移除 websocket 会话,并将该会话内容返回
*
* @param key
* @return
*/
public static WebSocketSession remove(String key) {
return webSocketSessionMap.remove(key);
}
/**
* 删除 websocket,并关闭连接
*
* @param key
*/
public static void removeAndClose(String key) {
WebSocketSession session = remove(key);
if (session != null) {
try {
session.close();
} catch (IOException e) {
log.error("Websocket session close exception ",e);
}
}
}
/**
* 获取 websocket 会话
*
* @param key
* @return
*/
public static WebSocketSession get(String key) {
return webSocketSessionMap.get(key);
}
/**
* 获取会话数量
*
* @return
*/
public static int count() {
return webSocketSessionMap.size();
}
}
在进行 websocket 握手成功之前进行身份验证
./demo-websocket-spring/src/main/java/com/ljq/demo/springboot/websocketspring/interceptor/SocketInterceptor.java
package com.ljq.demo.springboot.websocketspring.interceptor;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: websocket 握手拦截器
* @Author: junqiang.lu
* @Date: 2020/3/19
*/
@Slf4j
@Component
public class SocketInterceptor implements HandshakeInterceptor {
private static final String TOKEN_FIELD = "token";
/**
* websocket 握手之前
*
* @param serverHttpRequest
* @param serverHttpResponse
* @param webSocketHandler
* @param map
* @return
* @throws Exception
*/
@Override
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse,
WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
log.debug("websocket starts handshaking");
// 获取请求参数
HashMap<String, String> paramMap = HttpUtil.decodeParamMap(serverHttpRequest.getURI().getQuery(), "utf-8");
String token = paramMap.get(TOKEN_FIELD);
if (StrUtil.isNotBlank(token)) {
map.put("token", token);
log.debug("用户 [ {} ]握手成功", token);
return true;
}
log.debug("用户登录已失效");
return false;
}
/**
* websocket 握手之后
*
* @param serverHttpRequest
* @param serverHttpResponse
* @param webSocketHandler
* @param e
*/
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse,
WebSocketHandler webSocketHandler, Exception e) {
log.debug("握手完成!");
}
}
在 websocket 握手成功之后,管理已经创建的 websocket 连接
./demo-websocket-spring/src/main/java/com/ljq/demo/springboot/websocketspring/interceptor/SocketAuthHandler.java
package com.ljq.demo.springboot.websocketspring.interceptor;
import com.ljq.demo.springboot.websocketspring.web.SocketSessionManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.time.LocalDateTime;
import java.util.Objects;
/**
* @Description: websocket 连接拦截器
* @Author: junqiang.lu
* @Date: 2020/3/19
*/
@Slf4j
@Component
public class SocketAuthHandler extends TextWebSocketHandler {
private static final String TOKEN_FIELD = "token";
/**
* 握手成功之后
*
* @param session
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
Object token = session.getAttributes().get(TOKEN_FIELD);
if (Objects.nonNull(token)) {
// 用户连接成功,缓存用户会话
log.debug("用户[ {} ]创建连接", token);
SocketSessionManager.add(String.valueOf(token), session);
} else {
throw new RuntimeException("用户登录已失效");
}
}
/**
* 接收客户端消息
*
* @param session
* @param message
* @throws Exception
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 读取客户端消息
Object token = session.getAttributes().get(TOKEN_FIELD);
String payload = message.getPayload();
log.debug("收到用户 [{}] 的消息,消息内容为: {}",token, payload);
StringBuilder responseBuilder = new StringBuilder();
responseBuilder.append("服务端已接收到用户 [").append(token).append("] 的消息,消息内容为:");
responseBuilder.append(payload).append(",当前服务器时间: ");
responseBuilder.append(LocalDateTime.now());
session.sendMessage(new TextMessage(responseBuilder.toString()));
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
Object token = session.getAttributes().get(TOKEN_FIELD);
if (Objects.nonNull(token)) {
log.debug("用户 [{}] 断开连接", token);
SocketSessionManager.remove(String.valueOf(token));
}
}
}
方法说明:
afterConnectionEstablished()
方法是在 websocket 握手成功之后,创建 socket 连接的时候触发,等价于 socket 的原生注解 @OnOpen
handleTextMessage()
方法是在客户端向服务端发送文本消息的时候触发,等价于 socket 的原生注解 @OnMessage
。Spring 封装的还有 handleMessage()
方法,所有客户端发送消息都会触发该方法,无论什么类型的数据。
afterConnectionClosed()
方法是在客户端(请求)断开连接的时候触发,等价于 socket 的原生注解 @OnClose
./demo-websocket-spring/src/main/java/com/ljq/demo/springboot/websocketspring/interceptor/SocketConfig.java
package com.ljq.demo.springboot.websocketspring.interceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
/**
* @Description: websocket 拦截器配置
* @Author: junqiang.lu
* @Date: 2020/3/19
*/
@Configuration
@EnableWebSocket
public class SocketConfig implements WebSocketConfigurer {
@Autowired
private SocketAuthHandler socketAuthHandler;
@Autowired
private SocketInterceptor socketInterceptor;
private static final String WEB_SOCKET_PATH = "socketSpring";
/**
*
* @param webSocketHandlerRegistry
*/
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
webSocketHandlerRegistry.addHandler(socketAuthHandler, WEB_SOCKET_PATH)
.addInterceptors(socketInterceptor)
.setAllowedOrigins("*");
}
}
./demo-websocket-spring/src/main/java/com/ljq/demo/springboot/websocketspring/DemoWebsocketSpringApplication.java
package com.ljq.demo.springboot.websocketspring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author junqiang.lu
*/
@SpringBootApplication(scanBasePackages = {"com.ljq.demo.springboot"})
public class DemoWebsocketSpringApplication {
public static void main(String[] args) {
SpringApplication.run(DemoWebsocketSpringApplication.class, args);
}
}
在线 websocket 测试地址:
WebSocket在线测试工具
在线WebSocket测试工具
启动项目,在测试网站输入 websocket 地址:
ws://127.0.0.1:8201/socketSpring?token=demo1
127.0.0.1
为本机 ip 地址,可根据需要自行更改 ip 地址
demo1
为用户名,测试时可自行更换
连接成功,无返回结果,控制台日志:
2020-03-20 15:00:30 | DEBUG | http-nio-8201-exec-5 | c.l.d.s.w.interceptor.SocketInterceptor 39| websocket starts handshaking
2020-03-20 15:00:30 | DEBUG | http-nio-8201-exec-5 | c.l.d.s.w.interceptor.SocketInterceptor 45| 用户 [ demo1 ]握手成功
2020-03-20 15:00:30 | DEBUG | http-nio-8201-exec-5 | c.l.d.s.w.interceptor.SocketInterceptor 63| 握手完成!
2020-03-20 15:00:30 | DEBUG | http-nio-8201-exec-5 | c.l.d.s.w.interceptor.SocketAuthHandler 36| 用户[ demo1 ]创建连接
发送消息,消息内容为:
55555
返回结果为:
服务端已接收到用户 [demo1] 的消息,消息内容为:55555,当前服务器时间: 2020-03-20T15:01:20.990
控制台日志为:
2020-03-20 15:01:20 | DEBUG | http-nio-8201-exec-6 | c.l.d.s.w.interceptor.SocketAuthHandler 55| 收到用户 [demo1] 的消息,消息内容为: 55555
断开连接,无返回结果,控制台日志为:
2020-03-20 15:03:08 | DEBUG | http-nio-8201-exec-7 | c.l.d.s.w.interceptor.SocketAuthHandler 69| 用户 [demo1] 断开连接
至此,在 SpringBoot 中使用 Spring 封装注解简易集成 websocket 已实现
A Guide to the Java API for WebSocket
Spring Boot系列十六 WebSocket简介和spring boot集成简单消息代理
【websocket】spring boot 集成 websocket 的四种方式
WebSocket在线测试工具
在线WebSocket测试工具
Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo
个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.