네티 서버&클라이언트 프로그래밍 시작


자바 네트워크 프로그래밍에서 매우 유명한 프레임워크인 Netty 를 이용 해서 서버 & 클라이언트 프로그래밍을 하는 방법을 알아보는 첫번째 포스팅 입니다.
서버 어플리케이션 개발을 하면서 Netty는 알고 있었지만 대부분의 서버 어플리케이션이 Java.Nio API 로 개발이 되어 있어 Netty를 사용해 보지 못하였기 때문에
“네티 인 액션” 도서를 학습하면서 정리 해보겠습니다.

네티란?? 네티는 고성능의 안정적인 서버 & 클라이언트를 개발 할 수 있는 자바 기반의 프레임워크 입니다.
네티는 2009년 부터 시작이 되었고 많은 개발자들이 네티 오픈소스 프로젝트에 참여 하여 발전 하고 있고 많은 프로젝트에서 이미 검증이 되었습니다.
조금 늦었지만 고성능의 서버 & 클라이언트 프로그래밍을 할때 활용 하기 위해 학습하기로 합니다.

네티 서버&클라이언트 예제 프로그램을 작성 하여 어떤 기능들을 구현 했고 제공 하는지 학습 해보겠습니다.

네티를 이용한 서버 & 클라이언트 예제 소스 분석

  1. 테스트 환경

  2. Maven dependency

     <!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
     <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-all</artifactId>
         <version>4.1.44.Final</version>
     </dependency>
    
  3. 서버

    1) EchoServerhandler.java

     @Sharable      //@Sharable 어노테이션은 여러 채널에서 Handler를 공유 할 수 있음을 나타냄.
     public class EchoServerHandler extends ChannelInboundHandlerAdapter{
     	/**
     	 * 메시지가 들어올때마다 호출되는 메소드
     	 */
     	@Override
     	public void channelRead(ChannelHandlerContext ctx, Object msg) {
     		ByteBuf in = (ByteBuf) msg;
     		System.out.println(
     				"Server received : " + in.toString(CharsetUtil.UTF_8)
     				);
     		ctx.write(in);
     	}
        
     	/**
     	 * channelRead 메소드가 처리 완료 되었다는 것을 핸들러에게 통보 하는 메소드
     	 */
     	@Override
     	public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
     		ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
     			.addListener(ChannelFutureListener.CLOSE);
     	}
        
     	/**
     	 * 읽기 작업중 오류가 발생 했을 경우 호출 되는 메소드
     	 */
     	@Override
     	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
     		cause.printStackTrace();
     		ctx.close();
     	}
     }
    

    2) EchoServer.java

     public class EchoServer {
     	private final int port;
        	
     	public static void main(String[] args) throws InterruptedException {
     		int port = 25;
     		new EchoServer(port).start();
     	}
        
     	public EchoServer(int port) {
     		this.port = port;
     	}
        	
     	private void start() throws InterruptedException {
             final EchoServerHandler serverHandler = new EchoServerHandler();
             EventLoopGroup group = new NioEventLoopGroup();	//NIO 처리를 다루는 이벤트 루프 인스턴스 생성
             try {
                 ServerBootstrap b = new ServerBootstrap();  //ServerBootstrap 인스턴스 생성. ServerBootstrap은 서버 Channel 을 셋팅 할 수 있는 클래스 
                 b.group(group)
                     .channel(NioServerSocketChannel.class)					// Nio 전송 채널을 사용 하도록 셋팅
                     .localAddress(new InetSocketAddress(port))				// 서버 포트 주소 설정
                     .childHandler(new ChannelInitializer<SocketChannel>() { // child 이벤트 핸들러를 설정
                         @Override
                         protected void initChannel(SocketChannel ch) throws Exception {
                             ch.pipeline().addLast(serverHandler);			//serverHandler 을 pipeline으로 설정 한다.
                         }
                     });
                    
                 ChannelFuture f = b.bind().sync();	//서버를 비동기 식으로 바인딩 한다. sync() 는 바인딩이 완료되기를 대기한다.
                 /**
                  * ChannelFuture 는 작업이 완료되면 그 결과에 접근 할 수 있게 해주는 자리 표시자 역활을 하는 인터페이스이다.
                  */
                 f.channel().closeFuture().sync();	//채널의 CloseFuture를 얻고 완료 될때 까지 현재 스레드를 블로킹한다.
             } finally {
                 group.shutdownGracefully().sync();	//EventLoopGroup 을 종료 하고 모든 리소스를 해제 한다.
             }
         }
     }
    

    Netty 를 이용한 서버 구현이 완료 되었습니다. 클라이언트 소켓에서 msg 가 수신이 되면 channelRead() 메소드가 호출 되어 메시지를 출력하고
    출력된 메시지를 되돌려 주는 동작을 하게 됩니다.

    Java NIO Api 를 이용한 서버 코드를 보고 네티에서 어떤 기능들을 구현 하고 제공 하고 있는지 살펴 보겠습니다.

     public class nioServer {
        public static void main(String args[]) throws IOException {
             Selector selector = Selector.open();
             ServerSocketChannel serverSocket = ServerSocketChannel.open();
             serverSocket.bind(new InetSocketAddress("localhost", 3000));
             serverSocket.configureBlocking(false);
             serverSocket.register(selector, SelectionKey.OP_ACCEPT);
             ByteBuffer buffer = ByteBuffer.allocate(256);
       
             while (true) {
                selector.select();
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> iter = selectedKeys.iterator();
       
                while (iter.hasNext()) {
                    SelectionKey key = iter.next();
                
                    //채널 accept 처리
                    //채널 read 처리
                    //채널 write 처리
                    //......
                                      
                    iter.remove();
                }
             }
        }
     } 
    

    많은 코드가 생략 되었지만 Java NIO API를 사용 하면 위 처럼 channel, selector, buffer 처리를 직접 해야 합니다. 하지만 네티를 이용하여 작성을 하게 되면
    이런 부분들을 네티 프레임워크에서 관리를 해주기 때문에 사용자는 EchoServerHandler 를 이용한 비즈니스 로직 부분만 작성을 하면 될 것 입니다.

  4. 클라이언트

    1) EchoClientHandler.java

     @Sharable      //@Sharable 어노테이션은 여러 채널에서 Handler를 공유 할 수 있음을 나타냄.
     public class EchoClientHandler extends ChannelInboundHandlerAdapter{
     	/**
          * 서버로 연결이 만들어지면 channelActive 메소드가 호출 된다. 
          */
         @Override
         public void channelActive(ChannelHandlerContext ctx) throws Exception {
             ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
         }
        
         /**
          * 서버에서 메시지를 수신 하면 channelRead0 메소드가 호출 된다.
          */
         @Override
         protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
             System.out.println("client received : " + msg.toString(CharsetUtil.UTF_8));
         }
        
         /**
          * 예외 발생시 exceptionCaught 메소드가 호출된다.
          */
         @Override
         public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
             cause.printStackTrace();
             ctx.close();
         }
     }
    

    2) EchoClient.java

     public class EchoClient {
     	private final String host;
     	private final int port;
        	
     	public EchoClient(String host, int port) {
     		this.host = host;
     		this.port = port;
     	}
        	
     	public static void main(String[] args) throws InterruptedException {
     		String host = "localhost";
     		int port = 25;
     		new EchoClient(host, port).start();
     	}
        	
     	private void start() throws InterruptedException {
             EventLoopGroup group = new NioEventLoopGroup();	//NIO 처리를 다루는 이벤트 루프 인스턴스 생성
             try {
                 Bootstrap b = new Bootstrap();				//클라이언트 를 셋팅 할수 있는 인스턴스 bootStrap 생성
                 b.group(group)								
                 .channel(NioSocketChannel.class)			// Nio 전송 채널을 사용 하도록 셋팅
                 .remoteAddress(new InetSocketAddress(host, port))
                 .handler(new ChannelInitializer<SocketChannel>() {
                     @Override
                     protected void initChannel(SocketChannel ch) throws Exception {
                         ch.pipeline().addLast(new EchoClientHandler());	//EchoClientHandler 을 pipeline으로 설정 한다.
                     }
                 });
                 ChannelFuture f = b.bind().sync();	//서버를 비동기 식으로 바인딩 한다. sync() 는 바인딩이 완료되기를 대기한다.
                 /**
                  * ChannelFuture 는 작업이 완료되면 그 결과에 접근 할 수 있게 해주는 자리 표시자 역활을 하는 인터페이스
                  */
                 f.channel().closeFuture().sync();	//채널의 CloseFuture를 얻고 완료 될때 까지 현재 스레드를 블로킹한다.
             }finally {
                 group.shutdownGracefully().sync();	//EventLoopGroup 을 종료 하고 모든 리소스를 해제 한다.
             }
         }	
     }
    

    네티를 이용한 클라이언트도 서버와 동일하게 네트워크 부분은 네티에게 맏기고 EchoClientHandler 를 이용하여 메시지를 보내고 받는 부분을 어떻게 처리를 할지만
    작성을 하면 될 것입니다.

정리

네티를 이용한 강력한 서버 & 클라이언트 를 개발 하기 위해 학습을 시작 하였습니다. 네티 프레임워크를 내것으로 만들기 위해 계속 해서 학습 하기로 합니다. 다음 포스팅에서는 네티의 channel, eventLoop, channelFuture 에 대해서 정리 해보겠습니다.

Back to blog