class: center, middle # Netty 시작하기 (1) 고성능 네트워크 애플리케이션 프레임워크 [김대현](http://medium.com/@hatemogi) --- ## 김대현 #### 프리랜서 서버 개발자 * 2017, Netty 기반 LDAP 서버 개발 중 * 2016, Akka 기반 서버 개발 * 2015, Netty 기반 SMTP 서버 개발 * 2013, 사내 Redis SaaS 개발팀장, 160+ VM * 2010, 클라우드, 네트워크 파일동기화 프로토콜 설계 * 2010, MyPeople, 네트워크 아키텍쳐 설계 * 2009, Cafe 채팅 부하분산 서버 개발 * 2004, Cafe 한줄메모장 개발, 일일 3억 PV --- ## 지금 보시는 발표 자료는... > ### http://hatemogi.github.io/netty-startup 위 주소에서 직접 볼 수 있습니다. --- # 과정 * Netty의 기본 설명 * 문제 풀이 * 예제 이해 / 실습 개발 ## 목표 * Event-Driven, 비동기 I/O 이해 * Netty로 개발 경험 * (조만간) 실무에 응용 ??? 이 과정의 목표입니다. Netty의 기본을 설명하며, 중간중간 문제풀이로 이해를 돕고, 직접 예제를 개발해보는 과정을 통해, 오늘 4시간을 잘 보내고나면, "Event-Driven 서버 개발이 무엇인지, 비동기 I/O는 무엇인지 알겠다", "나도 Netty로 개발해봤다", "어쩌면 실무에 응용해볼 수 있겠다"는 생각이 드셨으면 좋겠습니다. --- ## 준비사항 * [IntelliJ Community](https://www.jetbrains.com/idea/download/) * [JDK 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) * [Git 클라이언트](http://git-scm.com/downloads) --- ## 5시간 진행 주제 1. 비동기 개념, 기본 인터페이스(`Channel`) 정리 1. [고성능 메모리 모델과 유연한 파이프라인](2.html) 1. [깔끔한 스레드 모델, Future/Promise 활용](3.html) 1. [풍부한 코덱과 WebSocket](4.html) --- ## 예제와 실습 1. 첫 Netty 서버: Discard 서버, Echo 서버 1. HTTP 서버를 직접 만들어 보자 1. 텍스트 프로토콜 기반의 채팅 서버를 만들자 1. 웹소켓을 써서 채팅 서버를 웹환경으로! > 매시간 직접 작성하셔야 하는 (간단한) 코드가 있습니다. --- ## 매 시간 구조
1. 설명 (~30분) 1. 예제 설명 / 데모 (~10분) 1. pair 실습 (~30분) * 총 9개 문제 풀이 --- class: full-height background-image: url(img/excercise.png) --- ##
다음의 공통점 * [Nginx](http://nginx.org) * [HAProxy](http://www.haproxy.org) * [Memcached](http://memcached.org) * [Redis](http://redis.io) * [Node.js?](http://nodejs.org) --- # 왜 Netty를 쓸까? * 고성능 * 쓰기 편하다 (유연하고 쉽다) --- ## 얼마나 빠른가? [![](img/webframeworkbenchmarks.png)](http://www.techempower.com/benchmarks/#section=data-r9&hw=i7&test=plaintext) --- ## Netty가 고성능인 이유 * Non-blocking Asynchronous 처리가 기본 * 적은 스레드로 많은 요청을 처리 * GC부하를 최소화하는 Zero-copy ByteBuf 지원 --- ## Netty가 편한 이유 * 각종 네트워크 프로토콜 (코덱) 기본 지원 * 필요한 부분을 쉽게 조립해 쓸 수 있는 구조 * 멀티스레딩 처리 없이도... --- ## Netty 버전 > 이 자료와 실습은 4.1.11.Final을 기준으로 합니다. --- # 동기와 비동기 * Synchronous vs. Asynchronous * Blocking vs. Non-blocking ## 우리말로 하자면... * 요청하고 결과를 계속 기다리기 * 완료되면 알려달라고 요청하고 다른 일 하기 --- ##
고객센터에 전화를 했는데...
모든 상담원이 통화중입니다. 1. 연결될 때까지 기다리기 1. 전화해달라는 기록 남기고 끊기 --- ##
동기 or 비동기 *
커피 주문 * 지하철 이용, 자가 운전 * 업무요청 메일, 업무문의 전화 * 팀장의 업무지시 --- ## 동기 (Synchronous) 코드
*Blocking* call ```java // 동기 방식 void greeting(Context ctx) { String req = ctx.readLine(); ctx.write("안녕, " + req); System.out.println("완료"); } ``` --- ## 비동기 (Asynchronous) 코드
*Non-blocking* call ```java // 비동기 방식 void greeting(Context ctx) { ctx.readLine().done(line -> { ctx.write("안녕, " + req); }); System.out.println("완료"); } ``` --- # Event-Driven Programming > In computer programming, event-driven programming is a programming paradigm in which the flow of the program is determined by events such as user actions (mouse clicks, key presses), sensor outputs, or messages from other programs/threads. Event-driven programming is the dominant paradigm used in graphical user interfaces and other applications (e.g. JavaScript web applications) that are centered on performing certain actions in response to user input.
Wikipedia
--- ## 사건 기반 프로그래밍 > 사건 기반 프로그래밍(영어: Event-driven programming; EDP)은 비주얼 베이직과 같이, 사용자의 명령·마우스 클릭·다른 프로그램의 메시지·키보드 글쇠 입력 등의 ‘사건’에 따라, 제어 흐름이 결정되어 일을 하도록 하게끔 만들어진 프로그래밍 언어 방식을 뜻한다.
위키백과
--- ## 등잔 밑의 Event-Driven 코드 ```javascript $("button").on("click", function(event) { console.log(event); alert("클릭"); }); ``` * 자바스크립트 * iOS 앱 * Android 앱 * 데스크탑 애플리케이션 --- ## True or False
* 비동기와 Event-driven은 같은 뜻이다? * Event-Driven은 데스크탑앱 클라이언트에만 적합하다? * 비동기식 싱글스레드에서는 Blocking Call을 할 수 없다? --- ##
업무요청시 * 업무 요청하고 옆에서 기다리고 있으면? * 업무 요청하고 돌아갔다가, 나중에 다시 와서 확인하면? * 업무 요청하면서, "다되면 저한테 알려주세요"하면? --- # Netty는 Async Event-Driven > 어? 그럼 그냥 Java의 NIO를 쓰면 안되나요? 됩니다만... --- # Netty의 핵심 인터페이스 * `Channel` * `ChannelFuture` * `ChannelHandler` * `ChannelHandlerContext` * `ChannelPipeline` * `EventLoop` --- ## Channel * 읽기, 쓰기, 연결(connect), 바인드(bind)등의 I/O 작업을 할 수 있는 요소 또는 네트워크 연결 * 모든 I/O 작업은 비동기 -> `ChannelFuture` ### 핵심 메소드 ```java ChannelFuture write(Object obj) ChannelFuture flush(Object obj) ChannelFuture writeAndFlush(Object obj) ChannelFuture closeFuture() ChannelPipeline pipeline() SocketAddress remoteAddress() ``` --- ## ChannelFuture * `Channel`의 I/O 작업의 결과 * `ChannelFutureListener`를 등록 결과에 따른 작업 ### 핵심 메소드 ```java ChannelFuture addListener(GenericFutureListener listener); Channel channel(); boolean isSuccess(); Throwable cause(); ChannelFuture await() ChannelFuture sync() ``` --- ## ChannelHandler * Netty의 핵심 요소! * Netty의 I/O 이벤트를 처리하는 인터페이스 * `ChannelInboundHandlerAdapter` * `ChannelOutboundHandlerAdapter` ### *전체* 메소드 ```java void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) void handlerAdded(ChannelHandlerContext ctx) void handlerRemoved(ChannelHandlerContext ctx) ``` --- ## ChannelHandlerContext * `ChannelHandler`는 `ChannelHandlerContext`를 통해 * 다음 `ChannelHandler`에게 이벤트를 넘기거나, * 동적으로 `ChannelPipeline`을 변경할 수 있음 - [실습4] ### 핵심 메소드 ```java Channel channel() ChannelPipeline pipeline() ChannelFuture write(Object msg) ChannelHandlerContext fireChannelActive(Object msg) ChannelHandlerContext fireChannelRead(Object msg) ``` --- ## ChannelPipeline * `Channel`에 드나드는 inbound / outbound 이벤트를 처리 * [Intercepting Filter 패턴](http://www.oracle.com/technetwork/java/interceptingfilter-142169.html) 처리, `ChannelHandler` 리스트 * 두번째 시간에 상세 설명 ### 주요 메소드 ```java ChannelPipeline addLast(ChannelHandler... handlers) ChannelPipeline addLast(String name, ChannelHandler handler) ChannelHandler remove(String name)
T remove(Class
handlerType) ``` --- ## EventLoop * 등록된 `Channel`들의 *모든 I/O 작업*을 처리 * 구현체 `NioEventLoopGroup`를 주로 사용 ### 주요 메소드 ```java boolean inEventLoop()
Future
submit(Callable
task)
Promise
newPromise()
ScheduledFuture
schedule(Callable
callable, long delay, TimeUnit unit) ``` --- ## Channel관련 인터페이스 전체 구조
--- ## 예제와 실습용 프로젝트 > https://github.com/hatemogi/netty-startup ### Git clone ```nohighlight git clone https://github.com/hatemogi/netty-startup ``` > 처음에는 실패하는 유닛테스트가 준비돼 있고, 실습문제를 모두 풀면 유닛 테스트가 모두 통과돼야합니다. #### Netty 4.1 JavaDoc * http://netty.io/4.1/api/index.html --- # 첫 예제: DiscardServer > src/nettystartup/h1/discard * DiscardServer.java * DiscardServerHandler.java --- ##
src/nettystartup/h1/discard/
DiscardServer.java ```java public final class DiscardServer { public static void main(String[] args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new DiscardServerHandler()); ChannelFuture f = b.bind(8010).sync(); System.err.println("Ready for 0.0.0.0:8010"); f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } } ``` --- ##
src/nettystartup/h1/discard/
DiscardServerHandler.java ```java class DiscardServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(C..H..Context ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; try { // discard } finally { buf.release(); // 이 부분은 두번째 시간에 설명합니다. } } @Override public void exceptionCaught(C..H..Context ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } ``` --- class: full-height background-image: url(img/discard_run1.png) --- class: full-height background-image: url(img/discard_run2.png) --- ## DiscardServer 테스트 > telnet localhost 8010 ```nohighlight Trying ::1... Connected to localhost. Escape character is '^]'. helo test ^] telnet> close Connection closed. ``` --- # Netty와 유닛테스팅 * EmbeddedChannel을 활용 ```java EmbeddedChannel ch = new EmbeddedChannel(new HandlerToBeTested()); ``` --- ## EmbeddedChannel ```java public class EmbeddedChannel extends AbstractChannel { public EmbeddedChannel(ChannelHandler... handlers) {} public Object readInbound() { ... } public Object readOutbound() { ... } public boolean writeInbound(Object... msgs) { ... } public boolean writeOutbound(Object... msgs) { ... } public void checkException() { ... } } ``` --- ##
test/.../h1/discard/
DiscardServerHandlerTest.java ```java public class DiscardServerHandlerTest { @Test public void discard() { String m = "discard test\n"; EmbeddedChannel ch = new EmbeddedChannel(new DiscardServerHandler()); ByteBuf in = Unpooled.wrappedBuffer(m.getBytes()); ch.writeInbound(in); ByteBuf r = ch.readOutbound(); assertThat(r, nullValue()); } } ``` --- class: full-height background-image: url("img/discard_test1.png") --- class: full-height background-image: url("img/discard_test2.png") --- # 첫번째 실습: EchoServer
* `EchoServer` * `EchoServerHandler` * `EchoServerHandlerTest` --- ##
src/nettystartup/h1/echo/
EchoServer.java ```java public final class EchoServer { public static void main(String[] args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class); // TODO: [실습 1-1] 이 부분을 채워서 EchoServerHandler를 등록합니다 ChannelFuture f = b.bind(8020).sync(); f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } } ``` --- ##
src/nettystartup/h1/echo/
EchoServerHandler.java ```java class EchoServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { // TODO: [실습1-2] 받은대로 응답하는 코드를 한 줄 작성합니다. release는 필요하지 않습니다. } @Override public void channelReadComplete(ChannelHandlerContext ctx) { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } ``` --- ##
test/nettystartup/h1/echo/
EchoServerHandlerTest.java ```java public class EchoServerHandlerTest { @Test public void echo() { String m = "echo test\n"; EmbeddedChannel ch = new EmbeddedChannel(new EchoServerHandler()); ByteBuf in = Unpooled.copiedBuffer(m, CharsetUtil.UTF_8); ch.writeInbound(in); ByteBuf r = (ByteBuf)ch.readOutbound(); releaseLater(r); assertThat("응답이 없습니다", r, notNullValue()); assertThat("참조수는 1이어야 합니다",r.refCnt(), is(1)); assertThat("수신한 텍스트가 결과로 와야합니다", r.toString(CharsetUtil.UTF_8), equalTo(m)); } } ``` --- ## 실습 정리 * Netty로 네트워크 서버를 만들어 봤습니다. * ChannelHandler의 기초를 익혔습니다. * 메모리 모델은 다음 시간에 설명합니다. --- ## 참고자료 * [User guide for Netty 4.x](http://netty.io/wiki/user-guide-for-4.x.html) * [Netty4 Intro](http://normanmaurer.me/presentations/2014-http-netty/slides.html) * [Why Netty?](http://normanmaurer.me/presentations/2014-netflix-netty/slides.html) * [The C10K problem](http://www.kegel.com/c10k.html) * [Web Framework Benchmarks](http://www.techempower.com/benchmarks/#section=data-r9&hw=i7&test=plaintext) * [Reference Counted Objects](http://netty.io/wiki/reference-counted-objects.html) * [Netty 4.x JavaDoc](http://netty.io/4.0/api/index.html) --- ## 다음 시간에는... > http://hatemogi.github.io/netty-startup/2.html