TcpClient.java 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. package com.usky.ems.protocol;
  2. import com.baomidou.mybatisplus.core.toolkit.StringUtils;
  3. import com.baomidou.mybatisplus.core.toolkit.Wrappers;
  4. import com.fasterxml.jackson.databind.ObjectMapper;
  5. import com.usky.ems.util.AesUtil;
  6. import org.dom4j.Document;
  7. import org.dom4j.DocumentException;
  8. import org.dom4j.DocumentHelper;
  9. import org.dom4j.Element;
  10. import org.slf4j.Logger;
  11. import org.slf4j.LoggerFactory;
  12. import java.io.IOException;
  13. import java.io.InputStream;
  14. import java.io.OutputStream;
  15. import java.net.Socket;
  16. import java.time.LocalDateTime;
  17. import java.time.ZoneOffset;
  18. import java.util.Objects;
  19. import java.util.concurrent.atomic.AtomicBoolean;
  20. /**
  21. * TCP客户端
  22. * 用于与能耗监管系统建立连接并发送数据
  23. *
  24. * @author system
  25. * @since 2024-01-01
  26. */
  27. public class TcpClient {
  28. private static final Logger logger = LoggerFactory.getLogger(TcpClient.class);
  29. private String host;
  30. private int port;
  31. private String authKey;
  32. private Socket socket;
  33. private InputStream inputStream;
  34. private OutputStream outputStream;
  35. private AtomicBoolean connected = new AtomicBoolean(false);
  36. private AtomicBoolean authenticated = new AtomicBoolean(false);
  37. public TcpClient(String host, int port, String authKey) {
  38. this.host = host;
  39. this.port = port;
  40. this.authKey = authKey;
  41. }
  42. /**
  43. * 建立TCP连接
  44. *
  45. * @return 是否连接成功
  46. */
  47. public boolean connect() {
  48. try {
  49. if (socket != null && !socket.isClosed()) {
  50. socket.close();
  51. }
  52. socket = new Socket(host, port);
  53. socket.setSoTimeout(30000); // 30秒超时
  54. inputStream = socket.getInputStream();
  55. outputStream = socket.getOutputStream();
  56. connected.set(true);
  57. authenticated.set(false);
  58. logger.info("TCP连接成功: {}:{}", host, port);
  59. return true;
  60. } catch (IOException e) {
  61. logger.error("TCP连接失败: {}:{}", host, port, e);
  62. connected.set(false);
  63. return false;
  64. }
  65. }
  66. /**
  67. * 关闭连接
  68. */
  69. public void close() {
  70. try {
  71. if (inputStream != null) {
  72. inputStream.close();
  73. }
  74. if (outputStream != null) {
  75. outputStream.close();
  76. }
  77. if (socket != null && !socket.isClosed()) {
  78. socket.close();
  79. }
  80. connected.set(false);
  81. authenticated.set(false);
  82. logger.info("TCP连接已关闭");
  83. } catch (IOException e) {
  84. logger.error("关闭TCP连接失败", e);
  85. }
  86. }
  87. /**
  88. * 身份认证
  89. *
  90. * @param buildingId 建筑ID
  91. * @param gatewayId 网关ID
  92. * @return 是否认证成功
  93. */
  94. public boolean authenticate(String buildingId, String gatewayId) {
  95. if (!connected.get()) {
  96. logger.error("未建立TCP连接,无法进行身份认证");
  97. return false;
  98. }
  99. try {
  100. // 1. 发送身份认证请求
  101. String requestXml = com.usky.ems.util.XmlBuilder.buildAuthRequest(buildingId, gatewayId);
  102. sendPacket(NetworkPacket.TYPE_AUTH, requestXml, false);
  103. // 2. 接收服务端返回的随机序列
  104. NetworkPacket response = receivePacket();
  105. if (response == null || response.getType() != NetworkPacket.TYPE_AUTH) {
  106. logger.error("接收身份认证响应失败");
  107. return false;
  108. }
  109. // 认证响应不加密
  110. String responseXml = new String(response.getData(), java.nio.charset.StandardCharsets.UTF_8);
  111. String sequence = parseSequenceFromXml(responseXml);
  112. if (sequence == null || sequence.isEmpty()) {
  113. logger.error("解析随机序列失败");
  114. return false;
  115. }
  116. // 3. 计算MD5:密钥 + 随机序列
  117. String md5 = com.usky.ems.util.Md5Util.md5(authKey + sequence);
  118. // 4. 发送MD5值
  119. String md5Xml = com.usky.ems.util.XmlBuilder.buildAuthMd5Request(buildingId, gatewayId, md5);
  120. sendPacket(NetworkPacket.TYPE_AUTH, md5Xml, false);
  121. // 5. 接收认证结果
  122. NetworkPacket result = receivePacket();
  123. if (result == null || result.getType() != NetworkPacket.TYPE_AUTH) {
  124. logger.error("接收认证结果失败");
  125. return false;
  126. }
  127. // 认证响应不加密
  128. String resultXml = new String(result.getData(), java.nio.charset.StandardCharsets.UTF_8);
  129. boolean success = parseAuthResult(resultXml);
  130. if (success) {
  131. authenticated.set(true);
  132. logger.info("身份认证成功");
  133. } else {
  134. logger.error("身份认证失败");
  135. }
  136. return success;
  137. } catch (Exception e) {
  138. logger.error("身份认证过程异常", e);
  139. return false;
  140. }
  141. }
  142. /**
  143. * 发送心跳
  144. *
  145. * @param buildingId 建筑ID
  146. * @param gatewayId 网关ID
  147. * @return 是否发送成功
  148. */
  149. public boolean sendHeartbeat(String buildingId, String gatewayId) {
  150. if (!connected.get() || !authenticated.get()) {
  151. logger.warn("连接未建立或未认证,无法发送心跳");
  152. return false;
  153. }
  154. try {
  155. String heartbeatXml = com.usky.ems.util.XmlBuilder.buildHeartbeatRequest(buildingId, gatewayId);
  156. sendPacket(NetworkPacket.TYPE_HEARTBEAT, heartbeatXml, false);
  157. NetworkPacket response1 = receivePacket();
  158. if (response1 != null && response1.getType() == NetworkPacket.TYPE_HEARTBEAT) {
  159. // 直接转换为可读XML(无需解密)
  160. String heartbeatResponseXml = new String(response1.getData(), java.nio.charset.StandardCharsets.UTF_8);
  161. System.out.println("可读报文:\n" + heartbeatResponseXml);
  162. boolean success = this.parseHeartbeatResult(heartbeatResponseXml);
  163. if (success) {
  164. logger.info("发送心跳成功");
  165. return true;
  166. }else{
  167. logger.info("发送心跳失败");
  168. return false;
  169. }
  170. }
  171. return false;
  172. } catch (Exception e) {
  173. logger.error("发送心跳失败", e);
  174. return false;
  175. }
  176. }
  177. /**
  178. * 发送能耗数据
  179. *
  180. * @param xmlData XML数据(未加密)
  181. * @return 是否发送成功
  182. */
  183. public boolean sendEnergyData(String xmlData) {
  184. if (!connected.get() || !authenticated.get()) {
  185. logger.warn("连接未建立或未认证,无法发送能耗数据");
  186. return false;
  187. }
  188. try {
  189. sendPacket(NetworkPacket.TYPE_ENERGY_DATA, xmlData, true);
  190. NetworkPacket response = receivePacket();
  191. if (response != null && response.getType() == NetworkPacket.TYPE_ENERGY_DATA) {
  192. // 直接转换为可读XML(无需解密)
  193. String readable = new String(response.getData(), java.nio.charset.StandardCharsets.UTF_8);
  194. System.out.println("可读报文:\n" + readable);
  195. boolean success = this.parseEnergyDataResult(readable);
  196. if (success) {
  197. logger.info("能耗数据发送成功");
  198. return true;
  199. }else{
  200. logger.info("能耗数据发送失败");
  201. return false;
  202. }
  203. }
  204. return false;
  205. } catch (Exception e) {
  206. logger.error("发送能耗数据失败", e);
  207. return false;
  208. }
  209. }
  210. /**
  211. * 将TCP报文字节数组直接转换为可读格式(无需解密)
  212. * @param dataBytes 原始报文字节数组
  213. * @param charset 报文字符编码(默认UTF-8)
  214. * @param format 报文格式(xml/json/plain,plain为普通文本)
  215. * @return 可读的报文字符串
  216. */
  217. public String convertToReadablePacket(byte[] dataBytes, String charset, String format) {
  218. if (dataBytes == null || dataBytes.length == 0) {
  219. return "空报文";
  220. }
  221. // 兜底处理字符编码,默认用UTF-8
  222. String actualCharset = (charset == null || charset.isEmpty()) ? "UTF-8" : charset;
  223. try {
  224. // 第一步:直接将字节数组转字符串(核心,无需解密)
  225. String rawText = new String(dataBytes, actualCharset);
  226. logger.debug("原始文本报文:{}", rawText);
  227. // 第二步:根据格式美化
  228. switch (format.toLowerCase()) {
  229. case "xml":
  230. return formatXml(rawText); // XML格式化(带缩进)
  231. case "json":
  232. return formatJson(rawText); // JSON格式化(带缩进)
  233. case "plain":
  234. default:
  235. return rawText; // 普通文本直接返回
  236. }
  237. } catch (Exception e) {
  238. logger.warn("转换为文本格式失败,返回十六进制格式:{}", e.getMessage());
  239. // 转换失败时,返回十六进制(所有二进制数据都能读)
  240. return bytesToHex(dataBytes);
  241. }
  242. }
  243. /**
  244. * XML格式化(美化缩进)
  245. */
  246. private String formatXml(String xmlText) throws DocumentException {
  247. Document document = DocumentHelper.parseText(xmlText);
  248. return document.asXML();
  249. }
  250. /**
  251. * JSON格式化(美化缩进)
  252. */
  253. private String formatJson(String jsonText) throws Exception {
  254. ObjectMapper mapper = new ObjectMapper();
  255. Object jsonObj = mapper.readValue(jsonText, Object.class);
  256. return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObj);
  257. }
  258. /**
  259. * 字节数组转十六进制字符串(兜底方案)
  260. * 格式:00 1A 2B ... (便于排查二进制数据问题)
  261. */
  262. private String bytesToHex(byte[] bytes) {
  263. StringBuilder sb = new StringBuilder();
  264. for (byte b : bytes) {
  265. sb.append(String.format("%02X ", b));
  266. }
  267. return "十六进制格式:" + sb.toString().trim();
  268. }
  269. /**
  270. * 发送数据包
  271. *
  272. * @param type 消息类型
  273. * @param xmlData XML数据
  274. * @param encrypt 是否加密
  275. */
  276. private void sendPacket(byte type, String xmlData, boolean encrypt) throws IOException {
  277. byte[] data;
  278. if (encrypt) {
  279. // 能耗数据需要AES加密
  280. try {
  281. data = AesUtil.encryptEnergyXml(xmlData, authKey);
  282. } catch (Exception e) {
  283. throw new IOException("加密能耗数据失败", e);
  284. }
  285. } else {
  286. // 身份认证和心跳不加密
  287. data = xmlData.getBytes(java.nio.charset.StandardCharsets.UTF_8);
  288. }
  289. NetworkPacket packet = new NetworkPacket();
  290. packet.setType(type);
  291. packet.setData(data);
  292. byte[] packetBytes = packet.encode();
  293. outputStream.write(packetBytes);
  294. outputStream.flush();
  295. }
  296. /**
  297. * 接收数据包
  298. *
  299. * @return 接收到的数据包
  300. */
  301. private NetworkPacket receivePacket() throws IOException {
  302. // 先读取7字节(Head + Type + Length)
  303. byte[] header = new byte[7];
  304. int bytesRead = 0;
  305. while (bytesRead < 7) {
  306. int n = inputStream.read(header, bytesRead, 7 - bytesRead);
  307. if (n == -1) {
  308. throw new IOException("连接已关闭");
  309. }
  310. bytesRead += n;
  311. }
  312. // 解析Length
  313. int length = ((header[3] & 0xFF) << 24) |
  314. ((header[4] & 0xFF) << 16) |
  315. ((header[5] & 0xFF) << 8) |
  316. (header[6] & 0xFF);
  317. // 读取Data
  318. byte[] data = new byte[length];
  319. bytesRead = 0;
  320. while (bytesRead < length) {
  321. int n = inputStream.read(data, bytesRead, length - bytesRead);
  322. if (n == -1) {
  323. throw new IOException("连接已关闭");
  324. }
  325. bytesRead += n;
  326. }
  327. // 组装完整数据包
  328. byte[] packetBytes = new byte[7 + length];
  329. System.arraycopy(header, 0, packetBytes, 0, 7);
  330. System.arraycopy(data, 0, packetBytes, 7, length);
  331. return NetworkPacket.decode(packetBytes);
  332. }
  333. /**
  334. * 从XML中解析随机序列
  335. */
  336. private String parseSequenceFromXml(String xml) {
  337. try {
  338. Document document = DocumentHelper.parseText(xml);
  339. Element root = document.getRootElement();
  340. Element idValidate = root.element("id_validate");
  341. if (idValidate != null) {
  342. Element sequence = idValidate.element("sequence");
  343. if (sequence != null) {
  344. return sequence.getTextTrim();
  345. }
  346. }
  347. } catch (Exception e) {
  348. logger.error("解析随机序列失败", e);
  349. }
  350. return null;
  351. }
  352. /**
  353. * 从XML中解析认证结果
  354. */
  355. private boolean parseAuthResult(String xml) {
  356. try {
  357. Document document = DocumentHelper.parseText(xml);
  358. Element root = document.getRootElement();
  359. Element idValidate = root.element("id_validate");
  360. if (idValidate != null && "result".equals(idValidate.attributeValue("operation"))) {
  361. Element result = idValidate.element("result");
  362. if (result != null) {
  363. return "pass".equals(result.getTextTrim());
  364. }
  365. }
  366. } catch (Exception e) {
  367. logger.error("解析认证结果失败", e);
  368. }
  369. return false;
  370. }
  371. /**
  372. * 从XML中解析推送能耗数据响应结果
  373. */
  374. private boolean parseEnergyDataResult(String xml) {
  375. try {
  376. Document document = DocumentHelper.parseText(xml);
  377. Element root = document.getRootElement();
  378. Element dataElement = root.element("data");
  379. if (dataElement == null) {
  380. logger.warn("XML中未找到<data>节点");
  381. }
  382. // 4. 获取<ack>子节点的文本值
  383. Element ackElement = dataElement.element("ack");
  384. if (ackElement == null) {
  385. logger.warn("XML中未找到<ack>节点");
  386. }
  387. return "OK".equals(ackElement.getTextTrim());
  388. } catch (Exception e) {
  389. logger.error("解析认证结果失败", e);
  390. }
  391. return false;
  392. }
  393. /**
  394. * 从XML中解析推送能耗数据响应结果
  395. */
  396. private boolean parseHeartbeatResult(String xml) {
  397. try {
  398. Document document = DocumentHelper.parseText(xml);
  399. Element root = document.getRootElement();
  400. Element dataElement = root.element("heart_beat");
  401. if (dataElement == null) {
  402. logger.warn("XML中未找到<heart_beat>节点");
  403. }
  404. // 4. 获取<ack>子节点的文本值
  405. Element ackElement = dataElement.element("time");
  406. if (ackElement == null) {
  407. logger.warn("XML中未找到<time>节点");
  408. }
  409. return StringUtils.isNotBlank(ackElement.getTextTrim());
  410. } catch (Exception e) {
  411. logger.error("解析认证结果失败", e);
  412. }
  413. return false;
  414. }
  415. public boolean isConnected() {
  416. return connected.get() && socket != null && !socket.isClosed();
  417. }
  418. public boolean isAuthenticated() {
  419. return authenticated.get();
  420. }
  421. }