From 80836e34ea03a30f75d9bfebd5d5d4fe9a56a014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dhji=28=EC=A7=80=EB=8C=80=ED=95=9C=29?= Date: Fri, 23 Feb 2024 13:40:22 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20websocket=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kac/socket/core/codec/DronEncoder.java | 9 +- .../command/impl/AdsbDronCommandImpl.java | 14 +-- .../command/impl/AntosDronCommandImpl.java | 14 +-- .../command/impl/SandboxDronCommandImpl.java | 12 +- .../kac/socket/core/config/AppReadyEvent.java | 4 +- .../kac/socket/core/config/NettyConfig.java | 20 ++-- ...ronSocketHandler.java => DronHandler.java} | 8 +- ...alizer.java => TcpChannelInitializer.java} | 6 +- ...ServerSocket.java => TcpSocketServer.java} | 2 +- .../AuthKeyStorage.java} | 11 +- .../ControlStorage.java} | 12 +- .../DronStorage.java} | 15 +-- .../src/main/resources/application.yml | 13 +- app/kac-websocket-app/build.gradle | 8 ++ .../websocket/KacWebSocketApplication.java | 14 +++ .../controller/SocketReceiverController.java | 37 ++++++ .../kac/websocket/core/codec/Decoder.java | 25 ++++ .../kac/websocket/core/codec/Encoder.java | 26 ++++ .../websocket/core/config/AppReadyEvent.java | 22 ++++ .../websocket/core/config/NettyConfig.java | 80 +++++++++++++ .../core/handler/WebSocketHandler.java | 42 +++++++ .../kac/websocket/core/model/ControlDTO.java | 86 +++++++++++++ .../kac/websocket/core/model/DronDTO.java | 96 +++++++++++++++ .../websocket/core/model/DronHistoryDTO.java | 20 ++++ .../core/socket/DronChannelInitializer.java | 41 +++++++ .../core/socket/WebSocketServer.java | 45 +++++++ .../core/storage/ChannelStorage.java | 41 +++++++ .../core/storage/ControlStorage.java | 83 +++++++++++++ .../kac/websocket/core/util/DronUtil.java | 10 ++ .../kac/websocket/service/ControlService.java | 113 ++++++++++++++++++ .../websocket/service/ScheduledService.java | 57 +++++++++ .../src/main/resources/application.yml | 30 +++++ 32 files changed, 950 insertions(+), 66 deletions(-) rename app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/handler/{DronSocketHandler.java => DronHandler.java} (92%) rename app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/{DronChannelInitializer.java => TcpChannelInitializer.java} (87%) rename app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/{DronServerSocket.java => TcpSocketServer.java} (97%) rename app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/{util/AuthKeyUtil.java => storage/AuthKeyStorage.java} (83%) rename app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/{util/ControlCacheUtil.java => storage/ControlStorage.java} (78%) rename app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/{util/DronCacheUtil.java => storage/DronStorage.java} (83%) create mode 100644 app/kac-websocket-app/build.gradle create mode 100644 app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/KacWebSocketApplication.java create mode 100644 app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/controller/SocketReceiverController.java create mode 100644 app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/codec/Decoder.java create mode 100644 app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/codec/Encoder.java create mode 100644 app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/config/AppReadyEvent.java create mode 100644 app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/config/NettyConfig.java create mode 100644 app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/handler/WebSocketHandler.java create mode 100644 app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/ControlDTO.java create mode 100644 app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/DronDTO.java create mode 100644 app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/DronHistoryDTO.java create mode 100644 app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/socket/DronChannelInitializer.java create mode 100644 app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/socket/WebSocketServer.java create mode 100644 app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/storage/ChannelStorage.java create mode 100644 app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/storage/ControlStorage.java create mode 100644 app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/util/DronUtil.java create mode 100644 app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/service/ControlService.java create mode 100644 app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/service/ScheduledService.java create mode 100644 app/kac-websocket-app/src/main/resources/application.yml diff --git a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DronEncoder.java b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DronEncoder.java index bbc705a..cf3fddf 100644 --- a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DronEncoder.java +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DronEncoder.java @@ -3,7 +3,6 @@ package kr.co.palnet.kac.socket.core.codec; import com.fasterxml.jackson.databind.ObjectMapper; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToMessageEncoder; -import kr.co.palnet.kac.socket.core.model.DronRS; import kr.co.palnet.kac.util.ObjectMapperUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -12,16 +11,16 @@ import java.util.List; @Slf4j @RequiredArgsConstructor -public class DronEncoder extends MessageToMessageEncoder { +public class DronEncoder extends MessageToMessageEncoder { private final ObjectMapper objectMapper = ObjectMapperUtils.getObjectMapper(); @Override - protected void encode(ChannelHandlerContext ctx, DronRS msg, List out) throws Exception { + protected void encode(ChannelHandlerContext ctx, Object msg, List out) throws Exception { log.info(">>>>> encode <<<"); try { - String json = objectMapper.writeValueAsString(msg); - out.add(json); +// String json = objectMapper.writeValueAsString(msg); +// out.add(json); } catch (Exception e) { log.warn("json parsing error : {} :: {}", e.getMessage(), msg); } diff --git a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/AdsbDronCommandImpl.java b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/AdsbDronCommandImpl.java index 7e1e7af..59a2fd7 100644 --- a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/AdsbDronCommandImpl.java +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/AdsbDronCommandImpl.java @@ -5,8 +5,8 @@ import kr.co.palnet.kac.socket.core.model.DronDTO; import kr.co.palnet.kac.socket.core.model.DronHistoryDTO; import kr.co.palnet.kac.socket.core.model.DronRQ; import kr.co.palnet.kac.socket.core.command.DronCommand; -import kr.co.palnet.kac.socket.core.util.ControlCacheUtil; -import kr.co.palnet.kac.socket.core.util.DronCacheUtil; +import kr.co.palnet.kac.socket.core.storage.ControlStorage; +import kr.co.palnet.kac.socket.core.storage.DronStorage; import kr.co.palnet.kac.socket.core.util.DronUtil; import kr.co.palnet.kac.socket.service.KacAppService; import kr.co.palnet.kac.socket.service.WebSocketService; @@ -83,7 +83,7 @@ public class AdsbDronCommandImpl implements DronCommand { // STEP 1. 전에 내부 메모리에서 controlId 조회 - ControlCacheUtil controlCacheUtil = ControlCacheUtil.getInstance(); + ControlStorage controlCacheUtil = ControlStorage.getInstance(); ControlDto dronCacheDTO = controlCacheUtil.get(dron.getObjectId()); if (dronCacheDTO == null) { @@ -119,7 +119,7 @@ public class AdsbDronCommandImpl implements DronCommand { controlDto.setRegTime(System.currentTimeMillis()); ControlDto newDronCacheDTO = ControlDto.builder().build(); - controlCacheUtil.set(dron.getObjectId(), newDronCacheDTO); + controlCacheUtil.put(dron.getObjectId(), newDronCacheDTO); } catch (IOException e) { log.error("요청한 URL 정보가 잘못되었습니다.", e.getMessage()); @@ -134,13 +134,13 @@ public class AdsbDronCommandImpl implements DronCommand { dron.setControlWarnCd(dronCacheDTO.isControlWarnCd()); dronCacheDTO.setRegTime(System.currentTimeMillis()); - controlCacheUtil.set(dron.getObjectId(), dronCacheDTO); + controlCacheUtil.put(dron.getObjectId(), dronCacheDTO); } // STEP 2. 이력 생성할 전문 전달 -> DRON의 대한 식별정보만 이력 관리 try { // 저장 해 놓았다가 한거번에 전송 - DronCacheUtil dronCacheUtil = DronCacheUtil.getInstance(); - dronCacheUtil.set(dron); + DronStorage dronCacheUtil = DronStorage.getInstance(); + dronCacheUtil.add(dron); } catch (Exception e) { log.error("ERROR : {}\n{}", e.getMessage(), e.getStackTrace()); } diff --git a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/AntosDronCommandImpl.java b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/AntosDronCommandImpl.java index dc8d91a..e256df3 100644 --- a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/AntosDronCommandImpl.java +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/AntosDronCommandImpl.java @@ -5,8 +5,8 @@ import kr.co.palnet.kac.socket.core.model.DronDTO; import kr.co.palnet.kac.socket.core.model.DronHistoryDTO; import kr.co.palnet.kac.socket.core.model.DronRQ; import kr.co.palnet.kac.socket.core.command.DronCommand; -import kr.co.palnet.kac.socket.core.util.ControlCacheUtil; -import kr.co.palnet.kac.socket.core.util.DronCacheUtil; +import kr.co.palnet.kac.socket.core.storage.ControlStorage; +import kr.co.palnet.kac.socket.core.storage.DronStorage; import kr.co.palnet.kac.socket.core.util.DronUtil; import kr.co.palnet.kac.socket.service.KacAppService; import kr.co.palnet.kac.socket.service.WebSocketService; @@ -82,7 +82,7 @@ public class AntosDronCommandImpl implements DronCommand { // STEP 1. 전에 내부 메모리에서 controlId 조회 - ControlCacheUtil controlCacheUtil = ControlCacheUtil.getInstance(); + ControlStorage controlCacheUtil = ControlStorage.getInstance(); ControlDto dronCacheDTO = controlCacheUtil.get(dron.getObjectId()); if (dronCacheDTO == null) { @@ -117,7 +117,7 @@ public class AntosDronCommandImpl implements DronCommand { controlDto.setRegTime(System.currentTimeMillis()); ControlDto newDronCacheDTO = ControlDto.builder().build(); - controlCacheUtil.set(dron.getObjectId(), newDronCacheDTO); + controlCacheUtil.put(dron.getObjectId(), newDronCacheDTO); } catch (IOException e) { log.error("요청한 URL 정보가 잘못되었습니다.", e.getMessage()); @@ -132,13 +132,13 @@ public class AntosDronCommandImpl implements DronCommand { dron.setControlWarnCd(dronCacheDTO.isControlWarnCd()); dronCacheDTO.setRegTime(System.currentTimeMillis()); - controlCacheUtil.set(dron.getObjectId(), dronCacheDTO); + controlCacheUtil.put(dron.getObjectId(), dronCacheDTO); } // STEP 2. 이력 생성할 전문 전달 -> DRON의 대한 식별정보만 이력 관리 try { // 저장 해 놓았다가 한거번에 전송 - DronCacheUtil dronCacheUtil = DronCacheUtil.getInstance(); - dronCacheUtil.set(dron); + DronStorage dronCacheUtil = DronStorage.getInstance(); + dronCacheUtil.add(dron); } catch (Exception e) { log.error("ERROR : {}\n{}", e.getMessage(), e.getStackTrace()); } diff --git a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/SandboxDronCommandImpl.java b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/SandboxDronCommandImpl.java index b69bcec..bf26667 100644 --- a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/SandboxDronCommandImpl.java +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/SandboxDronCommandImpl.java @@ -5,8 +5,8 @@ import kr.co.palnet.kac.socket.core.model.ControlDto; import kr.co.palnet.kac.socket.core.model.DronDTO; import kr.co.palnet.kac.socket.core.model.DronHistoryDTO; import kr.co.palnet.kac.socket.core.model.DronRQ; -import kr.co.palnet.kac.socket.core.util.ControlCacheUtil; -import kr.co.palnet.kac.socket.core.util.DronCacheUtil; +import kr.co.palnet.kac.socket.core.storage.ControlStorage; +import kr.co.palnet.kac.socket.core.storage.DronStorage; import kr.co.palnet.kac.socket.core.util.DronUtil; import kr.co.palnet.kac.socket.service.KacAppService; import kr.co.palnet.kac.socket.service.WebSocketService; @@ -85,7 +85,7 @@ public class SandboxDronCommandImpl implements DronCommand { Long start = System.currentTimeMillis(); // STEP 1. 전에 내부 메모리에서 controlId 조회 - ControlCacheUtil controlCacheUtil = ControlCacheUtil.getInstance(); + ControlStorage controlCacheUtil = ControlStorage.getInstance(); ControlDto dronCacheDTO = controlCacheUtil.get(dron.getObjectId()); if (dronCacheDTO == null) { @@ -121,7 +121,7 @@ public class SandboxDronCommandImpl implements DronCommand { controlDto.setRegTime(System.currentTimeMillis()); ControlDto newDronCacheDTO = ControlDto.builder().build(); - controlCacheUtil.set(dron.getObjectId(), newDronCacheDTO); + controlCacheUtil.put(dron.getObjectId(), newDronCacheDTO); } catch (IOException e) { log.error("요청한 URL 정보가 잘못되었습니다.", e.getMessage()); @@ -136,7 +136,7 @@ public class SandboxDronCommandImpl implements DronCommand { dron.setControlWarnCd(dronCacheDTO.isControlWarnCd()); dronCacheDTO.setRegTime(System.currentTimeMillis()); - controlCacheUtil.set(dron.getObjectId(), dronCacheDTO); + controlCacheUtil.put(dron.getObjectId(), dronCacheDTO); } @@ -144,7 +144,7 @@ public class SandboxDronCommandImpl implements DronCommand { // STEP 2. 이력 생성할 전문 전달 -> DRON의 대한 식별정보만 이력 관리 try { // 저장 해 놓았다가 한거번에 전송 - DronCacheUtil dronCacheUtil = DronCacheUtil.getInstance(); + DronStorage dronCacheUtil = DronStorage.getInstance(); // dronCacheUtil.set(dron); } catch (Exception e) { log.error("ERROR : {}\n{}", e.getMessage(), e.getStackTrace()); diff --git a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/AppReadyEvent.java b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/AppReadyEvent.java index c7ffa72..0a88738 100644 --- a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/AppReadyEvent.java +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/AppReadyEvent.java @@ -1,6 +1,6 @@ package kr.co.palnet.kac.socket.core.config; -import kr.co.palnet.kac.socket.core.socket.DronServerSocket; +import kr.co.palnet.kac.socket.core.socket.TcpSocketServer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -12,7 +12,7 @@ import org.springframework.stereotype.Component; @Component public class AppReadyEvent implements ApplicationListener { - private final DronServerSocket socketServer; + private final TcpSocketServer socketServer; @Override public void onApplicationEvent(ApplicationReadyEvent event) { diff --git a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/NettyConfig.java b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/NettyConfig.java index e404924..f0d6b1f 100644 --- a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/NettyConfig.java +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/NettyConfig.java @@ -6,7 +6,7 @@ import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; -import kr.co.palnet.kac.socket.core.socket.DronChannelInitializer; +import kr.co.palnet.kac.socket.core.socket.TcpChannelInitializer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -23,15 +23,15 @@ public class NettyConfig { private int port; @Value("${netty.socket.boss-count}") private int bossCount; - @Value("${netty.socket.worker-count}") - private int workerCount; @Value("${netty.socket.keep-alive}") private boolean keepAlive; + @Value("${netty.socket.tcp-nodelay}") + private boolean tcpNodelay; @Value("${netty.socket.backlog}") private int backlog; @Bean - public ServerBootstrap serverBootstrap(DronChannelInitializer nettyChannelInitializer) { + public ServerBootstrap serverBootstrap(TcpChannelInitializer channelInitializer) { log.info(">>>>> serverBootstrap <<<<<"); // ServerBootstrap: 서버 설정을 도와주는 class ServerBootstrap b = new ServerBootstrap(); @@ -40,12 +40,18 @@ public class NettyConfig { .channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.DEBUG)) // ChannelInitializer: 새로운 Channel을 구성할 때 사용되는 특별한 handler. 주로 ChannelPipeline으로 구성 - .childHandler(nettyChannelInitializer); + .childHandler(channelInitializer); // ServerBootstarp에 다양한 Option 추가 가능 // SO_BACKLOG: 동시에 수용 가능한 최대 incoming connections 개수 // 이 외에도 SO_KEEPALIVE, TCP_NODELAY 등 옵션 제공 - b.option(ChannelOption.SO_BACKLOG, backlog); + b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000); + b.option(ChannelOption.SO_BACKLOG, backlog); // 동시에 수용할 수 있는 소켓 연결 요청 수입니다. + + b.childOption(ChannelOption.TCP_NODELAY, tcpNodelay); // 반응속도를 높이기 위해 Nagle 알고리즘을 비활성화 합니다 + b.childOption(ChannelOption.SO_LINGER, 0); // 소켓이 close될 때 신뢰성있는 종료를 위해 4way-handshake가 발생하고 이때 TIME_WAIT로 리소스가 낭비됩니다. 이를 방지하기 위해 0으로 설정합니다. + b.childOption(ChannelOption.SO_KEEPALIVE, keepAlive); // Keep-alive를 켭니다. + b.childOption(ChannelOption.SO_REUSEADDR, false); // SO_LINGER설정이 있으면 안해도 되나 혹시나병(!)으로 TIME_WAIT걸린 포트를 재사용할 수 있도록 설정합니다. return b; } @@ -61,7 +67,7 @@ public class NettyConfig { @Bean(destroyMethod = "shutdownGracefully") public NioEventLoopGroup workerGroup() { log.info(">>>>> workerGroup <<<<<"); - return new NioEventLoopGroup(workerCount); + return new NioEventLoopGroup(); } // IP 소켓 주소(IP 주소, Port 번호)를 구현 diff --git a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/handler/DronSocketHandler.java b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/handler/DronHandler.java similarity index 92% rename from app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/handler/DronSocketHandler.java rename to app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/handler/DronHandler.java index 8dbe57d..72c7b66 100644 --- a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/handler/DronSocketHandler.java +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/handler/DronHandler.java @@ -6,7 +6,7 @@ import io.netty.channel.SimpleChannelInboundHandler; import kr.co.palnet.kac.socket.core.command.DronCommand; import kr.co.palnet.kac.socket.core.model.DronRQ; import kr.co.palnet.kac.socket.core.model.DronRS; -import kr.co.palnet.kac.socket.core.util.AuthKeyUtil; +import kr.co.palnet.kac.socket.core.storage.AuthKeyStorage; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -17,12 +17,12 @@ import java.util.Map; @Slf4j @ChannelHandler.Sharable @Component -public class DronSocketHandler extends SimpleChannelInboundHandler { +public class DronHandler extends SimpleChannelInboundHandler { private final Map commandMap; - public DronSocketHandler(List commandList) { + public DronHandler(List commandList) { commandMap = new HashMap<>(); for (DronCommand command : commandList) { commandMap.put(command.getCommandName(), command); @@ -58,7 +58,7 @@ public class DronSocketHandler extends SimpleChannelInboundHandler { return; } - if (AuthKeyUtil.getInstance().checkAuthKey(rq.getCommand())) { + if (AuthKeyStorage.getInstance().checkAuthKey(rq.getCommand())) { ctx.writeAndFlush(DronRS.builder() .code("-2000") .message("Invalid auth key.") diff --git a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/DronChannelInitializer.java b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/TcpChannelInitializer.java similarity index 87% rename from app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/DronChannelInitializer.java rename to app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/TcpChannelInitializer.java index bcec949..629e4fd 100644 --- a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/DronChannelInitializer.java +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/TcpChannelInitializer.java @@ -9,7 +9,7 @@ import io.netty.handler.codec.string.StringEncoder; import io.netty.util.CharsetUtil; import kr.co.palnet.kac.socket.core.codec.DronDecoder; import kr.co.palnet.kac.socket.core.codec.DronEncoder; -import kr.co.palnet.kac.socket.core.handler.DronSocketHandler; +import kr.co.palnet.kac.socket.core.handler.DronHandler; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -17,9 +17,9 @@ import org.springframework.stereotype.Component; @Slf4j @RequiredArgsConstructor @Component -public class DronChannelInitializer extends ChannelInitializer { +public class TcpChannelInitializer extends ChannelInitializer { - private final DronSocketHandler dronSocketHandler; + private final DronHandler dronSocketHandler; // 클라이언트 소켓 채널이 생성될 때 호출 @Override diff --git a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/DronServerSocket.java b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/TcpSocketServer.java similarity index 97% rename from app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/DronServerSocket.java rename to app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/TcpSocketServer.java index 11f51e2..a4fed30 100644 --- a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/DronServerSocket.java +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/TcpSocketServer.java @@ -13,7 +13,7 @@ import java.net.InetSocketAddress; @Slf4j @RequiredArgsConstructor @Component -public class DronServerSocket { +public class TcpSocketServer { private final ServerBootstrap serverBootstrap; private final InetSocketAddress tcpPort; private Channel serverChannel; diff --git a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/util/AuthKeyUtil.java b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/storage/AuthKeyStorage.java similarity index 83% rename from app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/util/AuthKeyUtil.java rename to app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/storage/AuthKeyStorage.java index 82e5599..8879609 100644 --- a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/util/AuthKeyUtil.java +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/storage/AuthKeyStorage.java @@ -1,27 +1,26 @@ -package kr.co.palnet.kac.socket.core.util; +package kr.co.palnet.kac.socket.core.storage; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.HashSet; import java.util.Set; @Slf4j -public class AuthKeyUtil { +public class AuthKeyStorage { private Set keys = new HashSet<>(); - private AuthKeyUtil() { + private AuthKeyStorage() { log.debug("AuthKeyUtil : init keys"); initKey(); } - public static AuthKeyUtil getInstance() { + public static AuthKeyStorage getInstance() { return LazyHolder.INSTANCE; } public static class LazyHolder { - private static final AuthKeyUtil INSTANCE = new AuthKeyUtil(); + private static final AuthKeyStorage INSTANCE = new AuthKeyStorage(); } private void initKey() { diff --git a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/util/ControlCacheUtil.java b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/storage/ControlStorage.java similarity index 78% rename from app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/util/ControlCacheUtil.java rename to app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/storage/ControlStorage.java index 53848f2..715c14a 100644 --- a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/util/ControlCacheUtil.java +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/storage/ControlStorage.java @@ -1,4 +1,4 @@ -package kr.co.palnet.kac.socket.core.util; +package kr.co.palnet.kac.socket.core.storage; import kr.co.palnet.kac.socket.core.model.ControlDto; import lombok.extern.slf4j.Slf4j; @@ -8,29 +8,29 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Slf4j -public class ControlCacheUtil { +public class ControlStorage { private final Map controlMap; private final int REMOVE_TIME = 1000 * 10; - private ControlCacheUtil() { + private ControlStorage() { controlMap = new ConcurrentHashMap<>(); } - public static ControlCacheUtil getInstance() { + public static ControlStorage getInstance() { return LazyHolder.INSTANCE; } public static class LazyHolder { - private static final ControlCacheUtil INSTANCE = new ControlCacheUtil(); + private static final ControlStorage INSTANCE = new ControlStorage(); } public ControlDto get(String objectId) { return controlMap.get(objectId); } - public ControlDto set(String objectId, ControlDto control) { + public ControlDto put(String objectId, ControlDto control) { return controlMap.put(objectId, control); } diff --git a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/util/DronCacheUtil.java b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/storage/DronStorage.java similarity index 83% rename from app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/util/DronCacheUtil.java rename to app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/storage/DronStorage.java index 42e23a9..f0768f2 100644 --- a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/util/DronCacheUtil.java +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/storage/DronStorage.java @@ -1,34 +1,31 @@ -package kr.co.palnet.kac.socket.core.util; +package kr.co.palnet.kac.socket.core.storage; -import kr.co.palnet.kac.socket.core.model.ControlDto; import kr.co.palnet.kac.socket.core.model.DronDTO; import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; import java.time.Instant; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Slf4j -public class DronCacheUtil { +public class DronStorage { private final Map> dronMap; private final int REMOVE_TIME = 1000 * 10; - private DronCacheUtil() { + private DronStorage() { dronMap = new ConcurrentHashMap<>(); } - public static DronCacheUtil getInstance() { + public static DronStorage getInstance() { return LazyHolder.INSTANCE; } public static class LazyHolder { - private static final DronCacheUtil INSTANCE = new DronCacheUtil(); + private static final DronStorage INSTANCE = new DronStorage(); } public Map> getAll() { @@ -48,7 +45,7 @@ public class DronCacheUtil { return dronMap.get(objectId); } - public void set(DronDTO dron) { + public void add(DronDTO dron) { if (dron == null || dron.getObjectId() == null || dron.getObjectId().isEmpty()) { return; } diff --git a/app/kac-socket-app/src/main/resources/application.yml b/app/kac-socket-app/src/main/resources/application.yml index 950a48e..e347e54 100644 --- a/app/kac-socket-app/src/main/resources/application.yml +++ b/app/kac-socket-app/src/main/resources/application.yml @@ -1,10 +1,10 @@ netty: socket: - tcp-port: 8200 + tcp-port: 8003 boss-count: 1 - worker-count: 10 keep-alive: false - backlog: 100 + tcp-nodelay: false + backlog: 3000 app: kac-app: @@ -12,6 +12,13 @@ app: web-socket: host: http://127.0.0.1:8100 +server: + port: 8004 + +spring: + threads: + virtual: + enabled: true --- diff --git a/app/kac-websocket-app/build.gradle b/app/kac-websocket-app/build.gradle new file mode 100644 index 0000000..c607bb1 --- /dev/null +++ b/app/kac-websocket-app/build.gradle @@ -0,0 +1,8 @@ + + +dependencies { + implementation "$boot:spring-boot-starter-web" + implementation "io.netty:netty-all:4.1.68.Final" + implementation project(":common:util") +} + diff --git a/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/KacWebSocketApplication.java b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/KacWebSocketApplication.java new file mode 100644 index 0000000..abe0f43 --- /dev/null +++ b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/KacWebSocketApplication.java @@ -0,0 +1,14 @@ +package kr.co.palnet.kac.websocket; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +@EnableScheduling +@SpringBootApplication +public class KacWebSocketApplication { + public static void main(String[] args) { + SpringApplication.run(KacWebSocketApplication.class, args); + } + +} diff --git a/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/controller/SocketReceiverController.java b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/controller/SocketReceiverController.java new file mode 100644 index 0000000..bb2fac9 --- /dev/null +++ b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/controller/SocketReceiverController.java @@ -0,0 +1,37 @@ +package kr.co.palnet.kac.websocket.controller; + + +import kr.co.palnet.kac.websocket.core.model.ControlDTO; +import kr.co.palnet.kac.websocket.core.model.DronDTO; +import kr.co.palnet.kac.websocket.core.storage.ControlStorage; +import kr.co.palnet.kac.websocket.service.ControlService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping("/v1/api/ws") +public class SocketReceiverController { + + private final ControlService controlService; + + @PostMapping("/dron") + public ResponseEntity receiver(@RequestBody DronDTO dronDTO) { + log.info("websocket message receiver : {}", dronDTO); + + ControlDTO history = controlService.dronDtoToControlDtoConvert(dronDTO); + + // DRON의 대한 식별정보만 이력 관리 + ControlStorage controlCache = ControlStorage.getInstance(); + controlCache.put(history); + + return ResponseEntity.ok().build(); + } + +} diff --git a/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/codec/Decoder.java b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/codec/Decoder.java new file mode 100644 index 0000000..946437a --- /dev/null +++ b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/codec/Decoder.java @@ -0,0 +1,25 @@ +package kr.co.palnet.kac.websocket.core.codec; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + + +@Slf4j +@RequiredArgsConstructor +public class Decoder extends MessageToMessageDecoder { + // private int DATA_LENGTH = 100; +// private final ObjectMapper objectMapper = ObjectMapperUtils.getObjectMapper(); + + @Override + protected void decode(ChannelHandlerContext ctx, Object in, List out) throws Exception { + log.info(">>>>> decode <<<<<"); + try { + } catch (Exception e) { + log.warn("decode parsing error : {} :: {}", e.getMessage(), in); + } + } +} diff --git a/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/codec/Encoder.java b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/codec/Encoder.java new file mode 100644 index 0000000..988b9e0 --- /dev/null +++ b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/codec/Encoder.java @@ -0,0 +1,26 @@ +package kr.co.palnet.kac.websocket.core.codec; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; +import kr.co.palnet.kac.util.ObjectMapperUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +public class Encoder extends MessageToMessageEncoder { + + private final ObjectMapper objectMapper = ObjectMapperUtils.getObjectMapper(); + + @Override + protected void encode(ChannelHandlerContext ctx, Object msg, List out) throws Exception { + log.info(">>>>> encode <<<"); + try { + } catch (Exception e) { + log.warn("json parsing error : {} :: {}", e.getMessage(), msg); + } + } +} diff --git a/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/config/AppReadyEvent.java b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/config/AppReadyEvent.java new file mode 100644 index 0000000..f30ca84 --- /dev/null +++ b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/config/AppReadyEvent.java @@ -0,0 +1,22 @@ +package kr.co.palnet.kac.websocket.core.config; + +import kr.co.palnet.kac.websocket.core.socket.WebSocketServer; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +public class AppReadyEvent implements ApplicationListener { + + private final WebSocketServer socketServer; + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + log.info(">>>> onApplicationEvent <<<<<"); + socketServer.start(); + } +} diff --git a/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/config/NettyConfig.java b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/config/NettyConfig.java new file mode 100644 index 0000000..9bea0bc --- /dev/null +++ b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/config/NettyConfig.java @@ -0,0 +1,80 @@ +package kr.co.palnet.kac.websocket.core.config; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelOption; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import kr.co.palnet.kac.websocket.core.socket.DronChannelInitializer; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.net.InetSocketAddress; + +@Slf4j +@RequiredArgsConstructor +@Configuration +public class NettyConfig { + + @Value("${netty.socket.tcp-port}") + private int port; + @Value("${netty.socket.boss-count}") + private int bossCount; + @Value("${netty.socket.keep-alive}") + private boolean keepAlive; + @Value("${netty.socket.tcp-nodelay}") + private boolean tcpNodelay; + @Value("${netty.socket.backlog}") + private int backlog; + + @Bean + public ServerBootstrap serverBootstrap(DronChannelInitializer channelInitializer) { + log.info(">>>>> serverBootstrap <<<<<"); + // ServerBootstrap: 서버 설정을 도와주는 class + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup(), workerGroup()); + // NioServerSocketChannel: incoming connections를 수락하기 위해 새로운 Channel을 객체화할 때 사용 + b.channel(NioServerSocketChannel.class); + b.handler(new LoggingHandler(LogLevel.DEBUG)); + // ChannelInitializer: 새로운 Channel을 구성할 때 사용되는 특별한 handler. 주로 ChannelPipeline으로 구성 + b.childHandler(channelInitializer); + + // ServerBootstarp에 다양한 Option 추가 가능 + // SO_BACKLOG: 동시에 수용 가능한 최대 incoming connections 개수 + // 이 외에도 SO_KEEPALIVE, TCP_NODELAY 등 옵션 제공 + b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000); + b.option(ChannelOption.SO_BACKLOG, backlog); + b.childOption(ChannelOption.TCP_NODELAY, tcpNodelay); // 반응속도를 높이기 위해 Nagle 알고리즘을 비활성화 합니다 + b.childOption(ChannelOption.SO_LINGER, 0); // 소켓이 close될 때 신뢰성있는 종료를 위해 4way-handshake가 발생하고 이때 TIME_WAIT로 리소스가 낭비됩니다. 이를 방지하기 위해 0으로 설정합니다. + b.childOption(ChannelOption.SO_KEEPALIVE, keepAlive); // Keep-alive를 켭니다. + b.childOption(ChannelOption.SO_REUSEADDR, true); // SO_LINGER설정이 있으면 안해도 되나 혹시나병(!)으로 TIME_WAIT걸린 포트를 재사용할 수 있도록 설정합니다. + + return b; + } + + // boss: incoming connection을 수락하고, 수락한 connection을 worker에게 등록(register) + @Bean(destroyMethod = "shutdownGracefully") + public NioEventLoopGroup bossGroup() { + log.info(">>>>> bossGroup <<<<<"); + return new NioEventLoopGroup(bossCount); + } + + // worker: boss가 수락한 연결의 트래픽 관리 + @Bean(destroyMethod = "shutdownGracefully") + public NioEventLoopGroup workerGroup() { + log.info(">>>>> workerGroup <<<<<"); + return new NioEventLoopGroup(); + } + + // IP 소켓 주소(IP 주소, Port 번호)를 구현 + // 도메인 이름으로 객체 생성 가능 + @Bean + public InetSocketAddress inetSocketAddress() { + log.info(">>>>> inetSocketAddress <<<<<"); + return new InetSocketAddress(port); + } +} diff --git a/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/handler/WebSocketHandler.java b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/handler/WebSocketHandler.java new file mode 100644 index 0000000..5903dde --- /dev/null +++ b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/handler/WebSocketHandler.java @@ -0,0 +1,42 @@ +package kr.co.palnet.kac.websocket.core.handler; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import kr.co.palnet.kac.websocket.core.storage.ChannelStorage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@ChannelHandler.Sharable +@Component +public class WebSocketHandler extends SimpleChannelInboundHandler { + + public WebSocketHandler() { + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame msg) throws Exception { + log.info(">>>> channelRead0 <<<<<"); + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + log.info(">>>>> channelRegistered <<<<<"); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + log.info(">>>>> channelActive <<<<<"); + ChannelStorage channelStorage = ChannelStorage.getInstance(); + log.info("active size before : {}", channelStorage.getAll().size()); + channelStorage.add(ctx.channel()); + log.info("active size after : {}", channelStorage.getAll().size()); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + log.info(">>>>> channelInactive <<<<<"); + } +} diff --git a/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/ControlDTO.java b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/ControlDTO.java new file mode 100644 index 0000000..103801c --- /dev/null +++ b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/ControlDTO.java @@ -0,0 +1,86 @@ +package kr.co.palnet.kac.websocket.core.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ControlDTO implements Comparable { + + private String messageTypeCd; + + private String controlId; + + private String trmnlId; + + private String objectTypeCd; + + private String objectId; + + private Double lat; + + private Double lon; + + private String elevType; + + private Double elev; + + private String speedType; + + private Double speed; + + private Double betteryLevel; + + private Double betteryVoltage; + + private String takeOffPositon; + + private String dronStatus; + + private Double heading; + + private String terminalRcvDt; + + private Instant serverRcvDt; + + private Instant controlStartDt; + + private Double moveDistance; + + private String moveDistanceType; + + // 환경센서 필드 + private Double sensorCo; + private Double sensorSo2; + private Double sensorNo2; + private Double sensorO3; + private Double sensorDust; + + private List> lastHistory; + + // 비정상 상황 식별 코드 (비정상: true) + private boolean controlWarnCd; + // 비정상 상황 알림 표출 코드 (알림: true, 미알림: false) + private boolean controlWarnNotyCd; + // 비정상 상황 알림 중복 체크 + private Integer controlCacheCount; + // 큐가 Socket서버에 도착한 시간 TODO : 타입 문제로 인한 임시주석 + private Instant regDt; + + @Override + public int compareTo(ControlDTO o) { + if (o.getControlStartDt() != null && controlStartDt != null) { + return o.getControlStartDt().compareTo(controlStartDt); + } + return 0; + } + +} diff --git a/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/DronDTO.java b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/DronDTO.java new file mode 100644 index 0000000..fc7dabd --- /dev/null +++ b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/DronDTO.java @@ -0,0 +1,96 @@ +package kr.co.palnet.kac.websocket.core.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DronDTO { + private String typeCd; // 01 : 최초 들어온 데이터 , 99 : 종료 시킬 데이터 + + private String messageType; + + private String terminalId; + + @Builder.Default + private Double moveDistance = 0.0; + + private String moveDistanceType; + + private String controlId; // 처음 위치 데이터가 들어 왔을때 생성 함 + + private String objectType; + + private String objectId; + + @Builder.Default + private Double lat = 0.0; + + @Builder.Default + private Double lon = 0.0; + + private String elevType; + + @Builder.Default + private Double elev = 0.0; + + private String speedType; + + @Builder.Default + private Double speed = 0.0; + + @Builder.Default + private Double betteryLevel = 0.0; + + @Builder.Default + private Double betteryVoltage = 0.0; + + private String dronStatus; + + @Builder.Default + private Double heading = 0.0; + + private String terminalRcvDt; + + private Instant serverRcvDt; + + private Instant controlStartDt; + + private Instant controlEndDt; + + private String areaTrnsYn; + + // 환경센서 필드 + @Builder.Default + private Double sensorCo = 0.0; + @Builder.Default + private Double sensorSo2 = 0.0; + @Builder.Default + private Double sensorNo2 = 0.0; + @Builder.Default + private Double sensorO3 = 0.0; + @Builder.Default + private Double sensorDust = 0.0; + + //최근 5건만 저장 + private List recentPositionHistory; + + // 전체 히스토리 저장 + private List postionHistory; + + // 비정상 상황 식별 코드 + private boolean controlWarnCd; + + // 큐가 Socket서버에 도착한 시간 + private Instant regDt; + // 큐가 Socket서버에 도착한 시간 + private boolean sendUtm; // 불법드론 전송 여부 + +} diff --git a/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/DronHistoryDTO.java b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/DronHistoryDTO.java new file mode 100644 index 0000000..96dbbf7 --- /dev/null +++ b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/DronHistoryDTO.java @@ -0,0 +1,20 @@ +package kr.co.palnet.kac.websocket.core.model; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DronHistoryDTO { + private String objectId; + + @Builder.Default + private Double lat = 0.0; + @Builder.Default + private Double lon = 0.0; +} diff --git a/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/socket/DronChannelInitializer.java b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/socket/DronChannelInitializer.java new file mode 100644 index 0000000..cbae8f4 --- /dev/null +++ b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/socket/DronChannelInitializer.java @@ -0,0 +1,41 @@ +package kr.co.palnet.kac.websocket.core.socket; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; +import io.netty.handler.timeout.IdleStateHandler; +import kr.co.palnet.kac.websocket.core.handler.WebSocketHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +public class DronChannelInitializer extends ChannelInitializer { + + private final WebSocketHandler webSocketHandler; + + // 클라이언트 소켓 채널이 생성될 때 호출 + @Override + protected void initChannel(SocketChannel ch) { + log.info(">>>>> initChannel <<<<<"); + ChannelPipeline pipeline = ch.pipeline(); + + pipeline.addLast(new HttpServerCodec()); + pipeline.addLast(new HttpObjectAggregator(65536)); + pipeline.addLast(new WebSocketServerCompressionHandler()); + pipeline.addLast(new WebSocketServerProtocolHandler("/ws", null, true)); + pipeline.addLast(new IdleStateHandler(0, 0, 180)); +// pipeline.addLast(new DronEncoder(), new DronDecoder()); + pipeline.addLast(webSocketHandler); +// pipeline.addLast(new WebSocketHandler()); + + log.info("pipeline : {}", pipeline); + } +} diff --git a/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/socket/WebSocketServer.java b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/socket/WebSocketServer.java new file mode 100644 index 0000000..3260f92 --- /dev/null +++ b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/socket/WebSocketServer.java @@ -0,0 +1,45 @@ +package kr.co.palnet.kac.websocket.core.socket; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.net.InetSocketAddress; + +@Slf4j +@RequiredArgsConstructor +@Component +public class WebSocketServer { + private final ServerBootstrap serverBootstrap; + private final InetSocketAddress tcpPort; + private Channel serverChannel; + + public void start() { + log.info(">>>>> start <<<<<"); + try { + // ChannelFuture: I/O operation의 결과나 상태를 제공하는 객체 + // 지정한 host, port로 소켓을 바인딩하고 incoming connections을 받도록 준비함 + ChannelFuture serverChannelFuture = serverBootstrap.bind(tcpPort).sync(); + + // 서버 소켓이 닫힐 때까지 기다림 + serverChannel = serverChannelFuture.channel().closeFuture().sync().channel(); + } + catch (InterruptedException e) { + e.printStackTrace(); + } + } + + // Bean을 제거하기 전에 해야할 작업이 있을 때 설정 + @PreDestroy + public void stop() { + log.info(">>>>> stop <<<<<"); + if (serverChannel != null) { + serverChannel.close(); + serverChannel.parent().closeFuture(); + } + } +} diff --git a/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/storage/ChannelStorage.java b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/storage/ChannelStorage.java new file mode 100644 index 0000000..6fc60f1 --- /dev/null +++ b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/storage/ChannelStorage.java @@ -0,0 +1,41 @@ +package kr.co.palnet.kac.websocket.core.storage; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelId; +import io.netty.channel.group.ChannelGroup; +import io.netty.channel.group.DefaultChannelGroup; +import io.netty.util.concurrent.GlobalEventExecutor; +import org.springframework.stereotype.Component; + + +@Component +public class ChannelStorage { + + private final ChannelGroup channelGroup; + + private ChannelStorage() { + channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); + } + + public static ChannelStorage getInstance() { + return ChannelStorage.LazyHolder.INSTANCE; + } + + public static class LazyHolder { + private static final ChannelStorage INSTANCE = new ChannelStorage(); + } + + + public void add(Channel channel) { + channelGroup.add(channel); + } + + public Channel get(ChannelId channelId) { + return channelGroup.find(channelId); + } + + public ChannelGroup getAll() { + return channelGroup; + } + +} \ No newline at end of file diff --git a/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/storage/ControlStorage.java b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/storage/ControlStorage.java new file mode 100644 index 0000000..f6e7c64 --- /dev/null +++ b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/storage/ControlStorage.java @@ -0,0 +1,83 @@ +package kr.co.palnet.kac.websocket.core.storage; + +import kr.co.palnet.kac.websocket.core.model.ControlDTO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class ControlStorage { + + private final int REMOVE_TIME_SECOND = 60; // 화면 노출 시간 + + private final Map controlMap; + + private ControlStorage() { + controlMap = new ConcurrentHashMap<>(); + } + + public static ControlStorage getInstance() { + return LazyHolder.INSTANCE; + } + + public static class LazyHolder { + private static final ControlStorage INSTANCE = new ControlStorage(); + } + + public Map getAll() { + if (controlMap.keySet().isEmpty()) { + return null; + } + return controlMap; + } + + public ControlDTO get(String objectId) { + if (objectId == null || objectId.isEmpty()) { + return null; + } + if (controlMap.get(objectId) == null) { + return null; + } + return controlMap.get(objectId); + } + + public ControlDTO put(ControlDTO control) { + if (control == null || control.getObjectId() == null || control.getObjectId().isEmpty()) { + return null; + } + + return controlMap.put(control.getObjectId(), control); + } + + public List getList() { + return new ArrayList<>(controlMap.values()); + } + + public ControlDTO remove(String objectId) { + return controlMap.remove(objectId); + } + + // 10초 마다 오랜된 데이터(1분 이상 새로운 데이터가 없는 경우) 제거 + @Scheduled(fixedDelay = 1000 * 10) + private void remove() { + // Key 의 존재하는 데이터는 마지막 서버수신 History Data + if (Objects.nonNull(controlMap)) { + controlMap.forEach((objectId, controlDTO) -> { + Instant serverRcvDt = controlDTO.getServerRcvDt(); + // Instant serverRcvDt와 now의 차이 + long diffSecond = Instant.now().getEpochSecond() - serverRcvDt.getEpochSecond(); + + if (diffSecond > REMOVE_TIME_SECOND) { + this.remove(objectId); + } + }); + } + } + +} \ No newline at end of file diff --git a/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/util/DronUtil.java b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/util/DronUtil.java new file mode 100644 index 0000000..bea3769 --- /dev/null +++ b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/util/DronUtil.java @@ -0,0 +1,10 @@ +package kr.co.palnet.kac.websocket.core.util; + +public class DronUtil { + public static boolean latlonCheck(double lat, double lon) { + if (lat > 32 && lat < 44 && lon > 124 && lon < 133) { + return true; + } + return false; + } +} diff --git a/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/service/ControlService.java b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/service/ControlService.java new file mode 100644 index 0000000..aaa7c36 --- /dev/null +++ b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/service/ControlService.java @@ -0,0 +1,113 @@ +package kr.co.palnet.kac.websocket.service; + +import kr.co.palnet.kac.websocket.core.model.ControlDTO; +import kr.co.palnet.kac.websocket.core.model.DronDTO; +import kr.co.palnet.kac.websocket.core.storage.ControlStorage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Slf4j +@RequiredArgsConstructor +@Service +public class ControlService { + + + public List getList() { + List list = new ArrayList<>(); + + ControlStorage controlCache = ControlStorage.getInstance(); + Map allHistory = controlCache.getAll(); + + log.info(">>> getList :: {}", allHistory); + + if (Objects.nonNull(allHistory)) { + allHistory.forEach((k, v) -> { + /* + int cacheCount = v.getControlCacheCount(); + + // 데이터가 수신 되지 않고 이전 데이터를 표출하고 있는 경우 + if (cacheCount == 1) { + if (v.isControlWarnCd() && v.isControlWarnNotyCd()) { + v.setControlCacheCount(2); + } + } + + // 비정상 상황 판별하여 알림 표출 X + if (cacheCount == 2) { + if (v.isControlWarnCd() && v.isControlWarnNotyCd()) { + v.setControlWarnNotyCd(false); + } + } + */ + + list.add(v); + }); + } + + // 기준 : 관제 시작일이 가장 느린순으로 상단에 올린다. + list.sort(Comparator.reverseOrder()); + + return list; + } + + public ControlDTO dronDtoToControlDtoConvert(DronDTO dronDTO) { + + ControlStorage controlCache = ControlStorage.getInstance(); + ControlDTO prevControlDTO = controlCache.get(dronDTO.getObjectId()); + + ControlDTO controlDTO = new ControlDTO(); + + controlDTO.setObjectId(dronDTO.getObjectId()); + controlDTO.setControlId(dronDTO.getControlId()); + controlDTO.setControlStartDt(dronDTO.getControlStartDt()); + controlDTO.setObjectTypeCd(dronDTO.getObjectType()); + controlDTO.setLat(dronDTO.getLat()); + controlDTO.setLon(dronDTO.getLon()); + controlDTO.setElevType(dronDTO.getElevType()); + controlDTO.setElev(dronDTO.getElev()); + controlDTO.setSpeedType(dronDTO.getSpeedType()); + controlDTO.setSpeed(dronDTO.getSpeed()); + controlDTO.setBetteryLevel(dronDTO.getBetteryLevel()); + controlDTO.setBetteryVoltage(dronDTO.getBetteryVoltage()); + controlDTO.setDronStatus(dronDTO.getDronStatus()); + controlDTO.setHeading(dronDTO.getHeading()); + controlDTO.setMoveDistance(dronDTO.getMoveDistance()); + controlDTO.setMoveDistanceType(dronDTO.getMoveDistanceType()); + + controlDTO.setServerRcvDt(dronDTO.getServerRcvDt()); + + // 환경 데이터 필드 추가 + controlDTO.setSensorCo(dronDTO.getSensorCo()); + controlDTO.setSensorSo2(dronDTO.getSensorSo2()); + controlDTO.setSensorNo2(dronDTO.getSensorNo2()); + controlDTO.setSensorO3(dronDTO.getSensorO3()); + controlDTO.setSensorDust(dronDTO.getSensorDust()); + + // 비정상 상황 식별코드 추가 + controlDTO.setControlWarnCd(dronDTO.isControlWarnCd()); + + if (prevControlDTO == null) { + if (controlDTO.isControlWarnCd()) { + controlDTO.setControlWarnNotyCd(true); // 최초 비정상 발생 + } + } else { + if (prevControlDTO.isControlWarnCd() && controlDTO.isControlWarnCd()) { + controlDTO.setControlWarnNotyCd(false); // 비정상 -> 비정상 + } + if (prevControlDTO.isControlWarnCd() && !controlDTO.isControlWarnCd()) { + controlDTO.setControlWarnNotyCd(false); // 비정상 -> 정상 + } + if (!prevControlDTO.isControlWarnCd() && controlDTO.isControlWarnCd()) { + controlDTO.setControlWarnNotyCd(true); // 정상 -> 비정상상 + } + } + + controlDTO.setControlCacheCount(1); + controlDTO.setRegDt(dronDTO.getRegDt()); + + return controlDTO; + } +} diff --git a/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/service/ScheduledService.java b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/service/ScheduledService.java new file mode 100644 index 0000000..22e11d8 --- /dev/null +++ b/app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/service/ScheduledService.java @@ -0,0 +1,57 @@ +package kr.co.palnet.kac.websocket.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.channel.group.ChannelGroup; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import kr.co.palnet.kac.util.ObjectMapperUtils; +import kr.co.palnet.kac.websocket.core.model.ControlDTO; +import kr.co.palnet.kac.websocket.core.storage.ChannelStorage; +import kr.co.palnet.kac.websocket.core.storage.ControlStorage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@Service +public class ScheduledService { + + private final ObjectMapper objectMapper = ObjectMapperUtils.getObjectMapper(); + + private int count = 0; + private int count2 = 0; + @Scheduled(fixedDelay = 1000) + public void test() { + log.info("test.... : {}", count++); + } + + // 접속한 모든 채널에 데이터 전송 + @Scheduled(fixedDelay = 10 * 1000) // 10초 + public void sendAllChannel() { + log.info("sendAllChannel: {}", count2++); + // 채널 가져오기 + ChannelStorage channelStorage = ChannelStorage.getInstance(); + ChannelGroup channelGroup = channelStorage.getAll(); + // 데이터 가져오기 + ControlStorage controlStorage = ControlStorage.getInstance(); + List controlDtoList = controlStorage.getList(); + + if (controlDtoList == null || controlDtoList.isEmpty()) return; + + try { + String json = objectMapper.writeValueAsString(controlDtoList); + channelGroup.forEach(channel -> { + channel.writeAndFlush(new TextWebSocketFrame(json)); + }); + } catch (JsonProcessingException e) { + log.warn("send fail to all channel. : json parsing error : {}\n{}", e.getMessage(), e.getStackTrace()); + } catch (Exception e) { + log.warn("send fail to all channel. : {}\n{}", e.getMessage(), e.getStackTrace()); + } + } + +} diff --git a/app/kac-websocket-app/src/main/resources/application.yml b/app/kac-websocket-app/src/main/resources/application.yml new file mode 100644 index 0000000..5120d4d --- /dev/null +++ b/app/kac-websocket-app/src/main/resources/application.yml @@ -0,0 +1,30 @@ +netty: + socket: + tcp-port: 8001 + boss-count: 1 + keep-alive: true + tcp-nodelay: true + backlog: 3000 + + +app: + kac-app: + host: http://127.0.0.1:8080 + +spring: + threads: + virtual: + enabled: true + +server: + port: 8002 + +--- + +spring: + config: + activate: + on-profile: local + + +