|
|
@@ -4,13 +4,9 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
|
|
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
|
|
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkWebSocketConfig;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
+import okhttp3.*;
|
|
|
|
|
|
-import java.net.URI;
|
|
|
-import java.net.http.HttpClient;
|
|
|
-import java.net.http.WebSocket;
|
|
|
-import java.time.Duration;
|
|
|
-import java.util.concurrent.CompletableFuture;
|
|
|
-import java.util.concurrent.CompletionStage;
|
|
|
+import java.util.concurrent.CountDownLatch;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
|
|
@@ -19,21 +15,23 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
* <p>
|
|
|
* 负责与外部 WebSocket 服务器建立连接并发送设备消息
|
|
|
* 支持 ws:// 和 wss:// 协议,支持 JSON 和 TEXT 数据格式
|
|
|
- * 基于 Java 11+ 内置的 java.net.http.WebSocket 实现
|
|
|
+ * 基于 OkHttp WebSocket 实现,兼容 JDK 8+
|
|
|
+ * <p>
|
|
|
+ * 注意:该类的线程安全由调用方(IotWebSocketDataRuleAction)通过分布式锁保证
|
|
|
*
|
|
|
* @author HUIHUI
|
|
|
*/
|
|
|
@Slf4j
|
|
|
-public class IotWebSocketClient implements WebSocket.Listener {
|
|
|
+public class IotWebSocketClient {
|
|
|
|
|
|
private final String serverUrl;
|
|
|
private final Integer connectTimeoutMs;
|
|
|
private final Integer sendTimeoutMs;
|
|
|
private final String dataFormat;
|
|
|
|
|
|
- private WebSocket webSocket;
|
|
|
+ private OkHttpClient okHttpClient;
|
|
|
+ private volatile WebSocket webSocket;
|
|
|
private final AtomicBoolean connected = new AtomicBoolean(false);
|
|
|
- private final StringBuilder messageBuffer = new StringBuilder();
|
|
|
|
|
|
public IotWebSocketClient(String serverUrl, Integer connectTimeoutMs, Integer sendTimeoutMs, String dataFormat) {
|
|
|
this.serverUrl = serverUrl;
|
|
|
@@ -44,8 +42,9 @@ public class IotWebSocketClient implements WebSocket.Listener {
|
|
|
|
|
|
/**
|
|
|
* 连接到 WebSocket 服务器
|
|
|
+ * <p>
|
|
|
+ * 注意:调用方需要通过分布式锁保证并发安全
|
|
|
*/
|
|
|
- @SuppressWarnings("resource")
|
|
|
public void connect() throws Exception {
|
|
|
if (connected.get()) {
|
|
|
log.warn("[connect][WebSocket 客户端已经连接,无需重复连接]");
|
|
|
@@ -53,17 +52,32 @@ public class IotWebSocketClient implements WebSocket.Listener {
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
- HttpClient httpClient = HttpClient.newBuilder()
|
|
|
- .connectTimeout(Duration.ofMillis(connectTimeoutMs))
|
|
|
+ // 创建 OkHttpClient
|
|
|
+ okHttpClient = new OkHttpClient.Builder()
|
|
|
+ .connectTimeout(connectTimeoutMs, TimeUnit.MILLISECONDS)
|
|
|
+ .readTimeout(sendTimeoutMs, TimeUnit.MILLISECONDS)
|
|
|
+ .writeTimeout(sendTimeoutMs, TimeUnit.MILLISECONDS)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ // 创建 WebSocket 请求
|
|
|
+ Request request = new Request.Builder()
|
|
|
+ .url(serverUrl)
|
|
|
.build();
|
|
|
|
|
|
- CompletableFuture<WebSocket> future = httpClient.newWebSocketBuilder()
|
|
|
- .connectTimeout(Duration.ofMillis(connectTimeoutMs))
|
|
|
- .buildAsync(URI.create(serverUrl), this);
|
|
|
+ // 使用 CountDownLatch 等待连接完成
|
|
|
+ CountDownLatch connectLatch = new CountDownLatch(1);
|
|
|
+ AtomicBoolean connectSuccess = new AtomicBoolean(false);
|
|
|
+
|
|
|
+ // 创建 WebSocket 连接
|
|
|
+ webSocket = okHttpClient.newWebSocket(request, new IotWebSocketListener(connectLatch, connectSuccess));
|
|
|
|
|
|
// 等待连接完成
|
|
|
- webSocket = future.get(connectTimeoutMs, TimeUnit.MILLISECONDS);
|
|
|
- connected.set(true);
|
|
|
+ boolean await = connectLatch.await(connectTimeoutMs, TimeUnit.MILLISECONDS);
|
|
|
+ if (!await || !connectSuccess.get()) {
|
|
|
+ close();
|
|
|
+ throw new Exception("WebSocket 连接超时或失败,服务器地址: " + serverUrl);
|
|
|
+ }
|
|
|
+
|
|
|
log.info("[connect][WebSocket 客户端连接成功,服务器地址: {}]", serverUrl);
|
|
|
} catch (Exception e) {
|
|
|
close();
|
|
|
@@ -72,36 +86,6 @@ public class IotWebSocketClient implements WebSocket.Listener {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- @Override
|
|
|
- public void onOpen(WebSocket webSocket) {
|
|
|
- log.debug("[onOpen][WebSocket 连接已打开]");
|
|
|
- webSocket.request(1);
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
|
|
|
- messageBuffer.append(data);
|
|
|
- if (last) {
|
|
|
- log.debug("[onText][收到 WebSocket 消息: {}]", messageBuffer);
|
|
|
- messageBuffer.setLength(0);
|
|
|
- }
|
|
|
- webSocket.request(1);
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
|
|
|
- connected.set(false);
|
|
|
- log.info("[onClose][WebSocket 连接已关闭,状态码: {},原因: {}]", statusCode, reason);
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public void onError(WebSocket webSocket, Throwable error) {
|
|
|
- connected.set(false);
|
|
|
- log.error("[onError][WebSocket 发生错误]", error);
|
|
|
- }
|
|
|
-
|
|
|
/**
|
|
|
* 发送设备消息
|
|
|
*
|
|
|
@@ -109,7 +93,8 @@ public class IotWebSocketClient implements WebSocket.Listener {
|
|
|
* @throws Exception 发送异常
|
|
|
*/
|
|
|
public void sendMessage(IotDeviceMessage message) throws Exception {
|
|
|
- if (!connected.get() || webSocket == null) {
|
|
|
+ WebSocket ws = this.webSocket;
|
|
|
+ if (!connected.get() || ws == null) {
|
|
|
throw new IllegalStateException("WebSocket 客户端未连接");
|
|
|
}
|
|
|
|
|
|
@@ -121,9 +106,11 @@ public class IotWebSocketClient implements WebSocket.Listener {
|
|
|
messageData = message.toString();
|
|
|
}
|
|
|
|
|
|
- // 发送消息并等待完成
|
|
|
- CompletableFuture<WebSocket> future = webSocket.sendText(messageData, true);
|
|
|
- future.get(sendTimeoutMs, TimeUnit.MILLISECONDS);
|
|
|
+ // 发送消息
|
|
|
+ boolean success = ws.send(messageData);
|
|
|
+ if (!success) {
|
|
|
+ throw new Exception("WebSocket 发送消息失败,消息队列已满或连接已关闭");
|
|
|
+ }
|
|
|
log.debug("[sendMessage][发送消息成功,设备 ID: {},消息长度: {}]",
|
|
|
message.getDeviceId(), messageData.length());
|
|
|
} catch (Exception e) {
|
|
|
@@ -136,18 +123,17 @@ public class IotWebSocketClient implements WebSocket.Listener {
|
|
|
* 关闭连接
|
|
|
*/
|
|
|
public void close() {
|
|
|
- if (!connected.get() && webSocket == null) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
try {
|
|
|
if (webSocket != null) {
|
|
|
- webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "客户端主动关闭")
|
|
|
- .orTimeout(5, TimeUnit.SECONDS)
|
|
|
- .exceptionally(e -> {
|
|
|
- log.warn("[close][发送关闭帧失败]", e);
|
|
|
- return null;
|
|
|
- });
|
|
|
+ // 发送正常关闭帧,状态码 1000 表示正常关闭
|
|
|
+ webSocket.close(1000, "客户端主动关闭");
|
|
|
+ webSocket = null;
|
|
|
+ }
|
|
|
+ if (okHttpClient != null) {
|
|
|
+ // 关闭连接池和调度器
|
|
|
+ okHttpClient.dispatcher().executorService().shutdown();
|
|
|
+ okHttpClient.connectionPool().evictAll();
|
|
|
+ okHttpClient = null;
|
|
|
}
|
|
|
connected.set(false);
|
|
|
log.info("[close][WebSocket 客户端连接已关闭,服务器地址: {}]", serverUrl);
|
|
|
@@ -174,4 +160,50 @@ public class IotWebSocketClient implements WebSocket.Listener {
|
|
|
'}';
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * OkHttp WebSocket 监听器
|
|
|
+ */
|
|
|
+ private class IotWebSocketListener extends WebSocketListener {
|
|
|
+
|
|
|
+ private final CountDownLatch connectLatch;
|
|
|
+ private final AtomicBoolean connectSuccess;
|
|
|
+
|
|
|
+ public IotWebSocketListener(CountDownLatch connectLatch, AtomicBoolean connectSuccess) {
|
|
|
+ this.connectLatch = connectLatch;
|
|
|
+ this.connectSuccess = connectSuccess;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onOpen(WebSocket webSocket, Response response) {
|
|
|
+ connected.set(true);
|
|
|
+ connectSuccess.set(true);
|
|
|
+ connectLatch.countDown();
|
|
|
+ log.info("[onOpen][WebSocket 连接已打开,服务器: {}]", serverUrl);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onMessage(WebSocket webSocket, String text) {
|
|
|
+ log.debug("[onMessage][收到消息: {}]", text);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onClosing(WebSocket webSocket, int code, String reason) {
|
|
|
+ connected.set(false);
|
|
|
+ log.info("[onClosing][WebSocket 正在关闭,code: {}, reason: {}]", code, reason);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onClosed(WebSocket webSocket, int code, String reason) {
|
|
|
+ connected.set(false);
|
|
|
+ log.info("[onClosed][WebSocket 已关闭,code: {}, reason: {}]", code, reason);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
|
|
+ connected.set(false);
|
|
|
+ connectLatch.countDown(); // 确保连接失败时也释放等待
|
|
|
+ log.error("[onFailure][WebSocket 连接失败]", t);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
}
|