오래간만에 Netty- (1)Writing a Discard Server

2017. 5. 19. 17:52Java/Netty

http://netty.io/wiki/user-guide-for-4.x.html  튜토리얼 해석 놀이~~~


Getting Started

네티의 핵심 구성에 대해 아주 빠르게 숙지 할수 있는 예제들을 보도록 하겠습니다.
예제를 마칠 무렵이면, 네티를 이용해서 서버와 클라이언트가 완성되어 있을 겁니다. 
혹시 하향식 접근방식을 더 좋아하면,2장 아키텍처 개론 부터 시작하고 나서 여기로 다시 오면 됩니다.

Before Getting Started

예제를 시작하는데 있어서 최소 요구사항으로 Netty의 가장 마지막 버전 과 JDK 1.6 이상의 JDK 딱 두개면 됩니다. 
마지막 버전의 네티는 프로젝트 다운로드 페이지에서 찾아보시고, 
JDK를 다운로드 하려면, 여러분이 좋아하는 벤더의 웹사이트에서  참고하시길 바랍니다. 

읽어가면서, 여러분은 해당 챕터상에 소개된 클래스들에 대한 많은 질문들을 갖게 될것입니다.
그 클래스들에 대해 좀 더 알고 싶을때마다 API 레퍼런스를 참고해주길 바랍니다.
이 문서상에 이름 지어진 모든 클래스들은 여러분의 편의를 위해 온라인  API 레퍼런스에 링크되어져 있습니다.
네티 프로젝트 커뮤니티에 문의하는 것을 절대 주저하지마시고, 잘못된 정보나  타입,문법상에 오류들의 있거나, 
혹 문서에 대해 발전시킬수 있는 좋은 생각을 갖고 있다면, 우리에게 알려주시길 바랍니다.

Writing a Discard Server

세상에서 가장 간단한 프로토콜은 "Hello World!" 가 아니고 , DISCARD 입니다.
이 프로토콜은 받은 데이터에 대해서 응답없이 버리는 겁니다. 

폐기 프로토콜을 구현하기 위해서, 여러분이 필요한 유일한 것은 모든 응답받은 데이터를 무시하는 것입니다.
바로 네티에서 만든 I/O 이벤트들을 다루는, 핸들러 구현부터 바로 시작해봅시다.

package com.neonex.nettystudy.discard;

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

/**
 * Handles a server-side channel
 * @author dennis
 *
 */
public class DiscardServerHandler extends ChannelInboundHandlerAdapter {          //(1)

            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {  //(2)
                        //Discard the received data silently.
                        ((ByteBuf)msg).release();          //(3)
            }

            @Override
            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {  //(4)
                        //Close the connection when an exception is raised.
                        cause.printStackTrace();
                        ctx.close();
            }

}

(1) DiscardServerHandler는 ChannelInboundHandler 의 구현체인 ChannelInboundHandlerAdaper를 확장합니다.
    ChannelInboundHandler은 여러분이 재정의 할수 있는 다양한 이벤트 핸들러 메소드들을 제공합니다.
    지금은 직접 핸들러 인터페이스를 구현하것 보단 ChannelInboundHandlerAdapter를 재정의 하는 것으로 충분합니다.

(2) 우리는 chanelRead() 이벤트 핸들러 메소드를 여기에 재정의합니다.
     이 메소드는 클라이언트에서 받은 새로운 데이터을 받을 때마다 받은 메시지를 갖고 호출됩니다. 
     이 예제에서는 받은 메시지 타입은 ByteBuf 입니다.
     (ByteBuf : A random and sequential accessible sequence of zero or more bytes(octets).
                    This interface provides an abstract view for one or more primitive byte arrays (byte[]) and NIO buffers.)

(3) 폐기 프로토콜을 구현하기위해선, 핸들러는 받은 메시지를 무시해야합니다.
      ByteBuf은 release() 메소드를 통해 즉시 해제되어야하는 참조 카운트된 객체 입니다.
     핸들러에 전달된 참조 카운트된 객체를 해제하는 핸들러의 책임에 주의해주세요.!!!(이에 대한 이해는 ByteBuf 에 대해 읽어보셔야 합니다.)
     일반적으로 channelRead() 핸들러 메소드는 아래와 같이 구현됩니다.

            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                        try{
                                    //Do something with msg
                        }finally{
                                    ReferenceCountUtil.release(msg);
                        }
            }

(4) exceptionCaught() 이벤트 핸들러 메소드는  I/O 에러 때문에 Netty에 의하거나,
    또는 이벤트들을 처리하는 과정에 Exception이 던져지는 경우  handler 구현에 의해서  exception를 일으킬때
    Throwable과 함께 호출됩니다.
     대부분의 경우, 예외획득은 로깅되어야 하고, 그것과 연관된 채널은 여기에서 닫혀야 하는데,
     이 메소드의 구현은 예외적인 상황을 다루는 것을 하길 원할때 마다 달라 질수 있습니다.
     예를 들면, 연결을 닫기 전에 reponse 메시지를 에러 코드와 함께 전송하고자 할수도 있다.
 
좋은게 좋은거라구, DISCARD  서버의 절반을 구현했답니다.
이제 남은건,  DiscardServerHandler와 함께 서버를 시작하는 main 메소드를 작성하는 것입니다.

package com.neonex.nettystudy.discard;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * Discards any incoming data.
 * @author dennis
 *
 */
public class DiscardServer {

            private int port;

            public DiscardServer(int port){
                        this.port = port;
            }

            public void run() throws Exception{
                        EventLoopGroup bossGroup = new NioEventLoopGroup();                           //(1)
                        EventLoopGroup workerGroup = new NioEventLoopGroup();
                        try{
                                     ServerBootstrap b = new ServerBootstrap();                       //(2)
                                     b.group(bossGroup,workerGroup)
                                     .channel(NioServerSocketChannel.class)                           //(3)
                                     .childHandler(new ChannelInitializer<SocketChannel>(){           //(4)

                                                @Override
                                                protected void initChannel(SocketChannel ch) throws Exception {
                                                            ch.pipeline().addLast(new DiscardServerHandler());
                                                }

                                     })
                                     .option(ChannelOption.SO_BACKLOG  ,128)                            //(5)
                                     .childOption(ChannelOption.SO_KEEPALIVE, true);                    //(6)

                                     //Bind and start to accept incoming connection
                                     ChannelFuture f = b.bind(port).sync();                             //(7)

                                     //Wait until the server socket is closed
                                     // In this example, this does not happen, but you can do that to gracefully.
                                     // shut down your server
                                     f.channel().closeFuture().sync();
                        }finally{
                                    bossGroup.shutdownGracefully();
                                    workerGroup.shutdownGracefully();
                        }

            }

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

                        if(args.length > 0)
                                    port = Integer.parseInt(args[0]);
                        else
                                    port = 8080;

                        new DiscardServer(port).run();
            }

}


1) NioEventLoopGroup 은  I/O 동작을 다루는 멀티쓰레드된 이벤트 루프입니다.
    Netty는 다른 종류의 transports용으로 다양한 EventLoopGroup 구현을 제공합니다.
    이 예제에서는 서버사이드 어플리케이션을 구현할 겁니다. 여기엔 두개의 NioEventLoopGroup이 사용됩니다.
   첫번째것은 boss 라고 하며, 들어오는 연결에 적용합니다.
   두번째는 worker라고 하는데 받은 연결의 트래픽을 담당합니다.
    boss가 연결을 한번 받고, worker에게 받은 연결을 등록합니다.

2) ServerBootstrap 은 서버를 셋업하는 헬퍼 클래스 입니다.
   직접 channel을 사용하여 서버를 셋업 할수 있지만, 좀 지겨운 작업이라서, 구지 할 필요는 없습니다.

3) 여기, 연결을 유입하는것에 허용하는 새로운 Channel을 초기화하는데 사용하는 NioServerSocketChannel를 사용하도록 지정합니다.

4) 여기에 지정한 핸들러는 항상 새로 받은 Channel에 의해 재정의합니다.
 ChannelInitializer는 새로운 Channel을 사용자가 설정하는데 도움을 주기 위한 특별한 목적의 핸들러입니다. 
 handler들을 추가하는 것으로 새로운 Channel의 ChannelPipeline를 설정합니다.
 예로, 여러분의 네트워크 어플리케이션을 구현하는 DiscardServerHandler 입니다.
 어플리케이션이 복잡해져가면서, pipeline에 보다 많은 handler를 추가할 꺼구, 
  결국 탑레벨 클래스안에 익명 클래스를 이끌어 낼겁니다.

5) Channel 구현에 특정한 파라미터들을 입력합니다.
   우리는 TPC/IP 서버를 만들건데, TcpNoDelay 와 KeepAlive 와 같은 소켓 옵션들을 적겠습니다.
   ChannelOption 에 대한 API 문서 와 제공된 ChannelOptions에 관련 개론을 얻으시려면 ChannelConfig 구현체 명세를 참고하세요

6) option() 와 childOption() 에 대해 알아차렸나요?
   유입 연결들을 받는 NioServerSocketChnnel을 위한 것이 option() 입니다.
   부모 ServerChannel, 이 경우에는 NioServerSocketChannel에 의해서 받은 Channel 들을 위한 것이 childOption() 입니다.

7) 준비가 되었습니다. 남은건 포트에 바인드하고, 서버를 시작하는 것 뿐이네요.
  여기, 해당 머신에 모든 NICs(Network interface cards)의 8080 포트에 바인딩합니다.
  여러분이 원하는 만큼 bind() 메소드를 여러번 호출할수 있습니다. (다른 바인드 주소를 사용해서)

축하드려요. Netty의 탑에 여러분의 첫번째 서버를 완성했습니다.


Looking into the Received Data


첫번째 서버를 작성했고, 실제 동작한다면, 테스트 해볼 필요가 있다.
테스트 하기 가장 쉬운 방법은 telnet command 를 사용하는 거다.
예를 들어, telnet localhost 8080 을 command 라인데 치고 아무거나 타이핑해봐라

하지만, 서버가 정말 동작한다고 말할수 있나?
이건 폐기 서버이기 때문에 정말 알수가 없다.
전혀 어떤 응답을 받을수가 없다.
정말 동작하는지 증명하기 위해서는, 받은 무언가를 프린트하는 서로로 수정해야 한다.
데이터를 받을때 마다 channelRead() 메소드가 인보크되는 걸 이미 알고 있다.
자 DiscardServerHandler 의 channelRead() 메소드안에 몇가지 코드를 집어 넣자.

@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf in = (ByteBuf) msg;
    try {
        while (in.isReadable()) { // (1)
            System.out.print((char) in.readByte());
            System.out.flush();
        }
    } finally {
        ReferenceCountUtil.release(msg); // (2)
    }
}

1) 비효율적인 루프는 정말 간단해 질수 있다. : System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII))
2) in.release()를 여기서 할수도 있다. 양자택일이다.

telnet 명령을 다시 해봐라. 그럼 메시지가 출력되겠지?
이 모든 소스는 io.netty.example.discard 분산 패키지안에 위치해 있답니다.