Netty 개요 및 에코 서버 예제
Netty를 공부하고자 하는 의지(?)는 충만했으나 매번 핑계를 대고 진행 하지 않았습니다. 그래서 이번엔 각잡고 Netty(이하 '네티')가 어떤것인지 감을 잡는 용도의 포스팅을 올리고자 합니다.
개요(OverView)
네티는 유지하기 쉬운 높은 성능의 프로토콜 서버 및 클라이언트를 신속한 개발을 위한 비동기 이벤트 드리븐 네트워크 어플리케이션 프레임워크입니다.
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
Netty Project
네티는 프로토콜 서버 및 클라이언트 같은 네트워크 어플리케이션의 빠르고 쉬운 개발을 가능하게 하는 NIO 클라이언트 서버 프레임워크입니다. 이는 TCP 또는 UDP 소켓 서버와 같은 네트워크 프로그래밍을 간단하게 할 수 있습니다.
Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.
Netty Project
Getting Started
현재 포스팅 시점으로 네티 5.X 버전이 가장 최신입니다. 따라서 네티 5.X버전으로 포스팅을 진행합니다.
준비
네티 5.X버전은 JDK 1.6 이상이 필요합니다. 그리고 최신 네티 라이브러리를 다운로드 받아야 하죠. 다운로드는 이 곳을 클릭해서 받습니다. 여기서 저는 netty-5.0.0.Alpha2.tar.bz2를 선택했습니다. 물론 Maven으로도 의존성 선언을 통해 받을 수 있습니다.
다운로드 파일 구조
다운로드 받은 netty-5.0.0.Alpha2.tar.bz2을 압축을 해제해 봅시다. 그러면 다음과 같은 구조로 되어 있음을 알 수 있습니다.
netty-5.0.0.Alpha2 ㄴ jar ㄴ all-in-one ㄴ <strong>netty-all-5.0.0.Alpha2.jar</strong> ㄴ netty-all-5.0.0.Alpha2-sources.jar ㄴ ... ㄴ javadoc ㄴ license ㄴ NOTICE.txt ㄴ LICENSE.txt ㄴ README.md ㄴ CONTRIBUTING.md
여기서 실제 사용해 볼것은 all-in-one 으로 이름 붙여진 디렉토리 하위의 netty-all-5.0.0.Alpha2.jar 파일입니다. 이것을 가지고 예제 프로그래밍을 진행합니다.
Echo Server
다양한 예제가 있지만 그중 에코 서버를 직접 구동시켜 봅시다. 해당 부분의 원문은 Writing an Echo Server부분에서 확인 할 수 있습니다.
예제 코드
에코 서버를 구현한 Full Code는 netty example(echo)를 확인해 봅시다. 이 포스팅에서는 진행에 필요한 부분만 추출해서 코드를 새로 작성했습니다.
기본적으로 필요한 클래스는 아래의 목록입니다.
- EchoServer
- EhcoServerHandler
- EchoClient
- EchoClientHandler
하나씩 개별로 코드 리뷰를 해봅시다. 딱히 제가 깊숙히 파고 든 부분은 없어서 돌아가게끔만 작성한 내용입니다. TT
EchoServer
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 EchoServer { // 서버 소켓 포트 번호를 지정합니다. private static final int PORT = 8080; public static void main(String[] args) { /* NioEventLoop는 I/O 동작을 다루는 멀티스레드 이벤트 루프입니다. 네티는 다양한 이벤트 루프를 제공합니다. 이 예제에서는 두개의 Nio 이벤트 루프를 사용합니다. 첫번째 'parent' 그룹은 인커밍 커넥션(incomming connection)을 액세스합니다. 두번째 'child' 그룹은 액세스한 커넥션의 트래픽을 처리합니다. 만들어진 채널에 매핑하고 스레드를 얼마나 사용할지는 EventLoopGroup 구현에 의존합니다. 그리고 생성자를 통해서도 구성할 수 있습니다. */ EventLoopGroup parentGroup = new NioEventLoopGroup(1); EventLoopGroup childGroup = new NioEventLoopGroup(); try{ // 서버 부트스트랩을 만듭니다. 이 클래스는 일종의 헬퍼 클래스입니다. // 이 클래스를 사용하면 서버에서 Channel을 직접 세팅 할 수 있습니다. ServerBootstrap sb = new ServerBootstrap(); sb.group(parentGroup, childGroup) // 인커밍 커넥션을 액세스하기 위해 새로운 채널을 객체화 하는 클래스 지정합니다. .channel(NioServerSocketChannel.class) // 상세한 Channel 구현을 위해 옵션을 지정할 수 있습니다. .option(ChannelOption.SO_BACKLOG, 100) .handler(new LoggingHandler(LogLevel.INFO)) // 새롭게 액세스된 Channel을 처리합니다. // ChannelInitializer는 특별한 핸들러로 새로운 Channel의 // 환경 구성을 도와 주는 것이 목적입니다. .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel sc) throws Exception { ChannelPipeline cp = sc.pipeline(); cp.addLast(new EhcoServerHandler()); } }); // 인커밍 커넥션을 액세스하기 위해 바인드하고 시작합니다. ChannelFuture cf = sb.bind(PORT).sync(); // 서버 소켓이 닫힐때까지 대기합니다. cf.channel().closeFuture().sync(); }catch(Exception e){ e.printStackTrace(); } finally{ parentGroup.shutdownGracefully(); childGroup.shutdownGracefully(); } } }
EhcoServerHandler
import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; public class EhcoServerHandler extends ChannelHandlerAdapter { // 채널을 읽을 때 동작할 코드를 정의 합니다. @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ctx.write(msg); // 메시지를 그대로 다시 write 합니다. } // 채널 읽는 것을 완료했을 때 동작할 코드를 정의 합니다. public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); // 컨텍스트의 내용을 플러쉬합니다. }; // 예외가 발생할 때 동작할 코드를 정의 합니다. @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); // 쌓여있는 트레이스를 출력합니다. ctx.close(); // 컨텍스트를 종료시킵니다. } }
EchoClient
import io.netty.bootstrap.Bootstrap; 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.NioSocketChannel; public class EchoClient { // 호스트를 정의합니다. 로컬 루프백 주소를 지정합니다. private static final String HOST = "127.0.0.1"; // 접속할 포트를 정의합니다. private static final int PORT = 8080; // 메시지 사이즈를 결정합니다. static final int MESSAGE_SIZE = 256; public static void main(String[] args) { EventLoopGroup group = new NioEventLoopGroup(); try{ Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel sc) throws Exception { ChannelPipeline cp = sc.pipeline(); cp.addLast(new EchoClientHandler()); } }); ChannelFuture cf = b.connect(HOST, PORT).sync(); cf.channel().closeFuture().sync(); } catch(Exception e){ e.printStackTrace(); } finally{ group.shutdownGracefully(); } } }
EchoClientHandler
import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; public class EchoClientHandler extends ChannelHandlerAdapter{ private final ByteBuf message; // 초기화 public EchoClientHandler(){ message = Unpooled.buffer(EchoClient.MESSAGE_SIZE); // 예제로 사용할 바이트 배열을 만듭니다. byte[] str = "abcefg".getBytes(); // 예제 바이트 배열을 메시지에 씁니다. message.writeBytes(str); } // 채널이 활성화 되면 동작할 코드를 정의합니다. @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // 메시지를 쓴 후 플러쉬합니다. ctx.writeAndFlush(message); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg)throws Exception { // 받은 메시지를 ByteBuf형으로 캐스팅합니다. ByteBuf byteBufMessage = (ByteBuf) msg; // 읽을 수 있는 바이트의 길이를 가져옵니다. int size = byteBufMessage.readableBytes(); // 읽을 수 있는 바이트의 길이만큼 바이트 배열을 초기화합니다. byte [] byteMessage = new byte[size]; // for문을 돌며 가져온 바이트 값을 연결합니다. for(int i = 0 ; i < size; i++){ byteMessage[i] = byteBufMessage.getByte(i); } // 바이트를 String 형으로 변환합니다. String str = new String(byteMessage); // 결과를 콘솔에 출력합니다. System.out.println(str); // 그후 컨텍스트를 종료합니다. ctx.close(); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
실행 결과
아래는 에코 서버의 콘솔 출력 내용입니다.
6월 08, 2015 22:22:12 오후 io.netty.handler.logging.LoggingHandler channelRegistered INFO: [id: 0x468a70d0] REGISTERED 6월 08, 2015 22:22:12 오후 io.netty.handler.logging.LoggingHandler bind INFO: [id: 0x468a70d0] BIND: 0.0.0.0/0.0.0.0:8080 6월 08, 2015 22:22:12 오후 io.netty.handler.logging.LoggingHandler channelActive INFO: [id: 0x468a70d0, /0:0:0:0:0:0:0:0:8080] ACTIVE 6월 08, 2015 22:23:14 오후 io.netty.handler.logging.LoggingHandler channelRead INFO: [id: 0x468a70d0, /0:0:0:0:0:0:0:0:8080] RECEIVED: [id: 0xc3efda59, /127.0.0.1:51782 => /127.0.0.1:8080]
아래는 에코 클라이언트 콘솔 출력 내용입니다.
abcefg
마무리
네티의 개요 및 간단한 에코 서버 프로그래밍을 통해 아주 짧게(?) 네티를 알아봤습니다. 이제 이걸 기반으로 해서 조금씩 스터디할 분량을 잡아가야겠어요. :)