package xiaozhi.modules.sys.utils; import java.io.Closeable; import java.io.IOException; import java.net.URI; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Predicate; import org.springframework.util.StopWatch; import org.springframework.web.socket.BinaryMessage; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketHttpHeaders; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.client.standard.StandardWebSocketClient; import org.springframework.web.socket.handler.AbstractWebSocketHandler; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import xiaozhi.common.utils.DateUtils; /** * WebSocketClientResource:支持 try-with-resources 模式 */ @Slf4j public class WebSocketClientManager implements Closeable { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); // 全局回调线程池 private static final ExecutorService CALLBACK_EXECUTOR = Executors .newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() { private final AtomicInteger cnt = new AtomicInteger(); public Thread newThread(Runnable r) { Thread t = new Thread(r, "ws-callback-" + cnt.getAndIncrement()); t.setDaemon(true); return t; } }); private volatile WebSocketSession session; private final BlockingQueue textMessageQueue; private final BlockingQueue binaryMessageQueue; private final CompletableFuture errorFuture; private final long maxSessionDuration; private final TimeUnit maxSessionDurationUnit; private volatile Consumer onText; private volatile Consumer onBinary; private volatile Consumer onError; private final int queueCapacity; // 私有构造,仅由 Builder 调用 private WebSocketClientManager(Builder b) { this.maxSessionDuration = b.maxSessionDuration; this.maxSessionDurationUnit = b.maxSessionDurationUnit; this.queueCapacity = b.queueCapacity; this.textMessageQueue = new LinkedBlockingQueue<>(queueCapacity); this.binaryMessageQueue = new LinkedBlockingQueue<>(queueCapacity); this.errorFuture = new CompletableFuture<>(); } public static WebSocketClientManager build(Builder b) throws InterruptedException, ExecutionException, TimeoutException, IOException { WebSocketClientManager ws = new WebSocketClientManager(b); StandardWebSocketClient client = new StandardWebSocketClient(); CompletableFuture future = client.execute(ws.new InternalHandler(b.uri), b.headers, URI.create(b.uri)); WebSocketSession sess = future.get(b.connectTimeout, b.connectUnit); if (sess == null || !sess.isOpen()) { throw new IOException("握手失败或会话未打开"); } // 设置缓冲区 sess.setTextMessageSizeLimit(b.bufferSize); sess.setBinaryMessageSizeLimit(b.bufferSize); ws.session = sess; return ws; } /** * 发送 Text */ public void sendText(String text) throws IOException { session.sendMessage(new TextMessage(text)); } public void sendBinary(byte[] data) throws IOException { session.sendMessage(new BinaryMessage(data)); } public void sendJson(Object payload) throws IOException { String json = OBJECT_MAPPER.writeValueAsString(payload); session.sendMessage(new TextMessage(json)); } private List listenerCustom( BlockingQueue queue, Predicate predicate) throws InterruptedException, TimeoutException, ExecutionException { List collected = new ArrayList<>(); long deadline = System.currentTimeMillis() + maxSessionDurationUnit.toMillis(maxSessionDuration); while (true) { if (errorFuture.isDone()) { errorFuture.get(); } long remaining = deadline - System.currentTimeMillis(); if (remaining <= 0) { throw new TimeoutException("等待批量消息超时"); } T msg = queue.poll(remaining, TimeUnit.MILLISECONDS); if (msg == null) { throw new TimeoutException("等待批量消息超时"); } collected.add(msg); if (predicate.test(msg)) { break; } } close(); return collected; } private List listenerCustomWithoutClose( BlockingQueue queue, Predicate predicate) throws InterruptedException, TimeoutException, ExecutionException { List collected = new ArrayList<>(); long deadline = System.currentTimeMillis() + maxSessionDurationUnit.toMillis(maxSessionDuration); while (true) { if (errorFuture.isDone()) { errorFuture.get(); } long remaining = deadline - System.currentTimeMillis(); if (remaining <= 0) { throw new TimeoutException("等待批量消息超时"); } T msg = queue.poll(remaining, TimeUnit.MILLISECONDS); if (msg == null) { throw new TimeoutException("等待批量消息超时"); } collected.add(msg); if (predicate.test(msg)) { break; } } // 不调用 close(),保持连接开放 return collected; } /** * 同步接收多条消息,直到 predicate 为 true 或超时抛异常; * * @return 返回监听期间的所有消息列表 */ public List listener(Predicate predicate) throws InterruptedException, TimeoutException, ExecutionException { return listenerCustom(textMessageQueue, predicate); } /** * 同步接收多条消息,直到 predicate 为 true 或超时抛异常; * 不自动关闭连接,适用于需要在同一连接上发送多个消息的场景 * * @return 返回监听期间的所有消息列表 */ public List listenerWithoutClose(Predicate predicate) throws InterruptedException, TimeoutException, ExecutionException { return listenerCustomWithoutClose(textMessageQueue, predicate); } public List listenerBinary(Predicate predicate) throws InterruptedException, TimeoutException, ExecutionException { return listenerCustom(binaryMessageQueue, predicate); } /** * 注册文本回调 */ public WebSocketClientManager onText(Consumer c) { this.onText = c; return this; } /** * 注册二进制回调 */ public WebSocketClientManager onBinary(Consumer c) { this.onBinary = c; return this; } /** * 注册错误回调 */ public WebSocketClientManager onError(Consumer c) { this.onError = c; return this; } /** * 关闭会话,try-with-resources / finally 自动调用 */ @Override public void close() { try { if (session != null && session.isOpen()) { session.close(CloseStatus.NORMAL); } } catch (IOException ignored) { } textMessageQueue.clear(); binaryMessageQueue.clear(); errorFuture.completeExceptionally(new IOException("WebSocket 已关闭")); } private class InternalHandler extends AbstractWebSocketHandler { private final String targetUri; private final StopWatch stopWatch; InternalHandler(String targetUri) { this.targetUri = targetUri; this.stopWatch = new StopWatch(); } /** * 连接建立时回调 */ @Override public void afterConnectionEstablished(WebSocketSession session) { // 保存会话 WebSocketClientManager.this.session = session; this.stopWatch.start(); log.info("ws连接成功, 目标URI: {}, 连接时间: {}", targetUri, DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN)); } /** * 处理文本消息 */ @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload = message.getPayload(); // 入队 textMessageQueue.offer(payload); // 回调用户注册的 onText if (onText != null) { CALLBACK_EXECUTOR.submit(() -> onText.accept(payload)); } } /** * 处理二进制消息 */ @Override protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception { ByteBuffer buf = message.getPayload(); byte[] data = new byte[buf.remaining()]; buf.get(data); // 入队 binaryMessageQueue.offer(data); // 回调用户注册的 onBinary if (onBinary != null) { CALLBACK_EXECUTOR.submit(() -> onBinary.accept(data)); } } /** * 传输错误时回调 */ @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { super.handleTransportError(session, exception); // 保持原有逻辑:完成 errorFuture、回调 onError、关闭会话、异步通知连接失败 errorFuture.completeExceptionally(exception); if (onError != null) { CALLBACK_EXECUTOR.submit(() -> onError.accept(exception)); } session.close(CloseStatus.SERVER_ERROR); } /** * 连接关闭时回调 */ @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { super.afterConnectionClosed(session, status); if (stopWatch.isRunning()) { stopWatch.stop(); } log.info("ws连接关闭, 目标URI: {}, 关闭时间: {}, 连接总时长: {}s,断开原因:{}", targetUri, DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN), DateUtils.millsToSecond(stopWatch.getTotalTimeMillis()),status); } } public static class Builder { private String uri; // 目标 WS URI private long connectTimeout = 3; // 请求连接等待时间 private TimeUnit connectUnit = TimeUnit.SECONDS; // 请求连接等待时间单位 private long maxSessionDuration = 5; // 最大连线时间,默认5秒 private TimeUnit maxSessionDurationUnit = TimeUnit.SECONDS; // 最大连线时间单位 private int queueCapacity = 100; // 消息队列容量 private int bufferSize = 8 * 1024; //默认 8kb private WebSocketHttpHeaders headers; // 请求头 /** * 目标 WS URI */ public Builder uri(String uri) { this.uri = Objects.requireNonNull(uri); return this; } public Builder headers(WebSocketHttpHeaders h) { this.headers = h; return this; } public Builder connectTimeout(long t, TimeUnit u) { this.connectTimeout = t; this.connectUnit = u; return this; } public Builder maxSessionDuration(long t, TimeUnit u) { this.maxSessionDuration = t; this.maxSessionDurationUnit = u; return this; } public Builder queueCapacity(int c) { this.queueCapacity = c; return this; } public Builder bufferSize(int c) { this.bufferSize = c; return this; } public WebSocketClientManager build() throws InterruptedException, ExecutionException, TimeoutException, IOException { return WebSocketClientManager.build(this); } } }