diff --git a/app/kac-socket-app/build.gradle b/app/kac-socket-app/build.gradle new file mode 100644 index 0000000..56b9d75 --- /dev/null +++ b/app/kac-socket-app/build.gradle @@ -0,0 +1,8 @@ +dependencies { + + implementation "$boot:spring-boot-starter" + implementation "io.netty:netty-all:4.1.68.Final" + implementation project(":common:util") + +} + diff --git a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/KacSocketApplication.java b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/KacSocketApplication.java new file mode 100644 index 0000000..ec93226 --- /dev/null +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/KacSocketApplication.java @@ -0,0 +1,15 @@ +package kr.co.palnet.kac.socket; + + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@Slf4j +@SpringBootApplication +public class KacSocketApplication { + public static void main(String[] args) { + log.info(">>>>> main <<<<<"); + SpringApplication.run(KacSocketApplication.class, args); + } +} \ No newline at end of file diff --git a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DronPayload.java b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DronPayload.java new file mode 100644 index 0000000..ba9cec0 --- /dev/null +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DronPayload.java @@ -0,0 +1,22 @@ +package kr.co.palnet.kac.socket.core.codec; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DronPayload { + + private String authKey; + + private String terminalId; + + private String command; + + private Object body; + +} diff --git a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DronPayloadDecorder.java b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DronPayloadDecorder.java new file mode 100644 index 0000000..bbab1e2 --- /dev/null +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DronPayloadDecorder.java @@ -0,0 +1,31 @@ +package kr.co.palnet.kac.socket.core.codec; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; +import kr.co.palnet.kac.util.ObjectMapperUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + + +@Slf4j +@RequiredArgsConstructor +public class DronPayloadDecorder extends MessageToMessageDecoder { + // private int DATA_LENGTH = 100; + private final ObjectMapper objectMapper = ObjectMapperUtils.getObjectMapper(); + + @Override + protected void decode(ChannelHandlerContext ctx, String in, List out) throws Exception { + log.info(">>>>> decode <<<<<"); + try { + DronPayload payload = objectMapper.readValue(in, DronPayload.class); + if (payload != null) { + out.add(payload); + } + } catch (Exception e) { + log.warn("decode parsing error : {} :: {}", e.getMessage(), in); + } + } +} diff --git a/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DronPayloadEncorder.java b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DronPayloadEncorder.java new file mode 100644 index 0000000..7648fbd --- /dev/null +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DronPayloadEncorder.java @@ -0,0 +1,28 @@ +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.util.ObjectMapperUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +public class DronPayloadEncorder extends MessageToMessageEncoder { + + private final ObjectMapper objectMapper = ObjectMapperUtils.getObjectMapper(); + + @Override + protected void encode(ChannelHandlerContext ctx, DronPayload msg, List out) throws Exception { + log.info(">>>>> encode <<<"); + try { + 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/config/AppReadyEvent.java b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/AppReadyEvent.java new file mode 100644 index 0000000..c7ffa72 --- /dev/null +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/AppReadyEvent.java @@ -0,0 +1,22 @@ +package kr.co.palnet.kac.socket.core.config; + +import kr.co.palnet.kac.socket.core.socket.DronServerSocket; +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 DronServerSocket socketServer; + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + log.info(">>>> onApplicationEvent <<<<<"); + socketServer.start(); + } +} 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 new file mode 100644 index 0000000..e404924 --- /dev/null +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/NettyConfig.java @@ -0,0 +1,74 @@ +package kr.co.palnet.kac.socket.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.socket.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.worker-count}") + private int workerCount; + @Value("${netty.socket.keep-alive}") + private boolean keepAlive; + @Value("${netty.socket.backlog}") + private int backlog; + + @Bean + public ServerBootstrap serverBootstrap(DronChannelInitializer nettyChannelInitializer) { + log.info(">>>>> serverBootstrap <<<<<"); + // ServerBootstrap: 서버 설정을 도와주는 class + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup(), workerGroup()) + // NioServerSocketChannel: incoming connections를 수락하기 위해 새로운 Channel을 객체화할 때 사용 + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.DEBUG)) + // ChannelInitializer: 새로운 Channel을 구성할 때 사용되는 특별한 handler. 주로 ChannelPipeline으로 구성 + .childHandler(nettyChannelInitializer); + + // ServerBootstarp에 다양한 Option 추가 가능 + // SO_BACKLOG: 동시에 수용 가능한 최대 incoming connections 개수 + // 이 외에도 SO_KEEPALIVE, TCP_NODELAY 등 옵션 제공 + b.option(ChannelOption.SO_BACKLOG, backlog); + + 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(workerCount); + } + + // IP 소켓 주소(IP 주소, Port 번호)를 구현 + // 도메인 이름으로 객체 생성 가능 + @Bean + public InetSocketAddress inetSocketAddress() { + log.info(">>>>> inetSocketAddress <<<<<"); + return new InetSocketAddress(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/DronSocketHandler.java new file mode 100644 index 0000000..62fc192 --- /dev/null +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/handler/DronSocketHandler.java @@ -0,0 +1,44 @@ +package kr.co.palnet.kac.socket.core.handler; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.*; +import kr.co.palnet.kac.socket.core.codec.DronPayload; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@ChannelHandler.Sharable +@Component +public class DronSocketHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, DronPayload msg) throws Exception { + log.info(">>>> channelRead0 <<<<<"); + log.info("Received0 JSON: " + msg.toString()); + ctx.writeAndFlush(msg); + } + + // 클라이언트와 연결되어 트래픽을 생성할 준비가 되었을 때 호출되는 메소드 + @Override + public void channelActive(ChannelHandlerContext ctx) { + log.info(">>>> channelActive <<<<<"); + String remoteAddress = ctx.channel().remoteAddress().toString(); + log.info("Remote Address: " + remoteAddress); +// ctx.writeAndFlush("success"); + } + + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + log.info(">>>> exceptionCaught <<<<<"); + // Close the connection when an exception is raised. + ctx.close(); + cause.printStackTrace(); + } + + + + +} 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/DronChannelInitializer.java new file mode 100644 index 0000000..793f69e --- /dev/null +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/DronChannelInitializer.java @@ -0,0 +1,40 @@ +package kr.co.palnet.kac.socket.core.socket; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.json.JsonObjectDecoder; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; +import io.netty.util.CharsetUtil; +import kr.co.palnet.kac.socket.core.codec.DronPayloadDecorder; +import kr.co.palnet.kac.socket.core.codec.DronPayloadEncorder; +import kr.co.palnet.kac.socket.core.handler.DronSocketHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +public class DronChannelInitializer extends ChannelInitializer { + +// private final DronSocketHandler dronSocketHandler; + + // 클라이언트 소켓 채널이 생성될 때 호출 + @Override + protected void initChannel(SocketChannel ch) { + log.info(">>>>> initChannel <<<<<"); + ChannelPipeline pipeline = ch.pipeline(); + + // decoder는 @Sharable이 안 됨, Bean 객체 주입이 안 되고, 매번 새로운 객체 생성해야 함 + DronPayloadDecorder sampleDecoder = new DronPayloadDecorder(); + + // 뒤이어 처리할 디코더 및 핸들러 추가 + pipeline.addLast(new JsonObjectDecoder()); + pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8), new StringEncoder(CharsetUtil.UTF_8)); + pipeline.addLast(new DronPayloadDecorder(), new DronPayloadEncorder()); + pipeline.addLast(new DronSocketHandler()); +// pipeline.addLast(dronSocketHandler); + } +} 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/DronServerSocket.java new file mode 100644 index 0000000..11f51e2 --- /dev/null +++ b/app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/DronServerSocket.java @@ -0,0 +1,45 @@ +package kr.co.palnet.kac.socket.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 DronServerSocket { + 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-socket-app/src/main/resources/application.yml b/app/kac-socket-app/src/main/resources/application.yml new file mode 100644 index 0000000..991e6cb --- /dev/null +++ b/app/kac-socket-app/src/main/resources/application.yml @@ -0,0 +1,15 @@ +netty: + socket: + tcp-port: 8090 + boss-count: 1 + worker-count: 10 + keep-alive: false + backlog: 100 + + +--- + +spring: + config: + activate: + on-profile: local \ No newline at end of file diff --git a/common/util/build.gradle b/common/util/build.gradle index 18cfbdb..7811b79 100644 --- a/common/util/build.gradle +++ b/common/util/build.gradle @@ -1,5 +1,5 @@ dependencies { - implementation "$boot:spring-boot-starter-json" + api "$boot:spring-boot-starter-json" } diff --git a/common/util/src/main/java/kr/co/palnet/kac/util/ObjectMapperUtils.java b/common/util/src/main/java/kr/co/palnet/kac/util/ObjectMapperUtils.java index 1699bab..60912cf 100644 --- a/common/util/src/main/java/kr/co/palnet/kac/util/ObjectMapperUtils.java +++ b/common/util/src/main/java/kr/co/palnet/kac/util/ObjectMapperUtils.java @@ -15,17 +15,6 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import java.time.format.DateTimeFormatter; import java.util.TimeZone; -/** - * packageName : com.sumair.common.core.utils - * fileName : ObjectMapperUtils - * author : dhji - * date : 2023-05-17(017) - * description : - * =========================================================== - * DATE AUTHOR NOTE - * ----------------------------------------------------------- - * 2023-05-17(017) dhji 최초 생성 - */ public class ObjectMapperUtils { public static ObjectMapper getObjectMapper() { return getObjectMapperBuilder().build();