| | |
| | | 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; |
| | |
| | | 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"); |
| | | } |
| | | |
| | |
| | | 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 |
| | | { |
| | |
| | | { |
| | | 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 |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |