번역을 포스팅 하기에 앞서, 하고 싶은말
오역이 많이 있습니다. 읽으시고 이상한 부분 있으면 알려 주세요. 퍼가셔도 되나, 수정 될 수 있으니, 링크도 같이 가져가세요.
원문 : http://netty.io/wiki/user-guide-for-4.x.html , 2015.01.06 으로
번역 : 최익필 - http://ikpil.com/1338
서문
문제
오늘날 우리는 서로 통신하기 위해 범용적인 애플리케이션이나 라이브러리를 사용합니다. 예를 들어 우리는 종종 웹 서버에서 정보를 검색하고 웹 서비스를 통해 RPC(리모트 프로시져 콜)를 작동 시키려고 HTTP 클라이언트 라이브러리를 사용합니다.
그러나, 범용 프로토콜이나 이 구현이 종종 잘 확장되지 않습니다. 우리가 범용 HTTP 서버를 대용량 파일, 이메일, 메세지, 거의 실시간에 가까운 금융 정보 등 그리고 멀티 플레이 게임 데이터에 사용하지 않는 것과 같습니다.
요구되는 것은 특별한 목적에 전념하는 고도로 최적화된 프로토콜 구현입니다. 예를 들어, AJAX에 기반한 챗 어플리케이션, 미디어 스트리밍, 또는 큰 파일 전송에 최적화된 HTTP 서버 구현을 원할 수 있습니다. 너는 너의 필요에 잘 조정되는 새로운 프로토콜을 디자인하고 구현을 원할 수 있습니다.
다른 피할 수 없는 경우는 당신이 기존 시스템과 상호 운용성을 보장하기 위해 기존 독점 프로토콜을 처리해야 할 때 입니다. 이 경우 중요한 것은 우리가 얼마나 빨리 만들 응용 프로그램을 안정성과 성능을 희생하지 않으면서 구현 할 수 있는 것 입니다.
해결책
네티(Netty) 프로젝트는 비동기 이벤트-드리븐 네트워크 애플리케이션 프레임워크와 유지보수 가능한 고성능, 고확장 프로토콜 서버와 클라이언트의 빠른 개발을 위한 공구를 제공하는 노력입니다.
바꿔 말해, 네티(Netty)는 비동기 클라이언트 서버 프레임워크 인데, 빠르고 쉽게 프로토콜 서버와 클라이언트 등의 네트워크 애플리케이션 개발을 가능하게 합니다. 그것은 TCP와 UDP 소켓 서버 개발등의 네트워크 프로그래밍을 멋지게 간소화 하고 능률적이게 합니다.
'빠르고 쉽게'는 만들 애플리케이션이 유지보수나 성능 이슈에 고생할 꺼라는 것을 의미하지 않습니다. 네티(Netty)는 FTP, SMTP, HTTP 그리고 다양한 바이너리와 텍스트에 기반한 기존 방식 등의 프로토콜들의 구현에서 쌓인 경험들로 신중하게 디자인 되었습니다. 결과적으로 네티(Netty)는 절충없이 개발, 성능, 안정성 및 편의성을 쉽게 달성 하는 방법을 찾는 것을 성공했다.
일부 사용자들은 이미 같은 장점을 가지고 있다고 주장하는 다른 네트워크 어플리케이션 프레임워크를 찾을 수 있으며, 당신들은 그것들이 네티(Netty)와 그렇게 다른점이 무엇인지 묻길 원할 수 있습니다. 그 대답은 철학에 내재되어 있습니다. 네티(Netty)는 첫날부터 API 표현과 구현 측면에서 최대한 쾌적한 경험을 당신에게 주도록 디자인 되어졌습니다.
시작하기
이 챕터는 신속하게 작업을 시작할 수 있게 간단한 예제들과 함께 둘러 보는 것입니다. 당신은 이 챕터의 마지막장에 있을 때, 네티(Netty)를 잘 처리하여 바로 클라이언트와 서버를 작성 할 수 있을 것입니다. 만약 당신이 당신이 뭔가를 배우는 하향식 접근 방식을 선호 한다면, 챕터2 아키텍쳐의 개요 부터 시작하여 여기로 되돌아 오길 원 할 수 있다.
시작하기 앞서
이 챕터에 소개하는 예제들을 실행하는 최소 요구사항들은 오직 두개; 네티(Netty)의 최신 버전과 JDK 1.6 이상 입니다. 네티(Netty)의 최신 버전은 프로젝트 다운로드 페이지(http://netty.io/downloads.html)에서 구할 수 있습니다. JDK의 올바른 버전을 다운로드 하려면, 선호하는 JDK 벤더의 웹사이트를 참조하시기 바랍니다.
읽은데로, 이 챕터에 소개된 클래스들에 대하여 더 많은 질문들이 있을 수 있습니다. 거기에 더 알고 싶을 땐, API 레퍼런스를 참조하시기 바랍니다. 이 문서의 모든 클래스 이름들은 편의를 위해 온라인 API 레퍼런스에 연결되어 있습니다.
또한 네티(Netty) 프로젝트 커뮤니티(http://netty.io/community.html)에 연락하길 주저하지 말아주시고 어떠한 부정확한 정보, 문법 오류나 오타와 설명서를 개선하는 좋은 생각이 있다면, 저희에게 알려 주세요.
DISCARD 서버 작성하기
세계에서 제일 단순한 프로토콜은 'Hello, World'가 아니고 DISCARD 입니다. 어떠한 응답없이 어떠한 수신된 데이터는 파기되는 프로토콜입니다.DISCARD 프로토콜을 구현하기 위해 모든 수신된 데이터를 무시하기만 하면 됩니다. 네티(Netty)에 의해 생성된 I/O 이벤트를 처리하는 핸들러 구현부터 시작합시다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package io.netty.example.discard; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; /** * Handles a server-side channel. */ public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1) @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2) // Discard the received data silently. ((ByteBuf) msg).release(); // (3) } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4) // Close the connection when an exception is raised. cause.printStackTrace(); ctx.close(); } } |
- DiscardServerHandler는 ChannelInboundHandlerAdapter 를 상속 했고, 이건 ChannelInboundHandler의 구현체 입니다. ChannelInboundHandler는 다양한 이벤트 헨들러 함수를 제공하고 오버라이드 할 수 있습니다.
- 우리는 channelRead() 이벤트 핸들러 메서드를 여기서 오버라이드 한다. 이 메서드는 수신된 데이터와 함께 호출 되는데, 언제든 새로운 데이터가 클라이언트로 부터 수신될 때 입니다. 이 예에선 수신된 메세지의 타입은 ByteBuf 입니다.
- DISCARD protocal 을 구현하려면, 핸들러가 수신된 메세지를 무시해야 합니다.ByteBuf는 참조객체인데, release() 메서드를 통해 확실하게 릴리즈 해야 합니다. 핸들러에 전달된 참조 카운트 객체를 릴리즈 하는 것은 핸들러의 책임임을 명심하시기 바랍니다. 보통 channelRead() 핸들러 메서드는 다음과 같이 구현 됩니다. 12345678
@Override
public
void
channelRead(ChannelHandlerContext ctx, Object msg) {
try
{
// Do something with msg
}
finally
{
ReferenceCountUtil.release(msg);
}
}
- exceptionCaught() 이벤트 핸들러 메서드는 이벤트 처리하는 동안 발생된 예외로 인한 핸들러의 구현이나 IO 에러로 인한 네티(Netty)의 예외 발생일 때 Throwable과 함께 호출 됩니다. 대부분의 경우, 잡힌 예외는 로깅되어야 하며, 이 메서드의 구현이 예외적인 상황을 대처하기 위해 수행 할 작업에 따라 다를 수 있지만, 연관된 채널은 여기서 닫아야 합니다. 예를 들어 커낵션이 닫히기 전에 에러 코드로 응답 메세지를 본길 원할 수 있다.
지금까지 좋습니다. 우리는 DISCARD 서버의 첫번째 반을 구현 했습니다. 지금 남은것은 DiscardServerHandler로 서버를 시작하는 main() 메서드를 작성하는 것이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | package io.netty.example.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. */ 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 public 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 connections. 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 { workerGroup.shutdownGracefully(); bossGroup.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(); } } |
- NioEventLoopGroup은 I/O 작업을 처리하는 멀티쓰레드의 이벤트 루프입니다. 네티(Netty)는 다른 종류의 전송을 위해 다양한 EventLoopGroup을 제공합니다. 우리는 이 예제에서 서버 사이드 애플리케이션을 구현 중 이며, 두 NioEventLoopGroup 이 사용 될 것 입니다. 'boss'라 불려지는 두번째는 들어오는 커낵션을 받아들입니다.
'worker'라 불려지고 두번째는 'boss'가 커낵션을 승인 하자마자 승인된 커낵션의 트래픽을 처리하고, 승인된 커넥션을 'worker'에 등록합니다. 얼마나 많은 쓰레드를 사용하고, 생성된 채널에 매핑하는 방법은 EventLoopGroup 구현에 따라 달라지며, 생성자를 통해서 구성 할 수 있습니다. - ServerBootstrap는 서버를 설정하는 핼퍼 클래스입니다. 직접 채널을 사용해서 서버를 설정 할 수 있지만, 지루한 과정이므로 주의 하시길 바라며 대부분의 경우 저걸 할 필요가 없습니다.
- 여기서 우리는 들어오는 커낵션을 승인하여 새로운 커낵션을 초기화 하려고 사용하는 NioServerSocketChanel.class를 사용하도록 명시합니다.
- 여기에 지정된 핸들러는 항상 새롭게 승인된 채널에게 평가되어 질 것이다. ChannelInitializer는 유저가 새 채널을 설정할 수 있도록 도움 주는 특별한 핸들러 입니다. 네트워크 어플리케이션을 구현하는데 DiscardServerHandler 같은 일부 핸들러들을 추가하여 새로운 Channel의 ChannelPipeline 을 설정하길 원할 가능성이 매우 높습니다.복잡해 지는 어플리케이션 처럼, 파이프 라인에 많은 핸들러들을 추가할 가능성이 높고, 최상위 클래스에서 이 익명 클래스를 뽑습니다.
- 또한 Channel 구현에 특정한 파라미터를 설정할 수 있습니다. 우리는 TCP/IP 서버를 작성 중 입니다. 그래서 tcpNoDelay 와 keepAlive 같은 소켓 옵션들을 설정하는 것이 허용됩니다. ChannelOption의 API 문서와 지원되는 채널 옵션에 대한 개요를 얻기 위해 특정한 ChannelConfig 구현들을 참조하시기 바랍니다.
- option() 과 childOption()을 주목 했나요? option() 은 들어오는 커넥션들을 받아들이는 NioServerSocketChannel 를 위한 것입니다. childOption()은 이 경우에서 NioServerSocketChannel 인 부모 ServerChannel에 의해 승인된 채널들을 위한 것입니다.
- 지금 갈 준비가 되었습니다. 포트에 바인드 하고, 서버를 시작하는게 남았습니다. 여기서 머신의 모든 NICs(네트워크 인터페이스 카드들)에 8000 포트에 바인드 합니다. 다른 바인드 주소를 원하는 만큰 여러번 bind() 메서드를 호출 할 수 있습니다.
축하합니다. 네티(Netty)를 기반해서 첫번째 서버를 완료했습니다!
수신 데이터 조사하기
지금 첫 번째 서버를 작성 했었고, 실제 작동 한다면 테스트 할 필요가 있습니다. 테스트하기 제일 쉬운 방법은 텔넷 커맨드를 사용하는 것입니다. 예를 들어 'telnet localhost 8080'을 입력하고, 뭔가를 타이핑 할 수 있습니다.
그러나, 서버가 정상 작동한다고 말할 수 있나요? 우리는 실제로 알 수 없습니다. 왜냐하면 폐기하는 서버이기 때문입니다. 어떤 응답도 전혀 못 받을 것 입니다. 실제 작동을 보여주기 위해, 수신된 것을 출력하는 서버로 수정해 봅시다.
우리는 이미 chanelRead() 메서드가 데이터를 수신 할 때는 언제든지 불려지는 것을 압니다. DiscardServerHandler의 channelRead() 메서드에 약간의 코드를 넣어 봅시다.
1 2 3 4 5 6 7 8 9 10 11 12 | @Override public 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) } } |
- 이 비효율적인 루프는 System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII)) 을 실제로 단수화 할 수 있습니다.
- 다른 방법으로는 여기서 in.release() 할 수 있습니다.
discard server 소스 코드 전문은 배포물의 io.netty.example.discard(http://netty.io/4.0/xref/io/netty/example/discard/package-summary.html) 패키지에 배치 되어 있습니다.
에코 서버 작성하기
지금까지 우리는 전혀 응답없이 데이터를 소비 했었습니다. 그러나 서버는 대개 요청에 응답 되길 추측합니다. 우리는 모든 수신된 데이터가 다시 보내지는 에코 프로토콜 구현에서 클라이언트에게 응답 메세지를 작성하는 방법을 배워 봅시다.
전 섹션에서 우리가 구현했던 discard server와 다른점은 오직 수신된 데이터를 콘솔에 출력하는 대신에 수신된 데이터를 다시 보내는 것입니다. 따라서 channelRead() 메서드를 충분히 다시 수정 합니다.
1 2 3 4 5 | @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ctx.write(msg); // (1) ctx.flush(); // (2) } |
- ChannelHandlerContext 객체는 다양한 옵션들을 제공 합니다. 즉, 다양한 I/O 이벤트들과 작업들을 킬 수 있게 합니다. 여기서 우리는 write(Object)를 그대로 수신된 메세지를 보내려고 호출합니다. 우리가 DISCARD 예제에서 했던것과 다르게 수신된 데이터를 릴리즈 하지 않았다는 것에 주의하시길 바랍니다. 네티(Netty)는 소켓(wire)에 쓰기여질 때 릴리즈 하기 때문입니다.
- ctx.write(Object)는 소켓(wire)에 메세지가 쓰여지는 것을 하지 않습니다. 그것은 내부적으로 버퍼링 한 후 ctx.flush()에 의해 소켓(wire)에 플러쉬 됩니다. 다른 방법으로는 짦게 하기 위해 ctx.writeAndFlush(msg)를 호출할 수 있습니다.
다시 telnet command를 실행 한다면, 보낸대로 되돌려 보내는 것을 볼 수 있습니다.
에코 서버 소스 전문은 배포물의 io.netty.example.echo(http://netty.io/4.0/xref/io/netty/example/echo/package-summary.html) 패키지에 배치 되어 있습니다.
타임 서버 작성하기
이 섹션에서 구현하기 위한 프로토콜은 Time 프로토콜입니다. 임의의 요청을 수신하지 않고, 32비트 정수를 포함한 메세지를 보내는 부분과 한번 메세지가 전송되면 커넥션을 잃는 부분이 전 예제와 다른점 입니다. 이번 예제에서는 구성하는 방법과 메세지를 보내는 것 및 완료한 커넥션을 닫는것을 배울 것 입니다.
왜냐하면 우리는 임의의 수신 데이터를 무시할 것이기 때문 입니다. 그러나 커넥션 하자마자 메세지를 보내는 것은 허용되며, 이번 시간에 channelRead() 메서드를 안 쓸것입니다. 대신에 우리는 channelActive() 메서드를 오버라이드 해야 합니다. 구현은 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package io.netty.example.time; public class TimeServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive( final ChannelHandlerContext ctx) { // (1) final ByteBuf time = ctx.alloc().buffer( 4 ); // (2) time.writeInt(( int ) (System.currentTimeMillis() / 1000L + 2208988800L)); final ChannelFuture f = ctx.writeAndFlush(time); // (3) f.addListener( new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) { assert f == future; ctx.close(); } }); // (4) } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } |
- 설명했다시피, channelActive() 메서드는 커낵션이 맺어지고 트래픽을 발생할 준비가 되었을 때 불려 질 것입니다. 이 메서드에서 현재 시간을 응답하는 32비트 정수를 만들어 봅시다.
- 새 메세지는 보내려면, 우리는 메세지를 포함할 새로운 버퍼를 할당 할 필요가 있습니다. 우리는 32비트 정수를 작성 할 것이므로 최소 4바이트 용량의 ByteBuf가 필요합니다. ChannelHandlerContext.alloc() 를 통해 현재 ByteBufAllocator 를 받으시고 새로운 버퍼를 할당하세요.
- 평소처럼, 구조화된 메세지를 보냅니다.
그러나 잠깐, flip은 어디에 있나요? NIO 에서 메세지를 전송하기 전에 java.nio.ByteBuffer.flip()을 호출하는데 사용하지 않았죠? ByteBuf는 두 이유 때문에 이러한 메서드가 없습니다. 하나는 읽기 작업이고 나머지는 쓰기 작업 때문입니다. 쓰기 인덱스는 읽기 인덱스가 변경하지 않는 동안 ByteBuf에 무엇인가 쓸 때 증가합니다. 읽기 인덱스와 쓰기 인덱스는 메세지의 시작과 끝을 각기 나타냅니다.
반면에 NIO 버퍼는 어디서 메세지 내용이 시작하고 flip 메서드 호출없이 끝내는지 계산할 깨끗한 방법을 제공하지 않습니다. 아무것도 아니거나 잘못된 메세지가 보내지기 때문에 버퍼를 플립(flip)하는 걸 잊어 먹을 때, 문제가 될 것 입니다. 이러한 오류는 네티(Netty) 에서 일어나지 않습니다. 왜냐하면 다른 작업 타입을 위한 다른 포인터가 있기 때문입니다. 그것에 익숙해지는 만큼 훨씬 쉬운 작업이 되는 것을 찾을 것입니다.
또 다른 주의해야 할 점은 ChannelHandlerContext.write() (와 writeAndFlush()) 는 ChannelFuture 를 리턴합니다. 한 ChannelFuture 는 아직 발생하지 않은 I/O 작업을 나타냅니다. 이 의미는 어떤 요청된 작업이 아직 수행되지 않았을 수 있는데, 네티(Netty) 에선 모든 작업이 비동기이기 때문입니다. 예를 들어 다음 코드는 메세지가 보내지기 전에 커낵션이 닫힐 수 있습니다.123Channel ch = ...;
ch.writeAndFlush(message);
ch.close();
그러므로, write() 메서드에 의해 리턴된 ChannelFuture 이 완료한 후 close() 메서드를 호출할 필요가 있고, 쓰기 작업이 완료 되어졌을 때, 리스너에게 통지합니다. close()는 또한 즉시 커낵션이 끊어지지 않을 수 있는 것에 주의 하세요. 그리고 이것도 ChanelFuture를 리턴합니다. - 쓰기 요청이 완료되면 통지는 어떻게 받을까요? 리턴된 ChannelFuture에 ChannelFutureListener를 추가하는 만큼 간단합니다. 여기서 우리는 작업이 완료 되었을 때, 채널을 닫는 새로운 익명의 ChannelFutureListener 를 생성 했습니다.
또는 사전에 정의된 리스너를 사용하여 코드를 단순화 할 수 있습니다.1f.addListener(ChannelFutureListener.CLOSE);
테스트하기 위해 우리의 타임 서버가 기대한 것 처럼 작동한다면, UNIX rdate 커맨드를 사용 할 수 있습니다.
1 | $ rdate -o <port> -p <host> |
<port> 부분은 main() 메서드에서 명시화한 포트 번호이고, <host>는 대개localhost 이다.
타임 클라이언트 작성하기
DISCARD 와 ECHO 서버와 다른, TIME 프로토콜을 위한 클라이언트가 필요한데 사람은 32비트 바이너리 데이터를 달력의 데이터로 변환할 수 없기 때문입니다. 이 섹션에서 우리는 서버가 올바르게 작동하는 확실한 방법을 논의하고, 네티(Netty)로 클라이언트를 작성하는 방법을 배웁니다.
네티(Netty)에서 서버와 클라이언트 사이에 가장 큰 차이점은 Bootstrap 과 Channel 구현이 사용되는 부분입니다. 다음 코드에서 봐 주시기 바랍니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | package io.netty.example.time; public class TimeClient { public static void main(String[] args) throws Exception { String host = args[ 0 ]; int port = Integer.parseInt(args[ 1 ]); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); // (1) b.group(workerGroup); // (2) b.channel(NioSocketChannel. class ); // (3) b.option(ChannelOption.SO_KEEPALIVE, true ); // (4) b.handler( new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast( new TimeClientHandler()); } }); // Start the client. ChannelFuture f = b.connect(host, port).sync(); // (5) // Wait until the connection is closed. f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); } } } |
- Bootstrap 은 이러한 클라이언트 측 같은 서버가 아닌 채널이나 비연결 채널을 제외하고 ServerBootstrap 과 유사합니다.
- 오직 EventLoopGroup 하나만 명시했다면, 그건 보스와 워커 그룹으로 양쪽 모두 사용될 것입니다. 보스 워커는 클라이언트 측 이지만 사용되지 않습니다.
- NioServerSocketChannel 대신에 NioSocketChannel 이 클라이언트 측의 Channel 을 생성하는데 사용되어지고 있습니다.
- 우리는 클라이언트 측의 SocketChannel이 부모를 가지고 있지 않기 때문에 ServerBootstrap 에 했던 것과 달리 여기서 childoption()을 사용하지 않는것을 참고하세요.
- 우리는 connect() 메서드를 bind() 메서드 대신에 호출해야 합니다.
당신이 볼 수 있듯이, 서버 측 코드와 정말 다르지 않습니다. ChannelHandler 구현은 어떤가요? 서버로 부터 32비트 정수를 받고, 사람이 읽을 수 있는 포멧으로 변환하고, 변환된 시간을 출력하고, 커넥션을 닫아야 합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | package io.netty.example.time; import java.util.Date; public class TimeClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf m = (ByteBuf) msg; // (1) try { long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L; System.out.println( new Date(currentTimeMillis)); ctx.close(); } finally { m.release(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } |
- TCP/IP의 네티(Netty)는 -'ByteBuf'- 로 피어에게 보냈던 데이터를 읽습니다.
이는 매우 간단히 보이고 서버측 예제와 어떤 다른점이 보이지 않습니다. 그러나 핸들러는 때때로 IndexOutOfBoundsException을 발생시켜 작업을 거부 할 것입니다. 다음 섹션에서 왜 이게 일어나는지 논의합니다.
스트림 기반 전송 다루기
소켓 버퍼의 하나의 작은 통고
이러한 TCP/IP 같은 스트림 기반 전송에서, 수신된 데이터는 소켓 수신 버퍼 안에 쌓여집니다. 불행히도 스트림 기반 전송 버퍼는 패킷 큐가 아니고 바이트 큐 입니다. 이 의미는 두 개의 독립적인 패킷처럼 두 개의 메세지를 보냈다면 OS는 두 개의 메세지로 취급하지 않지만 바이트의 묶음처럼 취급합니다. 그러므로 원격 피어가 보냈던 것을 정확히 읽기를 보장하지 않습니다. 예를 들어 OS의 TCP/IP 스택이 세 개의 패킷을 수신한 것으로 가정 해 봅시다.
ABC | DEF | GHI
스트림 기반 프로토콜의 일반적인 이 속성 때문에 어플리케이션에서 다음처럼 조각난 것을 읽을 가능성이 높습니다.
AB | CDEFG | H | I
따라서, 수신하는 부분에 관계없이 서버쪽이나 클라쪽이 수신된 데이터를 하나 혹은 어플리케이션 로직에 의해 쉽게 이해 될 수 있게 더 의미있는 프레임으로 드프래그(defrag) 해야 합니다. 위의 예제의 경우, 수신된 데이터는 다음과 같이 틀을 잡아야 합니다.
ABC | DEF | GHI
첫번째 해결법
지금 TIME 클라이언트 예제로 돌아가 봅시다. 우리는 그 같은 문제를 가지고 있습니다. 32비트 정수는 매우 작은 데이터 량이고 자주 파편화 될 것 같지 않습니다. 그러나 문제는 파편화 될 수 있습니다. 그리고 파편 가능성은 트래픽 증가에 따라 커질 것입니다.
단순한 해결법은 내부 축적 버퍼를 만드는 것이고 내부 버퍼에 4바이트 모두 수신될때까지 기다립니다. 다음은 그 문제를 고쳐 TimeClientHandler 구현을 수정한 것입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | package io.netty.example.time; import java.util.Date; public class TimeClientHandler extends ChannelInboundHandlerAdapter { private ByteBuf buf; @Override public void handlerAdded(ChannelHandlerContext ctx) { buf = ctx.alloc().buffer( 4 ); // (1) } @Override public void handlerRemoved(ChannelHandlerContext ctx) { buf.release(); // (1) buf = null ; } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf m = (ByteBuf) msg; buf.writeBytes(m); // (2) m.release(); if (buf.readableBytes() >= 4 ) { // (3) long currentTimeMillis = (buf.readUnsignedInt() - 2208988800L) * 1000L; System.out.println( new Date(currentTimeMillis)); ctx.close(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } |
- ChannelHandler 는 두 라이프 사이클 리스너 메서드가 있습니다. handlerAdded() 와 handlerRemoved() 입니다. 장 시간동안 차된되지 않는 임의의 (비)초기화 작업을 수행할 수 있습니다.
- 첫째로, 모든 수신된 데이터는 buf에 축적 되어야 합니다.
- 그리고, 핸들러는 이 예제에서 4바이트의 충분한 데이터를 가지고 있는지 체크해야 하고, 실제 비지니스 로직을 수행 합니다. 그렇지 않으면 네티(Netty)는 더 많은 데이터가 도착할 때 channelRead() 메서드를 다시 호출 할 것입니다. 그리고 결국 4바이트 모두 계산 되어 질 것입니다.
두번째 해결법
첫번째 해결법에서 TIME 클라이언트의 문제를 풀었지만 수정 된 핸들러는 깨끗하게 보이지 않습니다. 이러한 가변 길이 같은 여러 필드로 구성되어진 더 복잡한 프로토콜을 상상해 보세요. ChannelInboundHandler 의 구현은 유지보수가 매우 빨리 어렵게 될 것입니다.
눈치 챘겠지만, 하나의 ChannelHandler 보다 ChannelPipeline 에 더 많이 추가 할 수 있습니다. 그러므로 하나의 일체식 ChannelHandler 를 어플리케이션의 복잡함을 줄이기 위해 여러 모듈들로 나눌 수 있습니다. 예를 들어 TimeClientHandler를 두 개의 핸들러로 나눌 수 있습니다.
- 단편화 이슈를 처리하는 TimeDecoder, 그리고
- TimeClientHandler의 초기화 간단 버전
1 2 3 4 5 6 7 8 9 10 11 12 | package io.netty.example.time; public class TimeDecoder extends ByteToMessageDecoder { // (1) @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { // (2) if (in.readableBytes() < 4 ) { return ; // (3) } out.add(in.readBytes( 4 )); // (4) } } |
- ByteToMessageDecoder는 파편화 이슈를 쉽게 처리하도록 만들어 주는 ChannelInboundHandler 의 구현체 입니다.
- ByteToMessageDecoder 는 decode() 메서드를 새로운 데이터가 수신되면 언제든지 내부적으로 유지된 누적 버퍼와 함께 호출 합니다.
- decode()는 누적된 버퍼에 충분한 데이터가 없을 경우 out 에 아무것도 추가되지 않게 결정할 수 있습니다. ByteToMessageDecoder는 더 데이터를 수신했을 때, decode()를 호출 할 것입니다.
- 만약 decode()가 out 에 목적물을 추가 한다면, 이는 디코너(decoder)가 메세지를 성공적으로 디코딩 했다는 것을 의미합니다. ByteToMessageDecoder 는 누적된 버퍼의 읽은 부분을 폐기할 것입니다. 여러 메세지를 디코드할 필요가 없음을 기억해 주세요. ByteToMessageDecoder는 out 에 추가하지 않을 때까지 decode() 메서드 호출을 계속 할 것입니다.
이제 우리는 ChannelPipeLine에 삽입해야 하는 또 하나의 핸들러를 가지고 있습니다. 우리는 TimeCline에 ChannelInitializer 구현을 수정해야 합니다.
1 2 3 4 5 6 | b.handler( new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast( new TimeDecoder(), new TimeClientHandler()); } }); |
만약 당신이 모험적인 사람 이라면, 심지어 더 단순화 하는 ReplayingDecoder 로 시도해 보길 원할 수도 있습니다. 그래도 더 자세한 정보를 위해 API 레퍼런스에 문의할 필요가 있습니다.
1 2 3 4 5 6 7 | public class TimeDecoder extends ReplayingDecoder<Void> { @Override protected void decode( ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { out.add(in.readBytes( 4 )); } } |
게다가, 네티(Netty)는 매우 쉽게 대부분의 프로토콜을 구현할 수 있게 발군의(out-of-the-box) 디코더들을 제공하고, 획일적인(monolithic) 유지보수가 어려운 핸들러 구현을 하게 되는 것으로 부터 피하도록 도웁니다. 더 상세한 예제들을 위해 다음 패키지들을 참조 하세요.
- 바이너리 프로토콜을 위한 io.netty.example.factorial 과
- 텍스트 라인 기반 프로토콜을 위한 io.netty.example.telnet
첫째로, UnixTime이라 불리는 새로운 타입을 정의해 봅시다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package io.netty.example.time; import java.util.Date; public class UnixTime { private final long value; public UnixTime() { this (System.currentTimeMillis() / 1000L + 2208988800L); } public UnixTime( long value) { this .value = value; } public long value() { return value; } @Override public String toString() { return new Date((value() - 2208988800L) * 1000L).toString(); } } |
우리는 이제 ByteBuf 대신에 UnixTime을 생산하는 TimeDecode로 수정 할 수 있습니다.
1 2 3 4 5 6 7 8 | @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { if (in.readableBytes() < 4 ) { return ; } out.add( new UnixTime(in.readUnsignedInt())); } |
업데이트된 디코더로 TimeClientHandler는 더 이상 ByteBuf를 사용하지 않습니다.
1 2 3 4 5 6 | @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { UnixTime m = (UnixTime) msg; System.out.println(m); ctx.close(); } |
훤씬 더 간단하고 우아합니다. 맞죠? 같은 테크닉이 서버쪽에 적용 될 수 있습니다.
이번엔 첫번째로 TimeServerHandler 를 갱신해 봅시다.
1 2 3 4 5 | @Override public void channelActive(ChannelHandlerContext ctx) { ChannelFuture f = ctx.writeAndFlush( new UnixTime()); f.addListener(ChannelFutureListener.CLOSE); } |
이제, 오직 빠뜨린 부분은 인코더 인데, UnixTime을 다시 ByteBuf로 변환하는 ChannelOutboundHandler의 구현 입니다. 메세지를 인코딩 할 때 패킷 파편화와 조립을 처리할 필요가 없기 때문에 디코더를 작성하는 것보다 훨씬 단순합니다.
1 2 3 4 5 6 7 8 9 10 11 | package io.netty.example.time; public class TimeEncoder extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { UnixTime m = (UnixTime) msg; ByteBuf encoded = ctx.alloc().buffer( 4 ); encoded.writeInt(( int )m.value()); ctx.write(encoded, promise); // (1) } } |
- 이 한줄에 꽤 중요한 몇 가지가 있습니다.
첫째로, 우리는 인코드된 데이터가 실제로 소켓(wrie)에 쓰여질 때, 성공 혹은 실패를 네티(Netty)가 기록해서 오리지날 ChannelPromise 를 있는 그대로 전달합니다.
둘째로, 우리는 ctx.flush()를 호출 안 했습니다. flush() 작업을 오버라이드 하도록 의도된 헨들러 메서드 void flush(ChannelHandlerContext ctx) 가 있습니다.
심지어 더욱 단순화 하기 위해, MessageToByteEncoder 를 사용할 수 있습니다.
1 2 3 4 5 6 | public class TimeEncoder extends MessageToByteEncoder<UnixTime> { @Override protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) { out.writeInt(( int )msg.value()); } } |
마지막으로 남은 작업은 TimeServerHandler 전에 서버쪽의 ChannelPipeline에 TimeEncoder를 삽입하는 것입니다. (유저가이드에 코드가 없습니다.)
어플리케이션 종료하기
네티(Netty) 어플레케이션의 종료는 보통 shutdownGracefully()를 통해 생성된 모든 EventLoopGroup 들을 종료 하는 만큼 쉽습니다. 그것은 EventLoopGroup이 완전히 종료되어질 때 통지해주는 Future를 리턴하고 그룹에 속한 모든 Channel 들은 닫혀집니다.
개요
이 챕터에서, 우리는 네티(Netty)를 기반한 네트워크 어플리케이션을 완전히 작성하는 방법에 대한 데모로 네티(Netty)를 빠르게 여행 했습니다.
곧 이을 챕터들에 네티(Netty)에 대한 더 자세한 정보가 있습니다. 우리는 또한 io.netty.example 패키지에서 네티(Netty) 예제를 살펴보는 것이 좋습니다.
또한 the community(http://netty.io/community.html)는 항상 당신의 질문과 도움을 줄 아이디어를 가디리고 있으며, 네티(Netty) 및 문서를 당신의 피드백을 기반하여 계속해서 개선합니다.
'JAVA > Netty' 카테고리의 다른 글
Netty 프레임워크 SOCKET 옵션 (0) | 2019.05.20 |
---|---|
[Netty] 1. Netty의 기본설명 (0) | 2017.10.31 |
[Netty]3.3.1 ServerBootStrap API (0) | 2016.03.15 |
[Netty]2.2 블로킹과 논블로킹 (0) | 2016.03.11 |
[Netty]2.1. 동기 와 비동기 (0) | 2016.03.11 |
▶ JAVA 20. 소켓 통신 (0) | 2016.03.04 |
[JAVA] java.util package_Properties Class_예제 (0) | 2016.03.04 |
[JAVA]자바 소켓통신 예제 (0) | 2016.03.04 |