본문 바로가기
프로그래밍/Netty Codec 이해하기

Netty Pipeline 및 Codec 활용(4)

by Flow.X 2022. 1. 10.
728x90

이전에 해봤던  LineBasedFrameDecoder 외에 자주 쓰이는

LengthFieldBasedFrameDecoder를 배워보자 

 

Netty를 하면서 나에겐 가장 이해가 힘들었던 부분이기도 하다.( 내 실력 기준이다 ㅎㅎ)

 

우선 앞에서 해봤던 걸 티키타카 해보자 

카페 오픈 ▶ 손님입장 ▶ 손님에게 인사하기 ▶ 손님응답하기 ▶ 주문받기 ▶ 금액말하기 

 

우선  만들어보 보자 

우선 서버코드는 간단하게 channelHandler에서  channelActive에서 환영인사, channelRead에서 액션을 보낸다 

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        logger.info("channelActive..");
        ctx.channel().writeAndFlush("어서오세요. 손님~!");  // 손님 입장 시 
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        logger.info("channelRead..{}", msg);
        logger.info("손님 >> {}", msg );
        if(((String)msg).equals("네 반가워요 ~")) // 손님이 이렇게 대답하면
            ctx.channel().writeAndFlush("주문하시겠어요?"); // 요렇게 대답하기 
        else if(((String)msg).equals("커피한잔 주시겠어요?")){ // 상동
            ctx.channel().writeAndFlush("5천원입니다.");  // 상동
        }
    }

손님입장에서 반대가 되겠지

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        logger.info("카페 >> {}", msg);
        if(((String)msg).equals("어서오세요. 손님~!"))
            ctx.channel().writeAndFlush("네 반가워요 ~");
        else if(((String)msg).equals("주문하시겠어요?")){
            ctx.channel().writeAndFlush("커피한잔 주시겠어요?");
        }
    }

뭐 저렇게 해도 문제는 없다

근데 반드시 저런 규칙을 지킨다면 문제가 없지만, 지키지 않을것은 안봐도 알수가 있다.

문을 당기시오 라고 적어놔도 맨날 밀어 재끼는 사람들이 있듯이 .. 

 

그래서 말할때 마다 난 몇자 할꺼야를 먼저 앞에 붙혀주면 너무 좋을것 같다. 

그래서 생각한게 아래와 같다 

나 5자한다~안녕하세요   이런식으로 말하는 거지 

 

SampleServer.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;

import io.netty.handler.logging.LoggingHandler;
import io.netty.util.CharsetUtil;

public class SampleServer {
    private static final Logger logger = LoggerFactory.getLogger(SampleServer.class);

   public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup);
            b.channel(NioServerSocketChannel.class);
            b.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    logger.info("고객 들어옴 : initChannel..");
                    ChannelPipeline p = ch.pipeline();
                    p.addLast(new StringDecoder(CharsetUtil.UTF_8));
                    p.addLast(new SampleServerHandler());
                    p.addLast(new StringEncoder(CharsetUtil.UTF_8));
                }
            });
            ChannelFuture f = b.bind(8009).sync();
            logger.info("카페 문 열었음..");
            f.channel().closeFuture().sync();
            logger.info("카페 문 닫는 중..");
        } catch (Exception e) {
            logger.error("error = {}", e.getMessage());
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

class SampleServerHandler extends ChannelInboundHandlerAdapter {

    private static final Logger logger = LoggerFactory.getLogger(SampleServerHandler.class);

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        logger.info("channelActive..");
        ctx.channel().writeAndFlush("어서오세요. 손님~!");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        logger.info("channelRead..{}", msg);
        logger.info("손님 >> {}", msg );
        if(((String)msg).equals("네 반가워요 ~"))
            ctx.channel().writeAndFlush("주문하시겠어요?");
        else if(((String)msg).equals("커피한잔 주시겠어요?")){
            ctx.channel().writeAndFlush("5천원입니다.");
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        logger.info("exceptionCaught..{}", cause.getMessage());
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        logger.info("channelInactive..");
    }
}

SampleClient.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.util.CharsetUtil;

public class SampleClient {
    private static final Logger logger = LoggerFactory.getLogger(SampleClient.class);

    public static void main(String[] args) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap b = new Bootstrap(); // 손님
            b.group(group);
            b.channel(NioSocketChannel.class); // 난 Nio로 이야기 할꺼야
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline p = ch.pipeline();
                    p.addLast(new StringDecoder(CharsetUtil.UTF_8));
                    p.addLast(new SampleClientHandler()); // 난 이방식으로 이야기 할꺼야
                    p.addLast(new StringEncoder(CharsetUtil.UTF_8));
                }
            });
            ChannelFuture f = b.connect("localhost", 8009).sync(); // 연결
            f.channel().closeFuture().sync();
        } catch (Exception e) {
            logger.error("error .. {}", e);
        } finally {
            group.shutdownGracefully();
        }
    }
}

class SampleClientHandler extends ChannelInboundHandlerAdapter {

    private static final Logger logger = LoggerFactory.getLogger(SampleClientHandler.class);

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        logger.info("channelActive...");
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        logger.info("channelInactive...");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        logger.info("카페 >> {}", msg);
        if(((String)msg).equals("어서오세요. 손님~!"))
            ctx.channel().writeAndFlush("네 반가워요 ~");
        else if(((String)msg).equals("주문하시겠어요?")){
            ctx.channel().writeAndFlush("커피한잔 주시겠어요?");
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        logger.info("exceptionCaught...{}", cause.getMessage());
        ctx.close();
    }
}

 

LengthFieldBasedFrameDecoder

다양한 함수 파라미터가 있는데 밑에 두개만 이해하면 90프로는 이해한듯 

 

LengthFieldBasedFrameDecoder 함수설명

LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength)

 

maxFrameLength : 이건 최대길이다,  우리 카페는 한번에 10글자가 최대인데 넘어서 말하면 개무시하는 기능 

lengthFieldOffset : 이건 "나 몇자할께~" 의 "나"를 나타내는 시작점이다 . 보통의 경우는 0이겠지

lengthFieldLength : 이건 "..할께~" 까지의 길이이다.

 

나10자할께~(7자리)

안녕하세요안녕하세요(10자리)

 

이렇게 되면 LengthFieldBasedFrameDecoder(최대길이,0,7) // 0~7자리 읽어서 몇마디 하는지 보고, 뒤에 10자만 들음

읽었는데 최대길이 넘어가면 오류 발생시키는거고..

 

근데 손님이 음나10자할께~안녕하세요안녕하세요 이렇게 하는경우는 어떻게 한다

 LengthFieldBasedFrameDecoder(최대길이,1,7) //"음"은 빼버로기 1~7까지 읽으면 "나10자할께~" 만 뽑히는거지

 

LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip)

 

위에꺼에다가 기능을 더 넣었다.

lengthAdjustment, initalBytesToStrip 두개이다

 

가령 고객의 주문을 받고나서 주방에다 "나07자할께~아메리카노1잔" 이러는것 보단 그냥 "아메리카노1잔이 좋잖아?

initialBytesToStrip에다가 7을 넣어주면  "나07자할께~" 이걸 날려버리고 주방에게 전달해주는거지.

LengthFieldBasedFrameDecoder(최대길이,0,7,0,7) // 이렇게 하면 "아메리카노1잔"만 남는거지.

그걸 주방으로 쓔욱 .. 

 

lengthAdjustment는 나중에 보자. 적당한 방법이 생각이 안난다.

 

우선 코드에 LengthFieldBasedFramdeDecoder를 추가하자

Server.java에 추가

흐름은 이제 아래와 같다. ByteBuf를 보기위해 LoggingHandler를 INFO 레벨로 찍어본다 

 

LengthFieldBasedFrameDecoder(ByteBuf) ▶ StringDecoder (String) ▶

SampleServerHandler(String) ▶ StringEncoder(ByteBuf)로 날라간다 

b.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        logger.info("고객 들어옴 : initChannel..");
        ChannelPipeline p = ch.pipeline();
        p.addLast(new LoggingHandler(LogLevel.INFO)); // ByteBuf로 보기위한 로그 추가
        p.addLast(new LengthFieldBasedFrameDecoder(20, 0, 2)); // 최대 20이고, 4byte가 길이임
        p.addLast(new StringDecoder(CharsetUtil.UTF_8));
        p.addLast(new SampleServerHandler());
        p.addLast(new StringEncoder(CharsetUtil.UTF_8));
    }
});

돌려보자

당근 오류가 날것이다. "Adjusted frame length exceeds 20: 60294 "

최대 사이즈 20으로 해놨는데  앞에 2byte읽어보니 60294byte네 졸라 크네  오류야  ~ 라고 하는거다 

         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| eb 84 a4 20 eb b0 98 ea b0 80 ec 9b 8c ec 9a 94 |... ............|
|00000010| 20 7e                                           | ~              |
+--------+-------------------------------------------------+----------------+
15:43:29 INFO  [nioEventLoopGroup-3-1] SampleServerHandler - exceptionCaught..Adjusted frame length exceeds 20: 60294 - discarded

앞에 2byte ( eb 84 ) 16진수를 10진수로 변환하면  어마무시한 크기가 나온다.

실제 이것은 "네 반가워요~" 에서 "네"를 뜻한다.  "네" 라는건 3byte를 먹나보군 이라고 유추할수 있다

4번째 있는 "20" 은 공백을 나타낸다   

16진수
10진수 변환

그럼 손님이 말할때  우선 사이즈를 붙혀서 보내보자.

사이즈를 자동으로 붙이는 Codec또한 존재한다 

 

LengthFieldPrepender ( 사이즈 ) 를 보낸다 

설명을 보면 사이즈만큼 붙혀보낸다. 우린 사이즈를 2로 했으니 2로 해서 보내보자 

LengthFieldPrepender

SampleClient.java 수정

            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline p = ch.pipeline();
                    p.addLast(new LoggingHandler(LogLevel.INFO));

                    p.addLast(new StringDecoder(CharsetUtil.UTF_8));
                    
                    p.addLast(new SampleClientHandler()); // 난 이방식으로 이야기 할꺼야

                    p.addLast(new LengthFieldPrepender(2)); //사이즈 2byte 추가해 줘 
                    p.addLast(new StringEncoder(CharsetUtil.UTF_8));
                    
                }
            });

 

"네 안녕하세요~" 의 전/ 후를 보자 

//LengthFieldPrepender(2) 추가 전

         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| eb 84 a4 20 eb b0 98 ea b0 80 ec 9b 8c ec 9a 94 |... ............|
|00000010| 20 7e                                           | ~              |
+--------+-------------------------------------------------+----------------+


//LengthFieldPrepender(2) 추가 후 

         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 12 eb 84 a4 20 eb b0 98 ea b0 80 ec 9b 8c ec |..... ..........|
|00000010| 9a 94 20 7e                                     |.. ~            |
+--------+-------------------------------------------------+----------------+
16:22:06 INFO  [nioEventLoopGroup-3-4] SampleServerHandler - channelRead..네 반가워요 ~

추가한뒤 보면 0, 1 번째 자리에 00 12 가 추가되었다 

hex(0012)를 10진수로 바꾸면 18이 된다. 즉 18byte만큼이 손님이 하고픈 말이라는 뜻이다. 

 

해당 부분도 다양하게 내가 만들어서 할수도 있지만, 굳이 만들어서 할정도의 기능이 있을까 싶다? 저렇게 좋은게 있으니 잘만 활용하면 될듯 

 

실제 날 코딩을 하기위해서는 해당 부분 byte가 읽어지기 때까지 기다리고 있어야 하는데 그런 코드 없이 깔끔하게 읽을수 있다.

 

다음은 EmbeddedChannel 에 대해서 해볼까 한다..

서버와 클라이언트를 직접 만들어서 Codec관련 설명을 하려니 빡세다.

 

단순하게 Codec만 테스트 하려하면 EmbededChannel을 써서 코덱을 테스트하고 , 실제 적용까지 해볼수 있으니, 우선 그것먼저 급하게 해야 겠다 

 

 

 

728x90