springboot集成websocket和几种方式的个人见解
一、说明
最近没什么事,写下springboot集成websocket。记得之前要用的时候随便搜了下,相关文章很多,但是每个都不太一样,给我整的很疑惑。
最后找到篇文章才算是说的比较清楚,原来是有很多种方式。
参考文档:https://juejin.cn/post/6844903976727494669
http://www.mydlq.club/article/86/
spring-boot 集成websocket 常见方式:
1、原生jdk注解。 太原生了,功能支持很少。用着不太方便。
2、spring封装。简单封装,消息处理基本与netty一致。本文使用这种方式。
3、spring封装STOMP。感觉有点过渡封装了。
4、还有一些其他的方式,如netty、tio等,这种与spring-boot基本没啥关系,不能复用spring-boot的http端口(或者我不知道)。
二、使用
1、maven引用
spring-boot基础依赖这里不贴了
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
2、配置
端口:使用的是 spring-boot的内嵌的tomcat配置的端口,application.properties 中的“server.port=9090”。
添加个配置类:
testWs 是websocket路径,前端请求地址举例:ws://127.0.0.1:9090/testWs
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; @EnableWebSocket @Configuration public class WebSocketConfig implements WebSocketConfigurer { @Autowired private WebSocketHandler webSocketHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(webSocketHandler, "testWs") //跨域忽略 .setAllowedOrigins("*"); } }
3、WebSocketHandler 实现类,业务处理类
这里只演示文本类型消息。
基本与netty的操作方式一致。如果想在项目中主动给客户端写消息,可以将session缓存到map里,然后需要发消息时取出session进行发送。
import cn.hutool.core.lang.Console; import org.jetbrains.annotations.NotNull; 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; /** * websocket 连接和消息处理,这里继承TextWebSocketHandler,只处理文本消息。 **/ @Component public class MsgHandler extends TextWebSocketHandler { /** * 接收消息 */ @Override protected void handleTextMessage(@NotNull WebSocketSession session, TextMessage message) throws Exception { String msg = message.getPayload(); Console.log("收到消息:{}",msg); //这里只是演示下如何给客户端写消息。 session.sendMessage(new TextMessage("我收到你的消息了")); } /** * 连接成功 */ @Override public void afterConnectionEstablished(WebSocketSession session) { Console.log("连接connected:{}", session.getRemoteAddress()); } /** * 连接关闭 */ @Override public void afterConnectionClosed(WebSocketSession session, @NotNull CloseStatus status) { Console.log("关闭close:{},status:{}", session.getRemoteAddress(), status.toString()); } /** * 异常处理 */ @Override public void handleTransportError(WebSocketSession session, Throwable exception) { Console.log(exception.getCause(), "异常error:{}", session.getRemoteAddress()); } }
4、粘一个写的演示基本功能的WebSocketHandler
功能主要有:登录验证,session缓存,超时断连(要求前端每隔10s发送心跳,否则超过30s无消息将断连)。
业务处理类:
import cn.hutool.core.date.SystemClock; import cn.hutool.log.Log; import cn.sanenen.demo.common.Constant; import cn.sanenen.demo.entity.bean.UserBean; import cn.sanenen.utils.other.Emptys; import org.jetbrains.annotations.NotNull; 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.util.concurrent.atomic.AtomicInteger; /** * websocket 连接和消息处理,这里继承TextWebSocketHandler,只处理文本消息。 **/ @Component public class MsgHandler extends TextWebSocketHandler { private static final Log log = Log.get(); private static final AtomicInteger userId = new AtomicInteger(0); /** * 接收消息 */ @Override protected void handleTextMessage(@NotNull WebSocketSession session, TextMessage message) throws Exception { String msg = message.getPayload(); String resp = Emptys.STR; try { Object o = session.getAttributes().putIfAbsent(Constant.SESSION_RESPONSE, 1); //上次请求响应未返回,禁止请求。 if (o != null) { resp = "上次请求为返回,禁止请求"; } else { Object seatObj = session.getAttributes().get(Constant.SESSION_USER_KEY); //未登录 一般连接建立成功后的第一条消息都为 验证消息。 if (seatObj == null) { //就当是验证通过了,放入缓存。 UserBean userBean = new UserBean(); userBean.setId(userId.incrementAndGet()); session.getAttributes().put(Constant.SESSION_USER_KEY, userBean); //这里如果想限制用户已存在,不允许重复登录,可以使用putIfAbsent WebSocketSession webSocketSession = WebSocketMap.put(userBean.getId(), session); if (webSocketSession != null) { webSocketSession.sendMessage(new TextMessage("你被顶掉了。")); webSocketSession.getAttributes().clear(); webSocketSession.close(); } resp = "登录成功喽"; } else { //这里进行业务处理 //----- resp = "我是业务处理完的响应噢"; } session.getAttributes().put(Constant.SESSION_TIMEOUT, SystemClock.now()); session.getAttributes().remove(Constant.SESSION_RESPONSE); } session.sendMessage(new TextMessage(resp)); } catch (Exception e) { resp = "系统异常"; log.error(e); } finally { log.info("ip:{},request:{},resp:{}", session.getRemoteAddress(), msg, resp); } } /** * 连接成功 */ @Override public void afterConnectionEstablished(WebSocketSession session) { log.info("连接connected:{}", session.getRemoteAddress()); } /** * 连接关闭 */ @Override public void afterConnectionClosed(WebSocketSession session, @NotNull CloseStatus status) { UserBean user = (UserBean) session.getAttributes().get(Constant.SESSION_USER_KEY); if (user != null) { //做一些连接断开后的操作。 WebSocketMap.remove(user.getId()); } log.info("关闭close:{},status:{},user:{}", session.getRemoteAddress(), status.toString(), user); } /** * 异常处理 */ @Override public void handleTransportError(WebSocketSession session, Throwable exception) { log.error(exception.getCause(), "异常error:{}", session.getRemoteAddress()); } }
session缓存map,附带超时处理逻辑。
import cn.hutool.core.date.SystemClock; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.log.Log; import cn.sanenen.demo.common.Constant; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import java.io.IOException; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * 连接缓存,便于给前端推送消息 **/ public class WebSocketMap { private static final Map<Integer, WebSocketSession> MAP = new ConcurrentHashMap<>(); private static final Log log = Log.get(); static { new Thread(() -> { while (true) { try { Iterator<Map.Entry<Integer, WebSocketSession>> iterator = MAP.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<Integer, WebSocketSession> entry = iterator.next(); //最后一次收到消息的时间 Long endReadTime = (Long) entry.getValue().getAttributes().get(Constant.SESSION_TIMEOUT); if (endReadTime == null){ endReadTime = 0L; } long timeOut = SystemClock.now() - endReadTime; //超过30秒,断开连接。 if (timeOut > 30000) { WebSocketSession value = entry.getValue(); if (value != null && value.isOpen()) { Object o = value.getAttributes().get(Constant.SESSION_USER_KEY); log.info("sessionTimeoutClose:ip:{}:{}", value.getRemoteAddress(), o); value.close(); } iterator.remove(); } } ThreadUtil.sleep(10000); } catch (Exception e) { log.error(e); ThreadUtil.sleep(10000); } } }).start(); } /** * 添加 session */ public static WebSocketSession put(Integer jobId, WebSocketSession session) { return MAP.put(jobId, session); } /** * 删除 session,会返回删除的 session */ public static WebSocketSession remove(Integer jobId) { // 删除 session return MAP.remove(jobId); } /** * 删除并同步关闭连接 */ public static void removeAndClose(Integer jobId) { WebSocketSession session = remove(jobId); if (session != null && session.isOpen()) { try { session.close(); } catch (IOException e) { log.error(e); } } } /** * 获得 session */ public static WebSocketSession get(Integer jobId) { return MAP.get(jobId); } /** * 给指定用户发送消息 */ public static void send(Integer jobId, String msg) throws IOException { log.debug("sendEvent:{},{}", jobId, msg); WebSocketSession session = MAP.get(jobId); if (session != null && session.isOpen()) { session.sendMessage(new TextMessage(msg)); } else { log.info("session is null or closed:{},msg:{}", jobId, msg); } } /** * 给所有用户发送消息 public static void sendAll(String msg) throws IOException { for (WebSocketSession session : MAP.values()) { if (session != null && session.isOpen()) { session.sendMessage(new TextMessage(msg)); } } }*/ }
MsgHandler