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<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);
|
}
|
|
/** 返回该模拟器绑定的本机端口;构造时传入 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<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(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)
|
{
|
}
|
}
|