一、说明
最近没什么事,写下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));
}
}
}*/
}