지대한
7 months ago
32 changed files with 950 additions and 66 deletions
@ -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<String> 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() { |
@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
dependencies { |
||||
implementation "$boot:spring-boot-starter-web" |
||||
implementation "io.netty:netty-all:4.1.68.Final" |
||||
implementation project(":common:util") |
||||
} |
||||
|
@ -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); |
||||
} |
||||
|
||||
} |
@ -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<Void> 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(); |
||||
} |
||||
|
||||
} |
@ -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<Object> { |
||||
// private int DATA_LENGTH = 100;
|
||||
// private final ObjectMapper objectMapper = ObjectMapperUtils.getObjectMapper();
|
||||
|
||||
@Override |
||||
protected void decode(ChannelHandlerContext ctx, Object in, List<Object> out) throws Exception { |
||||
log.info(">>>>> decode <<<<<"); |
||||
try { |
||||
} catch (Exception e) { |
||||
log.warn("decode parsing error : {} :: {}", e.getMessage(), in); |
||||
} |
||||
} |
||||
} |
@ -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<Object> { |
||||
|
||||
private final ObjectMapper objectMapper = ObjectMapperUtils.getObjectMapper(); |
||||
|
||||
@Override |
||||
protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception { |
||||
log.info(">>>>> encode <<<"); |
||||
try { |
||||
} catch (Exception e) { |
||||
log.warn("json parsing error : {} :: {}", e.getMessage(), msg); |
||||
} |
||||
} |
||||
} |
@ -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<ApplicationReadyEvent> { |
||||
|
||||
private final WebSocketServer socketServer; |
||||
|
||||
@Override |
||||
public void onApplicationEvent(ApplicationReadyEvent event) { |
||||
log.info(">>>> onApplicationEvent <<<<<"); |
||||
socketServer.start(); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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<WebSocketFrame> { |
||||
|
||||
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 <<<<<"); |
||||
} |
||||
} |
@ -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<ControlDTO> { |
||||
|
||||
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<Map<String, Double>> 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; |
||||
} |
||||
|
||||
} |
@ -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<DronHistoryDTO> recentPositionHistory; |
||||
|
||||
// 전체 히스토리 저장
|
||||
private List<DronHistoryDTO> postionHistory; |
||||
|
||||
// 비정상 상황 식별 코드
|
||||
private boolean controlWarnCd; |
||||
|
||||
// 큐가 Socket서버에 도착한 시간
|
||||
private Instant regDt; |
||||
// 큐가 Socket서버에 도착한 시간
|
||||
private boolean sendUtm; // 불법드론 전송 여부
|
||||
|
||||
} |
@ -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; |
||||
} |
@ -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<SocketChannel> { |
||||
|
||||
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); |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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<String, ControlDTO> 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<String, ControlDTO> 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<ControlDTO> 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); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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<ControlDTO> getList() { |
||||
List<ControlDTO> list = new ArrayList<>(); |
||||
|
||||
ControlStorage controlCache = ControlStorage.getInstance(); |
||||
Map<String, ControlDTO> 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; |
||||
} |
||||
} |
@ -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<ControlDTO> 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()); |
||||
} |
||||
} |
||||
|
||||
} |
@ -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 |
||||
|
||||
|
||||
|
Loading…
Reference in new issue