SpringBoot+Websocket两种实现方式
在学习使用中,记录此文,避免踩坑,如有不好见谅。
1、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2、yml
spring:
thymeleaf:
prefix: classpath:/templates/
cache: false
3、原始实现方式
(1)新建websocket配置类
@Configuration
@EnableWebSocket
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
(2)新建websocket处理类
@Component
@ServerEndpoint("/websocket/{clientId}")
public class MyWebSocket {
private Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 在线人数
*/
public static int onlineNumber = 0;
/**
* 以id为key,MyWebSocket为对象保存起来
*/
private static Map<Integer, MyWebSocket> clients = new ConcurrentHashMap<>();
/**
* 会话
*/
private Session session;
/**
* 用户名称
*/
private int clientId;
/**
* 建立连接
*/
@OnOpen
public void onOpen(@PathParam(value = "clientId") int clientId, Session session) {
onlineNumber++;
this.clientId = clientId;
this.session = session;
logger.info("有新连接加入!当前在线人数" + onlineNumber);
}
@OnError
public void onError(Throwable error) {
logger.info("服务端发生了错误" + error.getMessage());
}
/**
* 连接关闭
*/
@OnClose
public void onClose() {
onlineNumber--;
logger.info("有连接关闭! 当前在线人数" + onlineNumber);
}
/**
* 收到客户端的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
}
public void sendMessageTo(String message, int ToTableId) throws IOException {
for (MyWebSocket item : clients.values()) {
if (item.clientId == ToTableId) {
item.session.getAsyncRemote().sendText(message);
break;
}
}
}
public void sendMessageAll(String message) throws IOException {
for (MyWebSocket item : clients.values()) {
item.session.getAsyncRemote().sendText(message);
}
}
public static synchronized int getOnlineCount() {
return onlineNumber;
}
}
(3)新建socket.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>websocket</title>
<script type="text/javascript" src="https://ajax.microsoft.com/ajax/jquery/jquery-1.4.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
</head>
<body>
<div style="margin: auto;text-align: center">
<h1>Welcome to websocket</h1>
</div>
<br/>
<br>
<div style="margin-right: 10px;text-align: right">
<button οnclick="closeWebSocket()">关闭连接</button>
</div>
<hr/>
<div id="message" style="text-align: center;"></div>
<input type="text" th:value="${clientId}" id="clientId" style="display: none" />
</body>
<script type="text/javascript">
var webSocket;
if ("WebSocket" in window)
{
var clientId = document.getElementById('clientId').value;
webSocket = new WebSocket("ws://localhost:8080/websocket/"+clientId);
//连通之后的回调事件
webSocket.onopen = function()
{
console.log("已经连通了websocket");
};
//接收后台服务端的消息
webSocket.onmessage = function (evt)
{
var received_msg = evt.data;
console.log("数据已接收:" +received_msg);
var obj = JSON.parse(received_msg);
console.log("可以解析成json:"+obj.messageType);
};
//连接关闭的回调事件
webSocket.onclose = function()
{
console.log("连接已关闭...");
};
}
else{
// 浏览器不支持 WebSocket
alert("您的浏览器不支持 WebSocket!");
}
function closeWebSocket() {
//直接关闭websocket的连接
webSocket.close();
}
</script>
</html>
(4)新建测试类
@Controller
public class SocketController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@RequestMapping("/websocket/{tableId}")
public String webSocket(@PathVariable String tableId, Model model){
try{
logger.info("跳转到websocket的页面上");
//通过Model进行对象数据的传递
model.addAttribute("clientId",tableId);
return "socket";
}
catch (Exception e){
logger.info("跳转到websocket的页面上发生异常,异常信息是:"+e.getMessage());
return "error";
}
}
}
(5)运行测试
到这,第一种方式已连接成功。
4、第二种方式(spring)
(1)新建websocket配置类
@Component
@EnableWebSocket
public class OtherWebSocketConfig implements WebSocketConfigurer {
@Autowired
private MyWebSocketHandler myWebSocketHandler;
@Autowired
private MyWebSocketInterceptor myWebSocketInterceptor;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myWebSocketHandler, "/websocket/{clientId}")
.setAllowedOrigins("*")
.addInterceptors(myWebSocketInterceptor);
}
}
注意:
如果使用第一种方式中的socket.html,使用此处的websocket配置类,连接会失败,浏览器将会报错200。如下:
registry.addHandler(myWebSocketHandler, "/websocket/{clientId}")
在此处的处理中,websocket消息处理器无法像第一种方式的
@ServerEndpoint("/websocket/{clientId}")
处理路径中的参数。所以导致socket.html中访问websocket链接的ws无法和配置类中要检测的路径相匹配,导致连接失败。
webSocket = new WebSocket("ws://localhost:8080/websocket/"+clientId);
所以此处要处理配置类的路径如下:
还有处理socket.html访问websocket路径如下:
这样访问就可以成功。在这个问题上踩坑严重,所以特地记录一下。
(2)新建websocket消息处理器
@Component
public class MyWebSocketHandler implements WebSocketHandler {
private static final Map<String, WebSocketSession> SESSIONS = new ConcurrentHashMap<>();
/**
* 建立连接成功后回调
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
SESSIONS.put(session.getId(), session);
System.out.println(String.format("成功建立连接~ tableId: %s", session.getId()));
}
/**
* 接收客户端发送的Socket
*/
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> webSocketMessage) throws Exception {
String msg = webSocketMessage.getPayload().toString();
System.out.println(msg);
}
/**
* 连接出错回调
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable throwable) throws Exception {
System.out.println("连接出错");
if (session.isOpen()) {
session.close();
}
}
/**
* 连接关闭回调
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
System.out.println("连接已关闭,status:" + closeStatus);
}
@Override
public boolean supportsPartialMessages() {
return false;
}
}
(3)websocket自定义拦截器
@Component
public class MyWebSocketInterceptor implements HandshakeInterceptor {
/**
* 前置拦截用来注册用户信息,绑定 WebSocketSession
*/
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
System.out.println("前置拦截~~");
if (!(request instanceof ServletServerHttpRequest)) {
return true;
}
//HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
//String clientId = (String) servletRequest.getSession().getAttribute("clientId");
//attributes.put("clientId", clientId);
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
System.out.println("后置拦截~~");
}
}
到此,第二种方式结束。
因为在第二种方式无法直接在配置中加入参数,那么就考虑到传参,处理参数问题,这个可以在websocket的自定义拦截器中,在前置拦截中通过request获取参数。
代码已上传github:https://github.com/liwangC/springboot