yeg
2026-08-26 db2eb9412ddc4ff687eb4c9b862053f49f9aec7b
ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsTcpTransport.java
@@ -1,21 +1,27 @@
package com.ruoyi.device.bts;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
import com.ruoyi.device.api.DeviceAdapterException;
public final class BtsTcpTransport implements BtsTransport
{
    private static final byte[] FRAME_END = "#\r\n".getBytes(StandardCharsets.UTF_8);
    private static final int DEFAULT_MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
    private static final int MAX_RESPONSE_TIMEOUT_MS = 30_000;
    private final String host;
    private final int port;
@@ -39,6 +45,9 @@
        this.maxResponseBytes = maxResponseBytes;
        if (host.isBlank() || port < 1 || port > 65535) throw new IllegalArgumentException("Invalid BTS TCP endpoint");
        if (connectTimeoutMs < 1 || responseTimeoutMs < 1) throw new IllegalArgumentException("Timeouts must be positive");
        // 厂商协议声明单条命令最长不超过约 30 秒,避免串行请求队列被错误配置长期阻塞。
        if (responseTimeoutMs > MAX_RESPONSE_TIMEOUT_MS)
            throw new IllegalArgumentException("BTS response timeout must not exceed 30 seconds");
        if (maxResponseBytes < 1) throw new IllegalArgumentException("maxResponseBytes must be positive");
    }
@@ -56,10 +65,41 @@
            output.flush();
            return readFrame(socket.getInputStream());
        }
        catch (ConnectException ex)
        {
            closeSocket();
            throw new BtsTransportException(BtsTransportException.Reason.CONNECTION_FAILED,
                    "BTS TCP connection failed for " + host + ":" + port, ex);
        }
        catch (SocketTimeoutException ex)
        {
            closeSocket();
            throw new BtsTransportException(BtsTransportException.Reason.TIMEOUT,
                    "BTS TCP exchange timed out for " + host + ":" + port, ex);
        }
        catch (EOFException ex)
        {
            closeSocket();
            throw new BtsTransportException(BtsTransportException.Reason.DISCONNECTED,
                    "BTS disconnected before completing a response for " + host + ":" + port, ex);
        }
        catch (ResponseTooLargeException ex)
        {
            closeSocket();
            throw new BtsTransportException(BtsTransportException.Reason.RESPONSE_TOO_LARGE,
                    "BTS response exceeded " + maxResponseBytes + " bytes for " + host + ":" + port, ex);
        }
        catch (InvalidUtf8Exception ex)
        {
            closeSocket();
            throw new BtsTransportException(BtsTransportException.Reason.INVALID_UTF8,
                    "BTS response is not valid UTF-8 for " + host + ":" + port, ex);
        }
        catch (IOException ex)
        {
            closeSocket();
            throw new DeviceAdapterException("BTS TCP exchange failed for " + host + ":" + port, ex);
            throw new BtsTransportException(BtsTransportException.Reason.IO_FAILURE,
                    "BTS TCP exchange failed for " + host + ":" + port, ex);
        }
        finally
        {
@@ -80,24 +120,50 @@
    {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        int matched = 0;
        while (buffer.size() < maxResponseBytes)
        while (true)
        {
            int value = input.read();
            if (value < 0) throw new IOException("BTS closed the connection before the response frame ended");
            if (value < 0) throw new EOFException("BTS closed the connection before the response frame ended");
            if ((byte) value == FRAME_END[matched])
            {
                matched++;
                if (matched == FRAME_END.length) return buffer.toString(StandardCharsets.UTF_8);
                if (matched == FRAME_END.length) return decodeUtf8(buffer.toByteArray());
                continue;
            }
            if (matched > 0)
            {
                buffer.write(FRAME_END, 0, matched);
                for (int i = 0; i < matched; i++) appendPayload(buffer, FRAME_END[i]);
                matched = 0;
                if ((byte) value == FRAME_END[0])
                {
                    matched = 1;
                    continue;
                }
            }
            buffer.write(value);
            appendPayload(buffer, value);
        }
        throw new IOException("BTS response exceeded " + maxResponseBytes + " bytes");
    }
    private void appendPayload(ByteArrayOutputStream buffer, int value) throws ResponseTooLargeException
    {
        if (buffer.size() >= maxResponseBytes) throw new ResponseTooLargeException();
        buffer.write(value);
    }
    private static String decodeUtf8(byte[] bytes) throws InvalidUtf8Exception
    {
        try
        {
            return StandardCharsets.UTF_8.newDecoder()
                    .onMalformedInput(CodingErrorAction.REPORT)
                    .onUnmappableCharacter(CodingErrorAction.REPORT)
                    .decode(ByteBuffer.wrap(bytes))
                    .toString();
        }
        catch (CharacterCodingException ex)
        {
            throw new InvalidUtf8Exception(ex);
        }
    }
    @Override
@@ -129,4 +195,19 @@
            socket = null;
        }
    }
    private static final class ResponseTooLargeException extends IOException
    {
        private static final long serialVersionUID = 1L;
    }
    private static final class InvalidUtf8Exception extends IOException
    {
        private static final long serialVersionUID = 1L;
        private InvalidUtf8Exception(Throwable cause)
        {
            super(cause);
        }
    }
}