package com.ruoyi.device.bts;
|
|
import java.io.ByteArrayOutputStream;
|
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;
|
|
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<>();
|
|
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();
|
}
|
|
static ScriptedBtsTcpSimulator refusingConnections() throws IOException
|
{
|
return new ScriptedBtsTcpSimulator(false);
|
}
|
|
int port()
|
{
|
return port;
|
}
|
|
void enqueue(String response)
|
{
|
actions.add(new ScriptedAction(Duration.ZERO, response.getBytes(StandardCharsets.UTF_8), false));
|
}
|
|
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));
|
}
|
|
void enqueueRaw(byte[] response)
|
{
|
actions.add(new ScriptedAction(Duration.ZERO, response.clone(), false));
|
}
|
|
void enqueueDisconnect()
|
{
|
actions.add(new ScriptedAction(Duration.ZERO, null, true));
|
}
|
|
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
|
{
|
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
int matched = 0;
|
while (true)
|
{
|
int value = input.read();
|
if (value < 0) throw new IOException("Client disconnected");
|
if ((byte) value == FRAME_END[matched])
|
{
|
matched++;
|
if (matched == FRAME_END.length) return buffer.toString(StandardCharsets.UTF_8);
|
continue;
|
}
|
if (matched > 0)
|
{
|
buffer.write(FRAME_END, 0, matched);
|
matched = 0;
|
}
|
buffer.write(value);
|
}
|
}
|
|
@Override
|
public void close() throws Exception
|
{
|
serverSocket.close();
|
executor.shutdownNow();
|
executor.awaitTermination(2, TimeUnit.SECONDS);
|
}
|
|
private record ScriptedAction(Duration delay, byte[] payload, boolean disconnect)
|
{
|
}
|
}
|