본문 바로가기
프로그래밍/Netty EchoServer 만들기

01. Netty EchoServer 만들기

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

여기서는 데이터를 수신 받아서 어떻게 핸들링 하는지 보면 될것 같다.

 

1. Visual Studio Code로  java project 하나 만들고

vscode java project 생성

2.   Reference Library 에 3개 추가 하자

vscode referenced library 추가

3. EchoServer.java

특별한건 없다. 이름만 바뀌었을 뿐.. 

그거외에 logger를 사용하기위한 설정외엔 특별한게 없다. logger를 각 단계별로 넣어서 어떤 이벤트가 발생하는지 볼수있게 했다. 

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.config.Configurator;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

public class EchServer {
    private static final Logger logger = LogManager.getLogger(EchServer.class.getName());

    public static void main(String[] args) throws Exception {

        Configurator.setLevel(EchServer.class.getName(), Level.ALL);

        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            logger.info("고객 들어옴 : initChannel..");
                            ChannelPipeline p = ch.pipeline();
                            p.addLast(new EchoServerHandler());
                        }
                    });
            logger.info("편의점 여는 중 ..");
            ChannelFuture f = b.bind(8009).sync();
            logger.info("편의점 열었음 ..");
            f.channel().closeFuture().sync();
            logger.info("편의점 닫는 중 ..");
        } catch (Exception e) {

        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
            logger.info("편의점 닫음 ..");
        }
    }
}

4. EchoServerHandler.java

여기도 특별한건 없고, channelActive, channelInactive, channelRead 만 남기고 나머진 삭제했다

그거외에 생성자에 log4j 2 로그관련 초기화 코드가 들어가 있다 

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.config.Configurator;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class EchoServerHandler extends ChannelInboundHandlerAdapter {
    private static final Logger logger = LogManager.getLogger(EchoServerHandler.class.getName());

    public EchoServerHandler() {
        Configurator.setLevel(EchoServerHandler.class.getName(), Level.ALL);
        logger.info("DiscardServerHandler init..");
    }

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

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

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        logger.info("channelRead..");
    }
}

 

3. 서버를 돌려보자

   main 함수 위에 보면 Run | Debug 중 아무거나 하나 누르면 된다 

돌려보면 로그는 아래 처럼 나온다. 

더보기
14:36:55.989 [main] INFO  EchServer - 편의점 여는 중 ..
14:36:56.042 [main] INFO  EchServer - 편의점 열었음 ..

 

4. 텔넷으로 접속해보자 

텔넷 접속했을때 

더보기
[flowx@dev:~/doc/study/netty/EchoServer] telnet localhost 8009
Trying ::1...
Connected to localhost.
Escape character is '^]'.

서버 로그가 나온다.  아래서 정의한 이벤트 중  3개만 사용했다.

2021.12.27 - [프로그래밍/Netty 기초] - 05. channeInboundAdapter 이벤트 순서

더보기
14:40:33.574 [nioEventLoopGroup-3-1] INFO  EchServer - 고객 들어옴 : initChannel..
14:40:33.583 [nioEventLoopGroup-3-1] INFO  EchoServerHandler - DiscardServerHandler init..
14:40:33.583 [nioEventLoopGroup-3-1] INFO  EchoServerHandler - channelActive..

 

5. Echo를 구현해 보자

echo라는게  손님이 말하면 그대로 돌려주는 내용이라,  channelRead에서 구현하면 될듯하다 

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
	ctx.channel().writeAndFlush(msg); // msg 받아서 다시 전달한다
	logger.info("channelRead..{}", msg); // 메시지를 찍어보자 
}

Telnet 창에서 입력하고 Enter치면 음... 메시지가 안나오고 PooledUnsafeDirectByteBuf(freed) 이렇게만 나오넹

더보기
[main] INFO  EchServer - 편의점 여는 중 ..
[main] INFO  EchServer - 편의점 열었음 ..
[nioEventLoopGroup-3-1] INFO  EchServer - 고객 들어옴 : initChannel..
[nioEventLoopGroup-3-1] INFO  EchoServerHandler - DiscardServerHandler init..
[nioEventLoopGroup-3-1] INFO  EchoServerHandler - channelActive..
[nioEventLoopGroup-3-1] INFO  EchoServerHandler - channelRead..PooledUnsafeDirectByteBuf(freed)

코드를 수정해보자. logger 찍을때 (String)으로 형변환 

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    ctx.channel().writeAndFlush(msg);
    logger.info("channelRead..{}", (String) msg); // String으로 형변환
}

다서 돌려보면 아무 결과도 나오지 않는다. 

EchoServerHandler.java에다가 ChannelInboundAdapter 이벤트 중  exceptionCaught 이벤트 추가하고 다시 돌려보자

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

음. 형변환을 할수가 없다고 하는군.. 

더보기
[nioEventLoopGroup-3-1] INFO  EchoServerHandler - exceptionCaught..io.netty.buffer.PooledUnsafeDirectByteBuf cannot be cast to java.lang.String

 

msg를 ByteBuf로 변환해서 toString() 해서 볼수 있다 

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String str = ((ByteBuf) msg).toString(CharsetUtil.UTF_8);
        logger.info("channelRead..{}", str);
    }

 

VSCode 개발하는 모습을 잠시.. 

왼쪽에는 EchoServer.java를 뛰우고, 오른쪽에는 EchoServerHandler.java를 띄워놨다.

그리고 왼쪽하단에는 EchoServer.java를 실행한 TERMINAL, 오른쪽 하단에는 서버접속을 위한 TERMINAL 이다

순서는 아래처럼 하면된다. 난 이게 편하다 ㅎㅎ

 

1) EchoServer.java 실행하면 TERMINAL 창이 열림

2) VS Code 우측 하단에 책모양 클릭하면 오른쪽으로 TERMINAL 이 열림

vscode 개발모습

 

아래 부분은  그냥 Skip 해도 된다.  개 삽질의 결과

 

바꿔보자.

 

여기서 ByteBuf라는 데이터 타입이 나오는데, 이것은 Byte 배열이지만 추가적인 정보를 갖고 있다. 

이 변수의 사이즈는 얼마이고, 얼만큼 읽었고(index), 어디부터 쓸수 있는지에 대한 위치정보(index)와 누가 얼마나 참조하는지(Refcnt) 가 들어가 있다. ( 이건 차근차근.. 경험 하고프면 여기 )

그러고 다시 돌려보자 .

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  ByteBuf buf = (ByteBuf) msg; // ByteBuf로 형변환
  ctx.channel().writeAndFlush(msg); // 클라이언트로 내려보내고 
  logger.info("channelRead..{}", buf.toString(CharsetUtil.UTF_8)); // ByteBuf to String변환
}

다시 코드로 돌아가서

오류가 다시 발생했다.

ByteBuf 에서는 누가 얼마나 참조하고 있는지를 카운팅하게된다. 

더보기
[nioEventLoopGroup-3-1] INFO  EchoServerHandler - exceptionCaught..refCnt: 0

로그 찍어보자

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        logger.info("첫번째 msg refcnt = {}", ReferenceCountUtil.refCnt(msg)); //msg refcnt = 1

        ByteBuf buf = (ByteBuf) msg;
        logger.info("두번째 msg refcnt = {} buf refcnt={}", ReferenceCountUtil.refCnt(msg),
                ReferenceCountUtil.refCnt(buf)); // msg refcnt =1, buf refcnt = 1

        ctx.channel().writeAndFlush(msg);
        logger.info("세번째 msg refcnt = {} buf refcnt={}", ReferenceCountUtil.refCnt(msg),
                ReferenceCountUtil.refCnt(buf)); // msg refcnt = 0, buf refcnt = 0

        logger.info("channelRead..{}", buf.toString(CharsetUtil.UTF_8)); // 오류
        logger.info("네번째 msg refcnt = {} buf refcnt={}", ReferenceCountUtil.refCnt(msg),
                ReferenceCountUtil.refCnt(buf));
    }
더보기

[nioEventLoopGroup-3-1] INFO  EchoServerHandler - 첫번째 msg refcnt = 1
[nioEventLoopGroup-3-1] INFO  EchoServerHandler - 두번째 msg refcnt = 1 buf refcnt=1
[nioEventLoopGroup-3-1] INFO  EchoServerHandler - 세번째 msg refcnt = 0 buf refcnt=0
[nioEventLoopGroup-3-1] INFO  EchoServerHandler - exceptionCaught..refCnt: 0

위 코드에서 보듯이  Reference Count가 0인 상태에서 참조하게 되면 위처럼 refCnt: 0 오류가 발생하게 된다 

 

retain(), release()를 사용하자 .

아래처럼 간단하게 보자.  이 정도만 알고 있어도 코딩하는데 지장이 없을 껄(?)

ReferenceCountUtil.retain(msg : Any) : 한번 더 쓸거니깐 참조 한번 올려줘

ctx.channel().writeAndFlush(msg) : 아싸.. 이미 한번 내가 써버렸어... 

ReferenceCountUtil.release(msg : Any) : 다 썼어~~ 

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

  logger.info("첫번째 msg refcnt = {}", ReferenceCountUtil.refCnt(msg));

  ByteBuf buf = ((ByteBuf) msg);
  logger.info("두번째 msg refcnt = {} buf refcnt={}", ReferenceCountUtil.refCnt(msg),
  					ReferenceCountUtil.refCnt(buf));

  ReferenceCountUtil.retain(msg);
  logger.info("retain msg refcnt = {} buf refcnt={}", ReferenceCountUtil.refCnt(msg),
  					ReferenceCountUtil.refCnt(buf));

  ctx.channel().writeAndFlush(msg);
  logger.info("세번째 msg refcnt = {} buf refcnt={}", ReferenceCountUtil.refCnt(msg),
  					ReferenceCountUtil.refCnt(buf));

  logger.info("channelRead..{}", buf.toString(CharsetUtil.UTF_8));
  logger.info("네번째 msg refcnt = {} buf refcnt={}", ReferenceCountUtil.refCnt(msg),
  					ReferenceCountUtil.refCnt(buf));

  ReferenceCountUtil.release(msg);
  logger.info("마지막 msg refcnt = {} buf refcnt={}", ReferenceCountUtil.refCnt(msg),
  					ReferenceCountUtil.refCnt(buf));
}

자 쓰고 반환도 잘되고 메시지도 잘 찍힌다.

더보기
[nioEventLoopGroup-3-1] INFO  EchoServerHandler - 첫번째 msg refcnt = 1
[nioEventLoopGroup-3-1] INFO  EchoServerHandler - 두번째 msg refcnt = 1 buf refcnt=1
[nioEventLoopGroup-3-1] INFO  EchoServerHandler - retain msg refcnt = 2 buf refcnt=2
[nioEventLoopGroup-3-1] INFO  EchoServerHandler - 세번째 msg refcnt = 1 buf refcnt=1
[nioEventLoopGroup-3-1] INFO  EchoServerHandler - channelRead..abc

[nioEventLoopGroup-3-1] INFO  EchoServerHandler - 네번째 msg refcnt = 1 buf refcnt=1
[nioEventLoopGroup-3-1] INFO  EchoServerHandler - 마지막 msg refcnt = 0 buf refcnt=0
728x90

'프로그래밍 > Netty EchoServer 만들기' 카테고리의 다른 글

02. EchoServer 응답값 바꿔보기  (0) 2022.01.07