
네티를 이해하기 쉽게 비유를 한번 해보자면 레스토랑에 비유할 수 있다
레스토랑? 갑자기 레스토랑이 무슨말인가? 나의 경우 동기/비동기의 개념이 등장하면 항상 레스토랑에 빗대어 이해한다.
만약 식당에 요리사가 한명이라면 손님이 몰리는 점심시간에 그 식당은 어떻게 되겠는가.. 아마 웨이팅(병목현상)을 못버티고 손님들이 떠나갈 것 이다(Timeout)
이런 맥락에서 Netty의 비동기 작동 방식은 마치 여러명의 요리사(EventLoopGroup)를 고용해서 급증하는 주문(트래픽)에 대응할 수 있을 것이다
고객(Client)이 테이블(Channel)에 앉아서 요청
매니저(BossGroup Thread)는 요청을 수락(Accept)하는 작업을 하고 입출력작업(I/O)을 요리사(WorkerGroup Thread)에 할당
요리사(Worker)는 요리시작(ServerInitializer)
3.1 파이프 라인은 데이터가 서버에 도달한 후 처리하는 체인
3.2 EchoServerHandler는 비즈니스 로직
주의할점은 매니저와 요리사라는 예시로 인해 상하관계, 종속적인 관계라고 오해할 수 있지만
Process - Thread가 아니라 둘다 Thread로 Boss 스레드는 새로운 연결을 수락하고, Worker 스레드는 데이터를 처리하는 역할을 맡아 수행하는 수평적 관계이다.
네트워크 프로그램에서 이벤트 발생의 주체는 소켓이며 이벤트 종류는 소켓 연결, 데이터 송수신이다.
소켓 통신을 이용해서 클라이언트와 서버의 연결을 유지한다
netty에서는 소켓통신을 하기 때문에 IP, port 번호가 필요하다.
개념적으로 이해한다 해도 잘 이해가 가지 않을 것이다. 코드를 보며 동작원리를 따라가 보자.
해당 config에는 기본 흐름중 첫번째인 Bootstrap부분이 포함되어 있고 네티 서버의 설정을 도와준다
/**
* ApplicationStartupTask
* 서버가 실행이 되고 Netty server를 시작하는 역할
*/
@Component
@RequiredArgsConstructor
public class ApplicationStartupTask implements ApplicationListener<ApplicationReadyEvent> {
// NettyServerSocket 의존성 주입
private final NettyServerSocket nettyServerSocket;
// (1)
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
nettyServerSocket.start();
}
}
🗨️ **주석 해석 및 정리**
/**
* NettyConfiguration
* Netty의 전반적인 설정이 들어있는 클래스입니다.
* 호스트라던지 포트등 기본 값으로 들어가는 값들의 설정과 연결에 관한 설정입니다.
*
*/
@Configuration
@RequiredArgsConstructor
public class NettyConfiguration {
@Value("${server.host}")
private String host;
@Value("${server.port}")
private int port;
@Value("${server.netty.boss-count}")
private int bossCount;
@Value("${server.netty.worker-count}")
private int workerCount;
@Value("${server.netty.keep-alive}")
private boolean keepAlive;
@Value("${server.netty.backlog}")
private int backlog;
@Bean
public ServerBootstrap serverBootstrap(NettyChannelInitializer nettyChannelInitializer) {
// ServerBootstrap : 서버 설정을 도와주는 class
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup(), workerGroup()) // (1)
.channel(NioServerSocketChannel.class) // (2)
.handler(new LoggingHandler(LogLevel.DEBUG))
.childHandler(nettyChannelInitializer); // (3)
serverBootstrap.option(ChannelOption.SO_BACKLOG, backlog);
return serverBootstrap;
}
@Bean(destroyMethod = "shutdownGracefully")
public NioEventLoopGroup bossGroup() {
return new NioEventLoopGroup(bossCount); // (4)
}
@Bean(destroyMethod = "shutdownGracefully") // (5)
public NioEventLoopGroup workerGroup() {
return new NioEventLoopGroup(workerCount); // (6)
}
@Bean
public InetSocketAddress tcpPort() {
// (7)
return new InetSocketAddress(host, port); // 적절한 포트 번호와 호스트 정보로 변경해주세요.
}
}
🗨️ **주석 해석 및 정리**
@Slf4j
@RequiredArgsConstructor
@Component
public class NettyServerSocket {
private final ServerBootstrap serverBootstrap;
private final InetSocketAddress tcpPort;
private Channel serverChannel;
// (1)
public void start() {
try {
// (2)
ChannelFuture serverChannelFuture = serverBootstrap.bind(tcpPort).sync();
// (3)
serverChannel = serverChannelFuture.channel().closeFuture().sync().channel();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@PreDestroy // (4)
public void stop() {
if (serverChannel != null) {
serverChannel.close();
serverChannel.parent().closeFuture();
}
}
}
🗨️ **주석 해석 및 정리**
/**
* NettyChannelInitializez
*
*/
@Component
@RequiredArgsConstructor
public class NettyChannelInitializer extends ChannelInitializer<SocketChannel> {
private final TestHandler testHandler;
// (1)
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// (2)
ChannelPipeline pipeline = ch.pipeline();
TestDecoder testDecoder = new TestDecoder();
// (3)
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8), testDecoder, testHandler);
}
}
🗨️ **주석 해석 및 정리**
/**
* TestDecoder
*
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class TestDecoder extends ByteToMessageDecoder {
private final int DATA_LENGTH = 2048;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() < DATA_LENGTH) {
return;
}
// (1)
ByteBuf buffer = in.readBytes(DATA_LENGTH);
String message = buffer.toString(StandardCharsets.UTF_8);
out.add(message); // String 객체를 out에 추가
buffer.release(); // (2)
}
}
🗨️ **주석 해석 및 정리**
/**
* TestHandler
* Netty server 의 핵심부분 클라이언트와의 연결 수립, 데이터 읽기 및 쓰기 , 예외처리등의 로직이 담겨있다.
*/
@ChannelHandler.Sharable // (1)
public class TestServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf)msg;
System.out.println("Server received : " + in.toString(CharsetUtil.UTF_8));
ctx.write(in); // (2)
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER) // (3)
.addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close(); // 채널 닫기
}
}
🗨️ **주석 해석 및 정리**
참고
https://velog.io/@akfls221/Netty
https://github.com/zbum/netty-spring-example