package com.ruoyi.device.bts;
|
|
import java.io.IOException;
|
import java.io.InputStream;
|
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;
|
|
/**
|
* 提供可复用的 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<ScriptedAction> actions = new LinkedBlockingQueue<>();
|
private final List<String> requests = new CopyOnWriteArrayList<>();
|
|
public ScriptedBtsTcpSimulator() throws IOException
|
{
|
this(true);
|
}
|
|
private ScriptedBtsTcpSimulator(boolean acceptConnections) throws IOException
|
{
|
serverSocket = new ServerSocket(0);
|
port = serverSocket.getLocalPort();
|
if (acceptConnections)
|
executor.submit(this::serve);
|
else
|
serverSocket.close();
|
}
|
|
public static ScriptedBtsTcpSimulator refusingConnections() throws IOException
|
{
|
return new ScriptedBtsTcpSimulator(false);
|
}
|
|
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());
|
requests.add(request);
|
ScriptedAction action = actions.poll(5, TimeUnit.SECONDS);
|
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)
|
{
|
}
|
}
|