package com.ruoyi.device.bts; 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.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; 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; private final ExecutorService executor = Executors.newSingleThreadExecutor(); 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); } public int port() { return port; } public void enqueue(String response) { actions.add(new ScriptedAction(Duration.ZERO, response.getBytes(StandardCharsets.UTF_8), false)); } 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)); } 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(BtsFrameReader.read(input, Integer.MAX_VALUE), StandardCharsets.UTF_8); } @Override public void close() throws Exception { serverSocket.close(); executor.shutdownNow(); executor.awaitTermination(2, TimeUnit.SECONDS); } private record ScriptedAction(Duration delay, byte[] payload, boolean disconnect) { } }