package com.battery.device.simulator; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; /** * 提供可复用的 BTS TCP 脚本设备边界,用于闭环与恢复测试。 */ public final class ScriptedBtsTcpSimulator implements AutoCloseable { private static final byte[] FRAME_END = "#\r\n".getBytes(StandardCharsets.UTF_8); private final ServerSocket serverSocket; private final int port; /** 连接服务线程显式创建并命名。该池只提交一个常驻服务任务,队列容量 1 只是声明不再接受更多任务。 */ private final ExecutorService executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1), simulatorThreadFactory()); private final BlockingQueue actions = new LinkedBlockingQueue<>(); private final List requests = new CopyOnWriteArrayList<>(); private final Function responseProvider; public ScriptedBtsTcpSimulator() throws IOException { this(0, true, null); } private ScriptedBtsTcpSimulator(int port, boolean acceptConnections, Function responseProvider) throws IOException { if (port < 0 || port > 65535) throw new IllegalArgumentException("port must be between 0 and 65535"); this.responseProvider = responseProvider; serverSocket = new ServerSocket(); serverSocket.bind(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), port)); this.port = serverSocket.getLocalPort(); if (acceptConnections) executor.submit(this::serve); else serverSocket.close(); } /** 创建一启动就关闭监听、任何连接都会被拒绝的模拟器。 */ public static ScriptedBtsTcpSimulator refusingConnections() throws IOException { return new ScriptedBtsTcpSimulator(0, false, null); } /** 创建仅监听本机、可按请求持续生成响应的模拟器。 */ public static ScriptedBtsTcpSimulator responding(int port, Function responseProvider) throws IOException { if (responseProvider == null) throw new IllegalArgumentException("responseProvider must not be null"); return new ScriptedBtsTcpSimulator(port, true, responseProvider); } /** 返回该模拟器绑定的本机端口;构造时传入 0 时返回的是系统分配端口而非 0。 */ public int port() { return port; } /** 向脚本队列追加一条零延迟应答,按 UTF-8 编码,由下一次请求按顺序取用。 */ public void enqueue(String response) { actions.add(new ScriptedAction(Duration.ZERO, response.getBytes(StandardCharsets.UTF_8), false)); } /** 追加一条延迟应答:连接服务线程收到请求后先等待 delay 再写出;delay 不得为负。 */ public void enqueueDelayed(Duration delay, String response) { if (delay.isNegative()) throw new IllegalArgumentException("delay must not be negative"); actions.add(new ScriptedAction(delay, response.getBytes(StandardCharsets.UTF_8), false)); } /** 追加一条零延迟的原始字节应答:字节原样写出后追加帧结束符,不做字符编码转换。 */ public void enqueueRaw(byte[] response) { actions.add(new ScriptedAction(Duration.ZERO, response.clone(), false)); } /** 追加一条断开动作:处理到该条时结束当前连接,且不写出任何应答。 */ public void enqueueDisconnect() { actions.add(new ScriptedAction(Duration.ZERO, null, true)); } /** 返回已接收请求帧的只读快照;只有脚本模式(未配置 responseProvider)才记录。 */ public List requests() { return List.copyOf(requests); } private void serve() { while (!serverSocket.isClosed()) { try (Socket socket = serverSocket.accept()) { serveConnection(socket); } catch (SocketException ex) { if (!serverSocket.isClosed()) throw new IllegalStateException(ex); } catch (Exception ignored) { } } } private void serveConnection(Socket socket) throws Exception { while (!socket.isClosed()) { String request = readFrame(socket.getInputStream()); if (responseProvider == null) requests.add(request); ScriptedAction action = responseProvider == null ? actions.poll(5, TimeUnit.SECONDS) : new ScriptedAction(Duration.ZERO, responseProvider.apply(request).getBytes(StandardCharsets.UTF_8), false); if (action == null) throw new IOException("No scripted BTS action available"); TimeUnit.MILLISECONDS.sleep(action.delay().toMillis()); if (action.disconnect()) return; socket.getOutputStream().write(action.payload()); socket.getOutputStream().write(FRAME_END); socket.getOutputStream().flush(); } } private String readFrame(InputStream input) throws IOException { return new String(readFrameBytes(input), StandardCharsets.UTF_8); } private byte[] readFrameBytes(InputStream input) throws IOException { java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream(); byte[] delimiter = FRAME_END; int matched = 0; while (true) { int current = input.read(); if (current < 0) throw new java.io.EOFException("BTS frame ended before delimiter"); if ((byte) current == delimiter[matched]) { matched++; if (matched == delimiter.length) return bytes.toByteArray(); continue; } if (matched > 0) { for (int i = 0; i < matched; i++) bytes.write(delimiter[i]); matched = 0; if ((byte) current == delimiter[0]) { matched = 1; continue; } } bytes.write(current); } } /** 关闭监听并终止连接服务线程,最多等待 2 秒,避免测试退出时残留线程。 */ @Override public void close() throws Exception { serverSocket.close(); executor.shutdownNow(); executor.awaitTermination(2, TimeUnit.SECONDS); } /** 模拟器连接服务线程统一命名并设为守护线程,避免测试退出时残留。 */ private static ThreadFactory simulatorThreadFactory() { AtomicInteger sequence = new AtomicInteger(); return runnable -> { Thread thread = new Thread(runnable, "bts-simulator-pool-" + sequence.incrementAndGet()); thread.setDaemon(true); return thread; }; } private record ScriptedAction(Duration delay, byte[] payload, boolean disconnect) { } }