package com.usky.ems.protocol; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.fasterxml.jackson.databind.ObjectMapper; import com.usky.ems.util.AesUtil; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; /** * TCP客户端 * 用于与能耗监管系统建立连接并发送数据 * * @author system * @since 2024-01-01 */ public class TcpClient { private static final Logger logger = LoggerFactory.getLogger(TcpClient.class); private String host; private int port; private String authKey; private Socket socket; private InputStream inputStream; private OutputStream outputStream; private AtomicBoolean connected = new AtomicBoolean(false); private AtomicBoolean authenticated = new AtomicBoolean(false); public TcpClient(String host, int port, String authKey) { this.host = host; this.port = port; this.authKey = authKey; } /** * 建立TCP连接 * * @return 是否连接成功 */ public boolean connect() { try { if (socket != null && !socket.isClosed()) { socket.close(); } socket = new Socket(host, port); socket.setSoTimeout(30000); // 30秒超时 inputStream = socket.getInputStream(); outputStream = socket.getOutputStream(); connected.set(true); authenticated.set(false); logger.info("TCP连接成功: {}:{}", host, port); return true; } catch (IOException e) { logger.error("TCP连接失败: {}:{}", host, port, e); connected.set(false); return false; } } /** * 关闭连接 */ public void close() { try { if (inputStream != null) { inputStream.close(); } if (outputStream != null) { outputStream.close(); } if (socket != null && !socket.isClosed()) { socket.close(); } connected.set(false); authenticated.set(false); logger.info("TCP连接已关闭"); } catch (IOException e) { logger.error("关闭TCP连接失败", e); } } /** * 身份认证 * * @param buildingId 建筑ID * @param gatewayId 网关ID * @return 是否认证成功 */ public boolean authenticate(String buildingId, String gatewayId) { if (!connected.get()) { logger.error("未建立TCP连接,无法进行身份认证"); return false; } try { // 1. 发送身份认证请求 String requestXml = com.usky.ems.util.XmlBuilder.buildAuthRequest(buildingId, gatewayId); sendPacket(NetworkPacket.TYPE_AUTH, requestXml, false); // 2. 接收服务端返回的随机序列 NetworkPacket response = receivePacket(); if (response == null || response.getType() != NetworkPacket.TYPE_AUTH) { logger.error("接收身份认证响应失败"); return false; } // 认证响应不加密 String responseXml = new String(response.getData(), java.nio.charset.StandardCharsets.UTF_8); String sequence = parseSequenceFromXml(responseXml); if (sequence == null || sequence.isEmpty()) { logger.error("解析随机序列失败"); return false; } // 3. 计算MD5:密钥 + 随机序列 String md5 = com.usky.ems.util.Md5Util.md5(authKey + sequence); // 4. 发送MD5值 String md5Xml = com.usky.ems.util.XmlBuilder.buildAuthMd5Request(buildingId, gatewayId, md5); sendPacket(NetworkPacket.TYPE_AUTH, md5Xml, false); // 5. 接收认证结果 NetworkPacket result = receivePacket(); if (result == null || result.getType() != NetworkPacket.TYPE_AUTH) { logger.error("接收认证结果失败"); return false; } // 认证响应不加密 String resultXml = new String(result.getData(), java.nio.charset.StandardCharsets.UTF_8); boolean success = parseAuthResult(resultXml); if (success) { authenticated.set(true); logger.info("身份认证成功"); } else { logger.error("身份认证失败"); } return success; } catch (Exception e) { logger.error("身份认证过程异常", e); return false; } } /** * 发送心跳 * * @param buildingId 建筑ID * @param gatewayId 网关ID * @return 是否发送成功 */ public boolean sendHeartbeat(String buildingId, String gatewayId) { if (!connected.get() || !authenticated.get()) { logger.warn("连接未建立或未认证,无法发送心跳"); return false; } try { String heartbeatXml = com.usky.ems.util.XmlBuilder.buildHeartbeatRequest(buildingId, gatewayId); sendPacket(NetworkPacket.TYPE_HEARTBEAT, heartbeatXml, false); NetworkPacket response1 = receivePacket(); if (response1 != null && response1.getType() == NetworkPacket.TYPE_HEARTBEAT) { // 直接转换为可读XML(无需解密) String heartbeatResponseXml = new String(response1.getData(), java.nio.charset.StandardCharsets.UTF_8); System.out.println("可读报文:\n" + heartbeatResponseXml); boolean success = this.parseHeartbeatResult(heartbeatResponseXml); if (success) { logger.info("发送心跳成功"); return true; }else{ logger.info("发送心跳失败"); return false; } } return false; } catch (Exception e) { logger.error("发送心跳失败", e); return false; } } /** * 发送能耗数据 * * @param xmlData XML数据(未加密) * @return 是否发送成功 */ public boolean sendEnergyData(String xmlData) { if (!connected.get() || !authenticated.get()) { logger.warn("连接未建立或未认证,无法发送能耗数据"); return false; } try { sendPacket(NetworkPacket.TYPE_ENERGY_DATA, xmlData, true); NetworkPacket response = receivePacket(); if (response != null && response.getType() == NetworkPacket.TYPE_ENERGY_DATA) { // 直接转换为可读XML(无需解密) String readable = new String(response.getData(), java.nio.charset.StandardCharsets.UTF_8); System.out.println("可读报文:\n" + readable); boolean success = this.parseEnergyDataResult(readable); if (success) { logger.info("能耗数据发送成功"); return true; }else{ logger.info("能耗数据发送失败"); return false; } } return false; } catch (Exception e) { logger.error("发送能耗数据失败", e); return false; } } /** * 将TCP报文字节数组直接转换为可读格式(无需解密) * @param dataBytes 原始报文字节数组 * @param charset 报文字符编码(默认UTF-8) * @param format 报文格式(xml/json/plain,plain为普通文本) * @return 可读的报文字符串 */ public String convertToReadablePacket(byte[] dataBytes, String charset, String format) { if (dataBytes == null || dataBytes.length == 0) { return "空报文"; } // 兜底处理字符编码,默认用UTF-8 String actualCharset = (charset == null || charset.isEmpty()) ? "UTF-8" : charset; try { // 第一步:直接将字节数组转字符串(核心,无需解密) String rawText = new String(dataBytes, actualCharset); logger.debug("原始文本报文:{}", rawText); // 第二步:根据格式美化 switch (format.toLowerCase()) { case "xml": return formatXml(rawText); // XML格式化(带缩进) case "json": return formatJson(rawText); // JSON格式化(带缩进) case "plain": default: return rawText; // 普通文本直接返回 } } catch (Exception e) { logger.warn("转换为文本格式失败,返回十六进制格式:{}", e.getMessage()); // 转换失败时,返回十六进制(所有二进制数据都能读) return bytesToHex(dataBytes); } } /** * XML格式化(美化缩进) */ private String formatXml(String xmlText) throws DocumentException { Document document = DocumentHelper.parseText(xmlText); return document.asXML(); } /** * JSON格式化(美化缩进) */ private String formatJson(String jsonText) throws Exception { ObjectMapper mapper = new ObjectMapper(); Object jsonObj = mapper.readValue(jsonText, Object.class); return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObj); } /** * 字节数组转十六进制字符串(兜底方案) * 格式:00 1A 2B ... (便于排查二进制数据问题) */ private String bytesToHex(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02X ", b)); } return "十六进制格式:" + sb.toString().trim(); } /** * 发送数据包 * * @param type 消息类型 * @param xmlData XML数据 * @param encrypt 是否加密 */ private void sendPacket(byte type, String xmlData, boolean encrypt) throws IOException { byte[] data; if (encrypt) { // 能耗数据需要AES加密 try { data = AesUtil.encryptEnergyXml(xmlData, authKey); } catch (Exception e) { throw new IOException("加密能耗数据失败", e); } } else { // 身份认证和心跳不加密 data = xmlData.getBytes(java.nio.charset.StandardCharsets.UTF_8); } NetworkPacket packet = new NetworkPacket(); packet.setType(type); packet.setData(data); byte[] packetBytes = packet.encode(); outputStream.write(packetBytes); outputStream.flush(); } /** * 接收数据包 * * @return 接收到的数据包 */ private NetworkPacket receivePacket() throws IOException { // 先读取7字节(Head + Type + Length) byte[] header = new byte[7]; int bytesRead = 0; while (bytesRead < 7) { int n = inputStream.read(header, bytesRead, 7 - bytesRead); if (n == -1) { throw new IOException("连接已关闭"); } bytesRead += n; } // 解析Length int length = ((header[3] & 0xFF) << 24) | ((header[4] & 0xFF) << 16) | ((header[5] & 0xFF) << 8) | (header[6] & 0xFF); // 读取Data byte[] data = new byte[length]; bytesRead = 0; while (bytesRead < length) { int n = inputStream.read(data, bytesRead, length - bytesRead); if (n == -1) { throw new IOException("连接已关闭"); } bytesRead += n; } // 组装完整数据包 byte[] packetBytes = new byte[7 + length]; System.arraycopy(header, 0, packetBytes, 0, 7); System.arraycopy(data, 0, packetBytes, 7, length); return NetworkPacket.decode(packetBytes); } /** * 从XML中解析随机序列 */ private String parseSequenceFromXml(String xml) { try { Document document = DocumentHelper.parseText(xml); Element root = document.getRootElement(); Element idValidate = root.element("id_validate"); if (idValidate != null) { Element sequence = idValidate.element("sequence"); if (sequence != null) { return sequence.getTextTrim(); } } } catch (Exception e) { logger.error("解析随机序列失败", e); } return null; } /** * 从XML中解析认证结果 */ private boolean parseAuthResult(String xml) { try { Document document = DocumentHelper.parseText(xml); Element root = document.getRootElement(); Element idValidate = root.element("id_validate"); if (idValidate != null && "result".equals(idValidate.attributeValue("operation"))) { Element result = idValidate.element("result"); if (result != null) { return "pass".equals(result.getTextTrim()); } } } catch (Exception e) { logger.error("解析认证结果失败", e); } return false; } /** * 从XML中解析推送能耗数据响应结果 */ private boolean parseEnergyDataResult(String xml) { try { Document document = DocumentHelper.parseText(xml); Element root = document.getRootElement(); Element dataElement = root.element("data"); if (dataElement == null) { logger.warn("XML中未找到节点"); } // 4. 获取子节点的文本值 Element ackElement = dataElement.element("ack"); if (ackElement == null) { logger.warn("XML中未找到节点"); } return "OK".equals(ackElement.getTextTrim()); } catch (Exception e) { logger.error("解析认证结果失败", e); } return false; } /** * 从XML中解析推送能耗数据响应结果 */ private boolean parseHeartbeatResult(String xml) { try { Document document = DocumentHelper.parseText(xml); Element root = document.getRootElement(); Element dataElement = root.element("heart_beat"); if (dataElement == null) { logger.warn("XML中未找到节点"); } // 4. 获取子节点的文本值 Element ackElement = dataElement.element("time"); if (ackElement == null) { logger.warn("XML中未找到