feat: 添加 spring-boot-websocket starter.

This commit is contained in:
ljh01459796
2023-06-25 20:30:56 +08:00
parent 22aefbf7c5
commit 176f3d9a3f
14 changed files with 558 additions and 1 deletions

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.orion.ops</groupId>
<artifactId>orion-ops-framework</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>orion-ops-spring-boot-starter-websocket</artifactId>
<name>${project.artifactId}</name>
<packaging>jar</packaging>
<description>项目 websocket 配置包</description>
<url>https://github.com/lijiahangmax/orion-ops-pro</url>
<dependencies>
<dependency>
<groupId>com.orion.ops</groupId>
<artifactId>orion-ops-common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,44 @@
package com.orion.ops.framework.websocket.config;
import com.orion.ops.framework.websocket.core.WebsocketContainerConfig;
import com.orion.ops.framework.websocket.interceptor.UserHandshakeInterceptor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
/**
* websocket 配置类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/6/25 19:45
*/
@EnableWebSocket
@AutoConfiguration
@EnableConfigurationProperties(WebsocketContainerConfig.class)
public class OrionWebsocketAutoConfiguration {
/**
* @return websocket 缓冲区大小配置
*/
@Bean
public ServletServerContainerFactoryBean servletServerContainerFactoryBean(WebsocketContainerConfig config) {
ServletServerContainerFactoryBean factory = new ServletServerContainerFactoryBean();
factory.setMaxBinaryMessageBufferSize(config.getBinaryBufferSize());
factory.setMaxTextMessageBufferSize(config.getBinaryBufferSize());
factory.setMaxSessionIdleTimeout(config.getSessionIdleTimeout());
return factory;
}
/**
* @return 用户认证拦截器 按需注入
*/
@Bean
public HandshakeInterceptor userHandshakeInterceptor() {
return new UserHandshakeInterceptor();
}
}

View File

@@ -0,0 +1,22 @@
package com.orion.ops.framework.websocket.constant;
/**
* websocket 属性
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/6/25 20:25
*/
public interface WsAttr {
String USER = "user";
String UID = "uid";
String TOKEN = "token";
String READONLY = "readonly";
String CONNECTED = "connected";
}

View File

@@ -0,0 +1,137 @@
package com.orion.ops.framework.websocket.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* ws服务端关闭code
*
* @author Jiahang Li
* @version 1.0.0
* @since 2021/6/16 15:18
*/
@AllArgsConstructor
@Getter
public enum WsCloseCode {
/**
* 未查询到token
*/
INCORRECT_TOKEN(4100, WsCloseReason.CLOSED_CONNECTION),
/**
* 伪造token
*/
FORGE_TOKEN(4120, WsCloseReason.CLOSED_CONNECTION),
/**
* token已被绑定
*/
TOKEN_BIND(4125, WsCloseReason.CLOSED_CONNECTION),
/**
* 未知的连接
*/
UNKNOWN_CONNECT(4130, WsCloseReason.CLOSED_CONNECTION),
/**
* 认证失败 id不匹配
*/
IDENTITY_MISMATCH(4140, WsCloseReason.IDENTITY_MISMATCH),
/**
* 认证信息不匹配
*/
VALID(4150, WsCloseReason.AUTHENTICATION_FAILURE),
/**
* 机器不合法
*/
INVALID_MACHINE(4200, WsCloseReason.CLOSED_CONNECTION),
/**
* 连接远程服务器连接超时
*/
CONNECTION_TIMEOUT(4201, WsCloseReason.CONNECTION_TIMEOUT),
/**
* 连接远程服务器失败
*/
CONNECTION_FAILURE(4202, WsCloseReason.REMOTE_SERVER_UNREACHABLE),
/**
* 远程服务器认证失败
*/
CONNECTION_AUTH_FAILURE(4205, WsCloseReason.REMOTE_SERVER_AUTHENTICATION_FAILURE),
/**
* 远程服务器认证出现异常
*/
CONNECTION_EXCEPTION(4210, WsCloseReason.UNABLE_TO_CONNECT_REMOTE_SERVER),
/**
* 机器未启用
*/
MACHINE_DISABLED(4215, WsCloseReason.MACHINE_DISABLED),
/**
* 打开shell出现异常
*/
OPEN_SHELL_EXCEPTION(4220, WsCloseReason.UNABLE_TO_CONNECT_REMOTE_SERVER),
/**
* 打开command出现异常
*/
OPEN_COMMAND_EXCEPTION(4225, WsCloseReason.UNABLE_TO_CONNECT_REMOTE_SERVER),
/**
* 打开sftp出现异常
*/
OPEN_SFTP_EXCEPTION(4230, WsCloseReason.UNABLE_TO_CONNECT_REMOTE_SERVER),
/**
* 服务出现异常
*/
RUNTIME_EXCEPTION(4300, WsCloseReason.CLOSED_CONNECTION),
/**
* 心跳结束
*/
HEART_DOWN(4310, WsCloseReason.CLOSED_CONNECTION),
/**
* 用户关闭
*/
DISCONNECT(4320, WsCloseReason.CLOSED_CONNECTION),
/**
* 结束
*/
EOF(4330, WsCloseReason.CLOSED_CONNECTION),
/**
* 读取失败
*/
READ_EXCEPTION(4335, WsCloseReason.CLOSED_CONNECTION),
/**
* 强制下线
*/
FORCED_OFFLINE(4500, WsCloseReason.FORCED_OFFLINE),
;
private final int code;
private final String reason;
public static WsCloseCode of(int code) {
for (WsCloseCode value : values()) {
if (value.code == code) {
return value;
}
}
return null;
}
}

View File

@@ -0,0 +1,30 @@
package com.orion.ops.framework.websocket.constant;
/**
* ws服务端关闭reason
*
* @author Jiahang Li
* @version 1.0.0
* @since 2021/6/16 15:21
*/
public interface WsCloseReason {
String CLOSED_CONNECTION = "closed connection...";
String IDENTITY_MISMATCH = "identity mismatch...";
String AUTHENTICATION_FAILURE = "authentication failure...";
String REMOTE_SERVER_UNREACHABLE = "remote server unreachable...";
String CONNECTION_TIMEOUT = "connection timeout...";
String REMOTE_SERVER_AUTHENTICATION_FAILURE = "remote server authentication failure...";
String MACHINE_DISABLED = "machine disabled...";
String UNABLE_TO_CONNECT_REMOTE_SERVER = "unable to connect remote server...";
String FORCED_OFFLINE = "forced offline...";
}

View File

@@ -0,0 +1,79 @@
package com.orion.ops.framework.websocket.constant;
import com.orion.lang.utils.Exceptions;
import com.orion.lang.utils.Strings;
import com.orion.lang.utils.Valid;
import lombok.AllArgsConstructor;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* ws服务端响应常量
*
* @author Jiahang Li
* @version 1.0.0
* @since 2021/4/16 21:48
*/
@AllArgsConstructor
public enum WsProtocol {
/**
* 正常返回
*/
OK("0"),
/**
* 连接成功
*/
CONNECTED("1"),
/**
* ping
*/
PING("2"),
/**
* pong
*/
PONG("3"),
/**
* 未知操作
*/
ERROR("4"),
;
private final String code;
/**
* 分隔符
*/
public static final String SYMBOL = "|";
public byte[] get() {
return Strings.bytes(code);
}
public byte[] msg(String body) {
Valid.notNull(body);
return this.msg(Strings.bytes(body));
}
public byte[] msg(byte[] body) {
return this.msg(body, 0, body.length);
}
public byte[] msg(byte[] body, int offset, int len) {
Valid.notNull(body);
try (ByteArrayOutputStream o = new ByteArrayOutputStream()) {
o.write(Strings.bytes(code + SYMBOL));
o.write(body, offset, len);
return o.toByteArray();
} catch (IOException e) {
throw Exceptions.ioRuntime(e);
}
}
}

View File

@@ -0,0 +1,32 @@
package com.orion.ops.framework.websocket.core;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* websocket 配置属性
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/6/25 19:57
*/
@Data
@ConfigurationProperties("spring.websocket")
public class WebsocketContainerConfig {
/**
* 二进制消息缓冲区大小 byte
*/
private Integer binaryBufferSize;
/**
* 文本消息缓冲区大小 byte
*/
private Integer textBufferSize;
/**
* session 最大超时时间 ms
*/
private Long sessionIdleTimeout;
}

View File

@@ -0,0 +1,34 @@
package com.orion.ops.framework.websocket.interceptor;
import com.orion.ops.framework.websocket.constant.WsAttr;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
/**
* 用户拦截器
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/6/25 20:16
*/
public class UserHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
// TODO 获取当前用户
attributes.put(WsAttr.USER, 1);
// if (user == null){
// return false;
// }
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
}
}

View File

@@ -0,0 +1,110 @@
package com.orion.ops.framework.websocket.utils;
import com.orion.lang.exception.AuthenticationException;
import com.orion.lang.exception.ConnectionRuntimeException;
import com.orion.lang.exception.DisabledException;
import com.orion.lang.exception.TimeoutException;
import com.orion.lang.utils.Exceptions;
import com.orion.lang.utils.Urls;
import com.orion.ops.framework.websocket.constant.WsCloseCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.Objects;
/**
* websocket 工具类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2021/6/14 0:36
*/
@Slf4j
public class WebSockets {
private WebSockets() {
}
/**
* 发送消息 忽略并发报错
*
* @param session session
* @param message message
*/
public static void sendText(WebSocketSession session, byte[] message) {
if (!session.isOpen()) {
return;
}
try {
// 响应
session.sendMessage(new TextMessage(message));
} catch (IllegalStateException e) {
// 并发异常
log.error("发送消息失败 {}", Exceptions.getDigest(e));
} catch (IOException e) {
throw Exceptions.ioRuntime(e);
}
}
/**
* 关闭会话
*
* @param session session
* @param code code
*/
public static void close(WebSocketSession session, WsCloseCode code) {
if (!session.isOpen()) {
return;
}
try {
session.close(new CloseStatus(code.getCode(), code.getReason()));
} catch (Exception e) {
log.error("websocket close failure", e);
}
}
/**
* 获取 urlToken
*
* @param request request
* @return token
*/
public static String getToken(ServerHttpRequest request) {
return Urls.getUrlSource(Objects.requireNonNull(request.getURI().toString()));
}
/**
* 获取 urlToken
*
* @param session session
* @return token
*/
public static String getToken(WebSocketSession session) {
return Urls.getUrlSource(Objects.requireNonNull(session.getUri()).toString());
}
/**
* 打开 session 异常关闭
*
* @param session session
* @param e e
*/
public static void openSessionStoreThrowClose(WebSocketSession session, Exception e) {
if (Exceptions.isCausedBy(e, TimeoutException.class)) {
close(session, WsCloseCode.CONNECTION_TIMEOUT);
} else if (Exceptions.isCausedBy(e, ConnectionRuntimeException.class)) {
close(session, WsCloseCode.CONNECTION_FAILURE);
} else if (Exceptions.isCausedBy(e, AuthenticationException.class)) {
close(session, WsCloseCode.CONNECTION_AUTH_FAILURE);
} else if (Exceptions.isCausedBy(e, DisabledException.class)) {
close(session, WsCloseCode.MACHINE_DISABLED);
} else {
close(session, WsCloseCode.CONNECTION_EXCEPTION);
}
}
}

View File

@@ -0,0 +1 @@
com.orion.ops.framework.websocket.config.OrionWebsocketAutoConfiguration

View File

@@ -22,6 +22,8 @@
<module>orion-ops-spring-boot-starter-swagger</module>
<module>orion-ops-spring-boot-starter-datasource</module>
<module>orion-ops-spring-boot-starter-mybatis</module>
<module>orion-ops-spring-boot-starter-job</module>
<module>orion-ops-spring-boot-starter-websocket</module>
</modules>
</project>