From 7fbed6288a60829f1c62df77944b620dad542ac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dhji=28=EC=A7=80=EB=8C=80=ED=95=9C=29?= Date: Wed, 21 Feb 2024 01:41:19 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20socket=20=EA=B8=B0=EB=B3=B8=20=ED=8B=80?= =?UTF-8?q?=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/kac-socket-app/build.gradle | 8 ++ .../kac/socket/KacSocketApplication.java | 15 ++++ .../kac/socket/core/codec/DronPayload.java | 22 ++++++ .../core/codec/DronPayloadDecorder.java | 31 ++++++++ .../core/codec/DronPayloadEncorder.java | 28 +++++++ .../kac/socket/core/config/AppReadyEvent.java | 22 ++++++ .../kac/socket/core/config/NettyConfig.java | 74 +++++++++++++++++++ .../core/handler/DronSocketHandler.java | 44 +++++++++++ .../core/socket/DronChannelInitializer.java | 40 ++++++++++ .../socket/core/socket/DronServerSocket.java | 45 +++++++++++ .../src/main/resources/application.yml | 15 ++++ common/util/build.gradle | 2 +- .../co/palnet/kac/util/ObjectMapperUtils.java | 11 --- 13 files changed, 345 insertions(+), 12 deletions(-) create mode 100644 app/kac-socket-app/build.gradle create mode 100644 app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/KacSocketApplication.java create mode 100644 app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DronPayload.java create mode 100644 app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DronPayloadDecorder.java create mode 100644 app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DronPayloadEncorder.java create mode 100644 app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/AppReadyEvent.java create mode 100644 app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/NettyConfig.java create mode 100644 app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/handler/DronSocketHandler.java create mode 100644 app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/DronChannelInitializer.java create mode 100644 app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/DronServerSocket.java create mode 100644 app/kac-socket-app/src/main/resources/application.yml 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();