You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

198 lines
7.6 KiB

5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
  1. package com.qyft.ms.device.client;
  2. import cn.hutool.json.JSONUtil;
  3. import com.qyft.ms.device.common.jsonrpc.JsonRpcRequest;
  4. import com.qyft.ms.device.config.TcpConfig;
  5. import com.qyft.ms.device.handler.DeviceMessageHandler;
  6. import com.qyft.ms.device.model.bo.DeviceFeedback;
  7. import io.netty.bootstrap.Bootstrap;
  8. import io.netty.buffer.Unpooled;
  9. import io.netty.channel.*;
  10. import io.netty.channel.nio.NioEventLoopGroup;
  11. import io.netty.channel.socket.nio.NioSocketChannel;
  12. import io.netty.handler.codec.json.JsonObjectDecoder;
  13. import io.netty.util.CharsetUtil;
  14. import jakarta.annotation.PostConstruct;
  15. import lombok.RequiredArgsConstructor;
  16. import lombok.extern.slf4j.Slf4j;
  17. import org.springframework.scheduling.annotation.Scheduled;
  18. import org.springframework.stereotype.Component;
  19. import java.net.InetSocketAddress;
  20. import java.util.HashMap;
  21. import java.util.Map;
  22. import java.util.UUID;
  23. import java.util.concurrent.CompletableFuture;
  24. import java.util.concurrent.TimeUnit;
  25. @Slf4j
  26. @Component
  27. @RequiredArgsConstructor
  28. public class TcpClient {
  29. private final TcpConfig tcpConfig;
  30. private final DeviceMessageHandler deviceMessageHandler;
  31. private final EventLoopGroup group = new NioEventLoopGroup();
  32. private Channel channel;
  33. private Bootstrap bootstrap;
  34. // 初始化方法,在Spring容器启动后调用
  35. @PostConstruct
  36. public void init() {
  37. if (tcpConfig.isEnable()) {
  38. connect();
  39. }
  40. }
  41. // 连接到TCP服务器的方法
  42. public void connect() {
  43. try {
  44. // 如果Bootstrap对象为空,则初始化它
  45. if (bootstrap == null) {
  46. bootstrap = new Bootstrap();
  47. bootstrap.group(group)
  48. .channel(NioSocketChannel.class)
  49. .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, tcpConfig.getTimeout())
  50. .handler(new ChannelInitializer<>() {
  51. @Override
  52. protected void initChannel(Channel ch) {
  53. // 添加JSON对象解码器到ChannelPipeline
  54. ch.pipeline().addLast(new JsonObjectDecoder());
  55. // 添加设备消息处理器到ChannelPipeline
  56. ch.pipeline().addLast(deviceMessageHandler);
  57. // 添加TCP连接处理器到ChannelPipeline
  58. ch.pipeline().addLast(new TcpConnectionHandler());
  59. }
  60. });
  61. }
  62. // 记录尝试连接到TCP服务器的日志
  63. log.info("尝试连接到TCP服务 {}:{}", tcpConfig.getHost(), tcpConfig.getPort());
  64. ChannelFuture future = bootstrap.connect(new InetSocketAddress(tcpConfig.getHost(), tcpConfig.getPort()));
  65. // 添加连接监听器
  66. future.addListener((ChannelFutureListener) f -> {
  67. if (f.isSuccess()) {
  68. // 连接成功,记录日志并设置channel
  69. channel = f.channel();
  70. log.info("已链接到TCP服务");
  71. } else {
  72. // 连接失败,记录日志并安排重试
  73. log.error("无法连接到TCP服务. {}ms后重试...", tcpConfig.getReconnect());
  74. f.channel().eventLoop().schedule(this::connect, tcpConfig.getReconnect(), TimeUnit.MILLISECONDS);
  75. }
  76. });
  77. } catch (Exception e) {
  78. // 捕获并记录连接过程中发生的异常
  79. log.error("尝试连接到TCP服务发生意外错误: {}", e.getMessage(), e);
  80. }
  81. }
  82. // 定时检查TCP连接的方法
  83. @Scheduled(fixedRateString = "${tcp.reconnect}")
  84. public void checkConnection() {
  85. if (channel == null || !channel.isActive()) {
  86. // 如果连接丢失,记录日志并尝试重新连接
  87. log.error("TCP服务链接丢失");
  88. connect();
  89. }
  90. }
  91. /**
  92. * 将obj转换成json后发送
  93. */
  94. public boolean sendToJSON(Object request) {
  95. String msg = JSONUtil.toJsonStr(request);
  96. return this.send(msg);
  97. }
  98. // 发送字符串请求到TCP服务器的方法
  99. public boolean send(String request) {
  100. if (channel != null && channel.isActive()) {
  101. // 如果连接有效,发送请求并返回true
  102. channel.writeAndFlush(Unpooled.copiedBuffer(request, CharsetUtil.UTF_8));
  103. return true;
  104. } else {
  105. // 如果连接无效,记录日志并返回false
  106. log.error("TCP服务未连接,无法发送请求: {}", request);
  107. return false;
  108. }
  109. }
  110. // 发送JSON-RPC命令到TCP服务器的方法,仅包含方法名
  111. public DeviceFeedback sendCommand(String method) {
  112. JsonRpcRequest request = new JsonRpcRequest();
  113. request.setMethod(method);
  114. return this.sendCommand(request);
  115. }
  116. // 发送JSON-RPC命令到TCP服务器的方法,包含方法名和参数
  117. public DeviceFeedback sendCommand(String method, Map<String, Object> params) {
  118. JsonRpcRequest request = new JsonRpcRequest();
  119. request.setMethod(method);
  120. request.setParams(params);
  121. return this.sendCommand(request);
  122. }
  123. // 发送JSON-RPC命令到TCP服务器的方法,包含完整的JsonRpcRequest对象
  124. public DeviceFeedback sendCommand(JsonRpcRequest request) {
  125. if (request.getId() == null) {
  126. // 如果请求ID为空,则生成一个新的UUID作为ID
  127. request.setId(UUID.randomUUID().toString());
  128. }
  129. CompletableFuture<DeviceFeedback> future = new CompletableFuture<>();
  130. deviceMessageHandler.responseMap.put(request.getId(), future);
  131. try {
  132. if (request.getParams() == null) {
  133. // 如果请求参数为空,则初始化一个空的HashMap
  134. request.setParams(new HashMap<>());
  135. }
  136. // 将请求对象转换为JSON字符串
  137. String requestJsonStr = JSONUtil.toJsonStr(request);
  138. log.info("发送TCP指令(同步) {}", requestJsonStr);
  139. // 发送请求到服务器,并等待响应
  140. if (this.send(requestJsonStr)) {
  141. return future.get(tcpConfig.getFeedbackTimeout(), TimeUnit.MILLISECONDS); // 等待 FEEDBACK 响应
  142. } else {
  143. return null;
  144. }
  145. } catch (Exception e) {
  146. // 捕获并记录发送过程中发生的异常
  147. log.error("发送TCP指令错误(同步) {}", JSONUtil.toJsonStr(request), e);
  148. } finally {
  149. // 确保请求完成后从responseMap中移除对应的Future
  150. deviceMessageHandler.responseMap.remove(request.getId());
  151. }
  152. return null;
  153. }
  154. // 内部类,处理TCP连接事件
  155. private class TcpConnectionHandler extends ChannelInboundHandlerAdapter {
  156. // 处理连接断开事件
  157. @Override
  158. public void channelInactive(ChannelHandlerContext ctx) throws Exception {
  159. // 记录连接断开的日志
  160. log.error("TCP连接丢失,准备重新连接...");
  161. if (channel != null) {
  162. // 关闭当前channel
  163. channel.close();
  164. }
  165. // 尝试重新连接
  166. connect();
  167. super.channelInactive(ctx);
  168. }
  169. // 处理异常事件
  170. @Override
  171. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
  172. // 记录异常日志
  173. log.error("TCP连接发生异常: {}", cause.getMessage(), cause);
  174. // 关闭当前channel
  175. ctx.close();
  176. }
  177. }
  178. }