TcpClient.java 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. package com.usky.ems.protocol;
  2. import com.usky.ems.util.AesUtil;
  3. import org.dom4j.Document;
  4. import org.dom4j.DocumentHelper;
  5. import org.dom4j.Element;
  6. import org.slf4j.Logger;
  7. import org.slf4j.LoggerFactory;
  8. import java.io.IOException;
  9. import java.io.InputStream;
  10. import java.io.OutputStream;
  11. import java.net.Socket;
  12. import java.util.concurrent.atomic.AtomicBoolean;
  13. /**
  14. * TCP客户端
  15. * 用于与能耗监管系统建立连接并发送数据
  16. *
  17. * @author system
  18. * @since 2024-01-01
  19. */
  20. public class TcpClient {
  21. private static final Logger logger = LoggerFactory.getLogger(TcpClient.class);
  22. private String host;
  23. private int port;
  24. private String authKey;
  25. private Socket socket;
  26. private InputStream inputStream;
  27. private OutputStream outputStream;
  28. private AtomicBoolean connected = new AtomicBoolean(false);
  29. private AtomicBoolean authenticated = new AtomicBoolean(false);
  30. public TcpClient(String host, int port, String authKey) {
  31. this.host = host;
  32. this.port = port;
  33. this.authKey = authKey;
  34. }
  35. /**
  36. * 建立TCP连接
  37. *
  38. * @return 是否连接成功
  39. */
  40. public boolean connect() {
  41. try {
  42. if (socket != null && !socket.isClosed()) {
  43. socket.close();
  44. }
  45. socket = new Socket(host, port);
  46. socket.setSoTimeout(30000); // 30秒超时
  47. inputStream = socket.getInputStream();
  48. outputStream = socket.getOutputStream();
  49. connected.set(true);
  50. authenticated.set(false);
  51. logger.info("TCP连接成功: {}:{}", host, port);
  52. return true;
  53. } catch (IOException e) {
  54. logger.error("TCP连接失败: {}:{}", host, port, e);
  55. connected.set(false);
  56. return false;
  57. }
  58. }
  59. /**
  60. * 关闭连接
  61. */
  62. public void close() {
  63. try {
  64. if (inputStream != null) {
  65. inputStream.close();
  66. }
  67. if (outputStream != null) {
  68. outputStream.close();
  69. }
  70. if (socket != null && !socket.isClosed()) {
  71. socket.close();
  72. }
  73. connected.set(false);
  74. authenticated.set(false);
  75. logger.info("TCP连接已关闭");
  76. } catch (IOException e) {
  77. logger.error("关闭TCP连接失败", e);
  78. }
  79. }
  80. /**
  81. * 身份认证
  82. *
  83. * @param buildingId 建筑ID
  84. * @param gatewayId 网关ID
  85. * @return 是否认证成功
  86. */
  87. public boolean authenticate(String buildingId, String gatewayId) {
  88. if (!connected.get()) {
  89. logger.error("未建立TCP连接,无法进行身份认证");
  90. return false;
  91. }
  92. try {
  93. // 1. 发送身份认证请求
  94. String requestXml = com.usky.ems.util.XmlBuilder.buildAuthRequest(buildingId, gatewayId);
  95. sendPacket(NetworkPacket.TYPE_AUTH, requestXml, false);
  96. // 2. 接收服务端返回的随机序列
  97. NetworkPacket response = receivePacket();
  98. if (response == null || response.getType() != NetworkPacket.TYPE_AUTH) {
  99. logger.error("接收身份认证响应失败");
  100. return false;
  101. }
  102. // 认证响应不加密
  103. String responseXml = new String(response.getData(), java.nio.charset.StandardCharsets.UTF_8);
  104. String sequence = parseSequenceFromXml(responseXml);
  105. if (sequence == null || sequence.isEmpty()) {
  106. logger.error("解析随机序列失败");
  107. return false;
  108. }
  109. // 3. 计算MD5:密钥 + 随机序列
  110. String md5 = com.usky.ems.util.Md5Util.md5(authKey + sequence);
  111. // 4. 发送MD5值
  112. String md5Xml = com.usky.ems.util.XmlBuilder.buildAuthMd5Request(buildingId, gatewayId, md5);
  113. sendPacket(NetworkPacket.TYPE_AUTH, md5Xml, false);
  114. // 5. 接收认证结果
  115. NetworkPacket result = receivePacket();
  116. if (result == null || result.getType() != NetworkPacket.TYPE_AUTH) {
  117. logger.error("接收认证结果失败");
  118. return false;
  119. }
  120. // 认证响应不加密
  121. String resultXml = new String(result.getData(), java.nio.charset.StandardCharsets.UTF_8);
  122. boolean success = parseAuthResult(resultXml);
  123. if (success) {
  124. authenticated.set(true);
  125. logger.info("身份认证成功");
  126. } else {
  127. logger.error("身份认证失败");
  128. }
  129. return success;
  130. } catch (Exception e) {
  131. logger.error("身份认证过程异常", e);
  132. return false;
  133. }
  134. }
  135. /**
  136. * 发送心跳
  137. *
  138. * @param buildingId 建筑ID
  139. * @param gatewayId 网关ID
  140. * @return 是否发送成功
  141. */
  142. public boolean sendHeartbeat(String buildingId, String gatewayId) {
  143. if (!connected.get() || !authenticated.get()) {
  144. logger.warn("连接未建立或未认证,无法发送心跳");
  145. return false;
  146. }
  147. try {
  148. String heartbeatXml = com.usky.ems.util.XmlBuilder.buildHeartbeatRequest(buildingId, gatewayId);
  149. sendPacket(NetworkPacket.TYPE_HEARTBEAT, heartbeatXml, false);
  150. logger.debug("心跳发送成功");
  151. return true;
  152. } catch (Exception e) {
  153. logger.error("发送心跳失败", e);
  154. return false;
  155. }
  156. }
  157. /**
  158. * 发送能耗数据
  159. *
  160. * @param xmlData XML数据(未加密)
  161. * @return 是否发送成功
  162. */
  163. public boolean sendEnergyData(String xmlData) {
  164. if (!connected.get() || !authenticated.get()) {
  165. logger.warn("连接未建立或未认证,无法发送能耗数据");
  166. return false;
  167. }
  168. try {
  169. sendPacket(NetworkPacket.TYPE_ENERGY_DATA, xmlData, true);
  170. logger.info("能耗数据发送成功");
  171. // 接收服务端响应
  172. NetworkPacket response = receivePacket();
  173. if (response != null && response.getType() == NetworkPacket.TYPE_ENERGY_DATA) {
  174. // 能耗数据响应需要解密
  175. try {
  176. byte[] responseData = response.getData();
  177. String responseXml = null;
  178. Exception lastException = null;
  179. // 方法1:首先尝试直接解密字节数组
  180. try {
  181. responseXml = AesUtil.decrypt(authKey,responseData);
  182. logger.debug("直接解密字节数组成功");
  183. } catch (Exception e1) {
  184. lastException = e1;
  185. logger.debug("直接解密字节数组失败: {}", e1.getMessage());
  186. // 方法2:如果直接解密失败,尝试作为Base64字符串处理
  187. // 先检查数据是否可能是Base64字符串(只包含Base64字符)
  188. if (responseXml == null) {
  189. try {
  190. String dataString = new String(responseData, java.nio.charset.StandardCharsets.UTF_8);
  191. // 检查是否只包含Base64字符(A-Z, a-z, 0-9, +, /, =)
  192. if (dataString.matches("^[A-Za-z0-9+/=]+$")) {
  193. // Base64解码后再解密
  194. byte[] decodedBytes = java.util.Base64.getDecoder().decode(dataString);
  195. responseXml = AesUtil.decrypt(authKey,decodedBytes);
  196. logger.debug("Base64解码后解密成功");
  197. } else {
  198. logger.debug("数据不是Base64格式,跳过Base64解码");
  199. }
  200. } catch (Exception e2) {
  201. lastException = e2;
  202. logger.debug("Base64解码后解密失败: {}", e2.getMessage());
  203. }
  204. }
  205. }
  206. if (responseXml != null) {
  207. logger.debug("服务端响应: {}", responseXml);
  208. } else {
  209. throw lastException != null ? lastException : new Exception("所有解密方法都失败");
  210. }
  211. } catch (Exception e) {
  212. logger.warn("解密服务端响应失败", e);
  213. logger.debug("响应数据长度: {}, 前100字节: {}",
  214. response.getData() != null ? response.getData().length : 0,
  215. response.getData() != null && response.getData().length > 0
  216. ? java.util.Arrays.toString(java.util.Arrays.copyOf(response.getData(), Math.min(100, response.getData().length)))
  217. : "null");
  218. }
  219. return true;
  220. }
  221. return true;
  222. } catch (Exception e) {
  223. logger.error("发送能耗数据失败", e);
  224. return false;
  225. }
  226. }
  227. /**
  228. * 发送数据包
  229. *
  230. * @param type 消息类型
  231. * @param xmlData XML数据
  232. * @param encrypt 是否加密
  233. */
  234. private void sendPacket(byte type, String xmlData, boolean encrypt) throws IOException {
  235. byte[] data;
  236. if (encrypt) {
  237. // 能耗数据需要AES加密
  238. try {
  239. data = AesUtil.encryptEnergyXml(xmlData, authKey);
  240. } catch (Exception e) {
  241. throw new IOException("加密能耗数据失败", e);
  242. }
  243. } else {
  244. // 身份认证和心跳不加密
  245. data = xmlData.getBytes(java.nio.charset.StandardCharsets.UTF_8);
  246. }
  247. NetworkPacket packet = new NetworkPacket();
  248. packet.setType(type);
  249. packet.setData(data);
  250. byte[] packetBytes = packet.encode();
  251. outputStream.write(packetBytes);
  252. outputStream.flush();
  253. }
  254. /**
  255. * 接收数据包
  256. *
  257. * @return 接收到的数据包
  258. */
  259. private NetworkPacket receivePacket() throws IOException {
  260. // 先读取7字节(Head + Type + Length)
  261. byte[] header = new byte[7];
  262. int bytesRead = 0;
  263. while (bytesRead < 7) {
  264. int n = inputStream.read(header, bytesRead, 7 - bytesRead);
  265. if (n == -1) {
  266. throw new IOException("连接已关闭");
  267. }
  268. bytesRead += n;
  269. }
  270. // 解析Length
  271. int length = ((header[3] & 0xFF) << 24) |
  272. ((header[4] & 0xFF) << 16) |
  273. ((header[5] & 0xFF) << 8) |
  274. (header[6] & 0xFF);
  275. // 读取Data
  276. byte[] data = new byte[length];
  277. bytesRead = 0;
  278. while (bytesRead < length) {
  279. int n = inputStream.read(data, bytesRead, length - bytesRead);
  280. if (n == -1) {
  281. throw new IOException("连接已关闭");
  282. }
  283. bytesRead += n;
  284. }
  285. // 组装完整数据包
  286. byte[] packetBytes = new byte[7 + length];
  287. System.arraycopy(header, 0, packetBytes, 0, 7);
  288. System.arraycopy(data, 0, packetBytes, 7, length);
  289. return NetworkPacket.decode(packetBytes);
  290. }
  291. /**
  292. * 从XML中解析随机序列
  293. */
  294. private String parseSequenceFromXml(String xml) {
  295. try {
  296. Document document = DocumentHelper.parseText(xml);
  297. Element root = document.getRootElement();
  298. Element idValidate = root.element("id_validate");
  299. if (idValidate != null) {
  300. Element sequence = idValidate.element("sequence");
  301. if (sequence != null) {
  302. return sequence.getTextTrim();
  303. }
  304. }
  305. } catch (Exception e) {
  306. logger.error("解析随机序列失败", e);
  307. }
  308. return null;
  309. }
  310. /**
  311. * 从XML中解析认证结果
  312. */
  313. private boolean parseAuthResult(String xml) {
  314. try {
  315. Document document = DocumentHelper.parseText(xml);
  316. Element root = document.getRootElement();
  317. Element idValidate = root.element("id_validate");
  318. if (idValidate != null && "result".equals(idValidate.attributeValue("operation"))) {
  319. Element result = idValidate.element("result");
  320. if (result != null) {
  321. return "pass".equals(result.getTextTrim());
  322. }
  323. }
  324. } catch (Exception e) {
  325. logger.error("解析认证结果失败", e);
  326. }
  327. return false;
  328. }
  329. public boolean isConnected() {
  330. return connected.get() && socket != null && !socket.isClosed();
  331. }
  332. public boolean isAuthenticated() {
  333. return authenticated.get();
  334. }
  335. }