package com.battery.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.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<ScriptedAction> actions = new LinkedBlockingQueue<>();
|
private final List<String> requests = new CopyOnWriteArrayList<>();
|
private final Function<String, String> responseProvider;
|
|
public ScriptedBtsTcpSimulator() throws IOException
|
{
|
this(0, true, null);
|
}
|
|
private ScriptedBtsTcpSimulator(int port, boolean acceptConnections,
|
Function<String, String> 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<String, String> 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<String> 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 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)
|
{
|
}
|
}
|