네티 알아보기 4일차 - 이벤트 핸들러

이벤트 루프

네티에서 빼놓을 수 없는 이벤트에 대해서 알아보고자 합니다.

통상적인 이벤트 기반 어플리케이션이 이벤트를 처리하는 방법은 크게 두 가지라고 합니다.

  • 이벤트 리스너와 이벤트 처리 스레드에 기반한 방법
  • 이벤트 큐에 이벤트를 등록하고 이벤트 루프가 큐에 접근하여 사용하는 방법

맨 위의 방식은 대부분의 UI처리 프레임워크가 사용하는 방법이라고 합니다. 로직을 리스너에 등록하고 처리 스레드가 등록된 로직을 수행하는 방식이죠.

생각나는 예를 들면

1
JS : document.getElementById("myBtn").addEventListener("click", displayDate);

정도 있겠네요.

두번쨰 방식인 이벤트 루프+큐의 방식은 객체에서 이벤트가 발생하면 이벤트 큐에 입력되고 이벤트 루프 스레드가 체크해서 이벤트를 가져와 처리하는 방식입니다.
계속해서 체크하며 돌기 때문에 Loop인것일까요?
여기서 스레드가 단일과 다중으로 나뉘고 이벤트의 결과를 돌려주는 방식에 따라 콜백 패턴과 퓨처 패턴으로 나뉩니다.
Netty는 둘 다 지원한다고 하니 잠시 후 알아보겠습니다.

단일/다중 스레드 이벤트 루프

단일 스레드 루프는 이벤트를 처리하는 스레드가 하나인 상태를 말합니다.
그래서 하나의 입력된 스레드가 이벤트 큐에 입력된 이벤트를 처리하므로 큐에 입력된 이벤트를 순차적으로 실행이 가능한 장점을 가지고 있지요.
단점은 당연히 기본 멀티코어인 요즘 시대에 코어를 1개밖에 못써먹는 치명적인 단점이 있습니다.
특히 JS기반의 Node.js… 그래서 멀티코어를 활용하고자 많은 방법을 개발 및 사용중입니다만 아무튼 그렇습니다.

다중 스레드 루프는 이벤트를 처리하는 스레드가 여러개인 상태를 말합니다.
단점은 이벤트 루프의 갯수가 한정적이므로 접근하려는 경합이 일어나며 여러 스레드가 실행하므로 실행순서와 발생순서가 일치하지 않습니다.
하지만 장점은 남는 자원을 아낌없이 다 쓸 수 있다는 점이지요.

영상하나를 올리고자 합니다. 해당 영상은 CPU vs GPU 영상 입니다만 CPU를 단일스레드, GPU를 멀티스레드라고 생각해보면 그림을 그린다는 목적을 똑같이 가졌을 때 멀티스레드로 인한 효율을 간접적으로 볼 수 있습니다.

nvidia youtube에서 퍼왔습니다.

하지만 다중 스레드 루프도 만능은 아닙니다. 스레드 경합과 컨텍스트 스위칭(스레드가 가진 스택 정보를 레지스터로 복사하는 작업)에 의한 비용이 늘어나서 코어의 활용으로 인한 성능향상보다 성능을 깎아먹게 되는 현상이 있으니 늘 적당한 갯수를 탄력적으로 모니터링하며 유지해야 하겠습니다.

네티의 이벤트 루프

Netty가 다중 스레드 루프를 사용했다면 너도 나도 가져가려는 이벤트 스레드로 인해 전송하다가 채널이 닫히는 등 엄청난 혼란이 일어났겠지만 Netty의 경우 다음과 같은 특징으로 인하여 다중 스레드 루프를 사용함에도 단점을 극복했다고 합니다.

  • 네티의 이벤트는 채널에서 발생
  • 이벤트 루프 객체는 큐를 가지고 있다
  • 네티의 채널은 하나의 이벤트 루프에 등록된다.

즉, 1채널당 1개의 이벤트 루프에만 등록되기 때문에 한곳에서만 처리가 가능하다는 것이지요.
그리고 루프 객체당 큐를 가지고 있기 때문에 큐를 공유하지 않아 다른 객체에서 해당 큐의 접근이 불가능하여 이벤트를 빼앗기는 일이 없다고 하겠습니다.

조금 비유하면 컨퍼런스를 갔는데 여러 홀에서 동시에 발표를 한다 가정하면 우리의 몸이 만화속 닌자마냥 분신술이 불가능하기 때문에 하나의 발표밖에 못듣게 되죠.
그래서 여러 발표(이벤트)를 함에도 홀(이벤트 큐)이 공유되지 않아 하나의 발표밖에 못듣는거죠.(이벤트 처리)

네티의 비동기 I/O 처리

앞서 살펴보았던 이벤트 처리에 비해 유용하게 쓸 수 있는 퓨처패턴을 사용해 보겠다고 합니다.(책에서)
퓨처패턴은 당장 완료되지는 않지만 언젠가는 완료될 것으로 생각하고 미리 짜 놓으면 퓨처 객체에서 메소드의 처리 결과에 따라 진행하는 패턴입니다.

간단한 코드로 표현하면 다음과 같은 것입니다.(실제로 실행은 안되니 의사코드로써 봐주시기 바랍니다.)

1
2
3
4
5
6
7
8
9
10
11
12
public class NettyFuture {
public static void main(String[] args) {
Work work = new Work();
Future<Work> nettyFuture = new Future<Work>() {
if (future.isDone) {
//일이 끝났다 다음 해야할 일을...
} else {
//끝나지 않았으니 빨리 빨리 정신으로 갈구자
}
}
}
}

실제로 여러분이 처음 작성한 코드에서는 퓨처패턴이 사용중이었습니다.

1
2
ChannelFuture f =bootstrap.bind(8888).sync();
f.channel().closeFuture().sync();

위의 채널퓨처 객체를 사용하고 있었죠. sync() 메소드가 bind의 결과가 올 때 까지 블로킹하고 bind의 처리가 완료되면 같이 sync메소드도 진행됩니다.
위의 채널이 닫힐 경우 sync로 같이 닫아주는 형식으로 진행되고 있죠.

즉 퓨처패턴은 일단 프로그래머는 로직을 짜면 추후에 미래의 결과에 따라 진행하는 방식입니다.

퓨처패턴의 진행결과를 가져와서 처리해야 하는데 while로 계속해서 가져오는 방식도 있으나 복잡성 증가라던지 조금 그래서… 이벤트 방식의 리스너에 담아서 사용도 가능합니다.

1
ChannelFuture.addListener(ChannelFutureListener.CLOSE) <-- 이런식으로 말이죠

다음은 해당리스너 인터페이스를 구현하면 될 것이고요.

일단 ChannelFutureListener 인터페이스만 살펴보면 해당 인터페이스를 통해서 제공하는 것은 다음과 같습니다.

1
2
3
ChannelFutureListener.CLOSE
ChannelFutureListener.CLOSE_ON_FAILURE
ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE

그 외에 여러가지 이벤트 리스너를 사용해서 비동기의 제어도 가능합니다.

ChannelFuture.addListener API

보시는것과 같이 꼭 하나의 리스너만 받고 있지는 않으니 여러가지로 응용하시면 되겠습니다.

결론

네티의 이벤트 루프 스레드는 단일과 다중 스레드에서 이벤트 수행 순서의 차이가 없는 것이 장점이며 루프 스레드의 갯수를 쉽게 조정할 수 있으니 편하신대로 개발하시면 되겠습니다.
더 적은 고민으로 좋은 품질의 제품을 만들 수 있도록 해주었으니 잘 써먹어야겠죠.

네티 알아보기 3일차 - 채널 파이프라인 + 코덱

주요 용어들

정의

  • 채널 파이프라인 - 채널에서 발생한 이벤트가 이동하는 통로
  • 이벤트 핸들러 - 채널 파이프라인을 따라 이동한 이벤트를 처리하는 클래스
  • 코덱 - 이벤트 핸들러를 상속받아서 구현한 것

네티의 이벤트 실행

보통의 연결된 소켓에서 데이터를 받는다면 다음과 같은 로직으로 처리할 것입니다.

  1. 소켓에 데이터가 있는지 확인
  2. 데이터가 있으면 읽는 메서드를 호출 후 데이터 처리
  3. 데이터가 올 때 까지 기달리다가 오면 2번으로 돌아감
  4. 네트워크가 종료되면 관련된 부분 처리

Netty에서는 다음과 같이 처리합니다.

  1. 데이터가 들어오면 Netty의 이벤트 루프가 채널 파이프라인에 등록된 첫 번째 이벤트 핸들러를 가져온다.
  2. 이벤트 핸들러에 데이터 수신 후 작동할 이벤트가 있는지 확인ㅁ
  3. 가져온 이벤트 핸들러에 관련 메소드가 없으면 이벤트가 처리되거나 핸들러를 다 가져올떄까지 계속 반복
  4. 네트워크가 종료되면 관련된 부분 처리

즉, 데이터를 입출력 후 처리하는 부분은 이벤트로 관리하고 있으니 이벤트 처리만 해주면 Netty를 쉽게 쓸 수 있습니다.

채널 파이프라인

구조

netty.io API의 설명

저렇게 채널에서 이벤트를 받으면 파이프라인으로 들어가서 이벤트 핸들러에 의해 맞는 이벤트를 처리하게 됩니다.
하나의 채널 파이프라인에 여러 이벤트 핸들러를 등록할 수 있습니다.

다시 에코서버안의 내용을 가져와서 보겠습니다.

1
2
3
4
5
6
7
8
bootstrap.group(loopGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new EchoServerHandler());
}
});

해당 코드를 보면 ChannelPipeline pipeline = socketChannel.pipeline()에서 파라미터로 받은 채널안의 파이프라인을 가져오고 다음 pipeline.addLast(new EchoServerHandler()) 메소드로 파이프라인의 마지막에 핸들러를 등록하는 것을 볼 수 있습니다.

그리고 Netty는 소켓채널에 파이프라인을 등록하고 핸들러의 설정을 등록하기 위해서 세 단계 프로세스를 거칩니다.

  1. 클라이언트 연결에 대응하는 채널 객체를 생성하고 빈 채널 파이프라인 객체를 생성하여 소켓 채널에 할당합니다.
  2. 채널에 등록된 ChannelInitializer 인터페이스의 구현체를 가져와서 initChannel 메소드를 호출합니다.
  3. 1에서 등록된 파이프라인 객체를 가져온 후 파이프라인에 입력된 이벤트 핸들러 객체를 등록합니다.

위의 세 단계가 완료 되면 채널이 등록됐다는 이벤트와 함께 데이터 송수신을 위한 이벤트 처리가 시작된다.

이벤트 핸들러

Netty는 비동기 처리를 위해 두가지를 제공합니다. 하나는 퓨처패턴이며 하나는 이벤트 핸들러입니다.
여기서는 이벤트 핸들러에 대해 자세히 알아보겠습니다.

대부분의 이벤트는 한 번만 수행하지만 다시 이벤트를 발생시켜 다른 등록된 핸들러에서 추가로 이벤트 처리를 시킬 수 있습니다.

채널 인바운드 이벤트

인바운드 이벤트는 소켓 채널에서 발생한 이벤트 중 상대방이 어떤 동작을 취했을 때 발생합니다.
클라이언트가 서버에 접속한 상태에서 서버에게 데이터를 보낼 경우 Netty의 소켓 채널은 데이터를 받았다고 채널 파이프라인에게 데이터 수신 이벤트를 보내고 채널 파이프라인은 등록한 인바운드 이벤트 핸들러를 호출합니다.

ChannelInboundHandler인터페이스를 가지고 인바운드 이벤트를 처리할 수 있습니다.
안의 메소드는 다음과 같습니다.

Inbound

이벤트 호출 순서는 다음과 같습니다. 위에서 아래 순입니다.

1
2
3
4
5
6
7
8
9
10
11
이벤트 루프에 채널 등록(channelRegistered)

채널 활성화(channelActive)

데이터 수신(channelRead)

데이터 수신 완료(channelReadComplete)

채널 비활성화(channelInactive)

이벤트 루프에서 채널 제거(channelUnregistered)

아마 대부분 메인로직은 데이터 수신~수신완료에 쓰일것이라 보입니다.

이벤트를 간단히 정리해보겠습니다.

  • channelRegistered의 이벤트 발생 시점은 서버와 클라이언트 모두 처음 소켓 채널이 생성할 때 발생하며, 서버의 경우 클라이언트 소켓 채널이 서버 소켓 채널에 등록될 때 다시 발생합니다.
  • channelActive의 이벤트는 서버 또는 클라이언트가 연결한 직후 한 번 수행할 작업을 처리하기에 적합합니다.(예를 들면 Hello Client 라던가…)
  • channelReadchannelReadComplete 이벤트는 데이터의 수신시 발생하지만 Read는 채널에 데이터가 있을 때 발생하고 Complete는 데이터를 다 읽었을 때 발생합니다.
  • channelInactive가 발생하면 Read가 불가능합니다.
  • channelUnregistered가 발생하면 등록했던 소켓에 대한 이벤트 처리가 불가능합니다.

채널 아웃바운드 이벤트

대부분 클라이언트 객체에서 처리할 것으로 보이는 아웃바운드입니다.
요청을 보내거나 데이터 전송, 소켓 닫기 등의 이벤트를 가지고 있습니다.

Outbound

이벤트를 간단히 정리해보겠습니다.

  • bind 이벤트는 서버 소켓 채널에 클라이언트가 연결을 대기하는 IP와 포트가 설정되었을 때 발생합니다. 파라미터 중 SocketAddress로 서버에서 사용하는 IP/포트 정보를 확인 가능합니다.
  • connect 이벤트는 서버에 연결되었을 때 발생합니다.
  • disconnect 이벤트는 연결이 끊어졌을 때 발생합니다.
  • close 이벤트는 소켓 채널의 연결이 닫혔을 때 발생합니다.
  • write 이벤트는 소켓 채널에 데이터가 기록되면 발생합니다.
  • flush 이벤트는 소켓 채널에 대한 flush 메소드가 호출되면 발생한다고 합니다.

ChannelHandlerContext

대부분의 이벤트가 ChannelHandlerContext를 파라미터로 제공하는 것을 볼 수 있습니다. 이 녀석의 경우 채널 파이프라인이나 다른 핸들러의 상호작용을 도와주는 인터페이스입니다.
즉, writeAndFlush등의 메서드로 채널에 데이터를 기록하며 close 메소드로 연결종료나 자신이 속한 파이프라인의 동적 수정, 다음 Handler에 통지하는 등 다양한 기능을 수행합니다.

코덱

우리가 동영상 플레이어를 설치 시 말하는 그 코덱과 의미가 같습니다. 즉, 전송을 보내는/받은 파일을 특정 루틴에 의해 압축,변환 시키고 해제를 시키는 라이브러리 같은 개념으로 생각하면 되겠죠.
Netty 에서는 코덱을 통해 데이터-패킷으로 상호 변환을 합니다. 그리고 Netty의 코덱은 템플릿 메소드 패턴을 사용했다고 합니다.(실행순서만 위에서 설정하고 실제 구현은 하위에서)

사용자가 직접 만들 수 있지만 대부분의 프로토콜은 Netty에서 기본제공하므로 가져다가 쓰시는 것을 추천합니다.
기본 예제는 io.netty.example 패키지에 포함되어있다고 합니다.

결론

이벤트가 참 많네요. 코덱까지 세트로 제공하니 쉽게 데이터를 주고 받을 수 있을 것 같은 느낌입니다.
3일차를 마치고 4일차로 가보겠습니다. 점점 양이 많아지는것을 느낍니다.

네티 알아보기 2일차 - 부트스트랩

2일차를 신나게 달려봅시다.

3장

프로토콜

아무래도 통신관련 프레임워크기 때문에 통신 프로토콜을 모를 수도 없어서 간단하게 알아보도록 하겠습니다.

TCP

TCP의 경우 연결을 확인하고 메시지를 전송하는 것을 보장하는 특성을 가지고 있습니다.
메시지를 받을때마다 받았다는 신호를 보내 서로 전송되었고 받았는지 확인하죠.

UDP

UDP는 메시지는 전송하지만 받았는지 전혀 확인하지 않기 때문에 누락, 분실, 메시지 순서를 보장하지 않는 등의 특징을 가지고 있습니다.
그럼에도 왜 쓰냐하면 체크하는 과정이 없기 때문에 TCP에 비해 상대적으로 오버헤드가 적고 속도가 빠른 장점이 있습니다.

SCTP

검색하다가 IBM에서 좋은 정보를 제공하기에 가져왔습니다.

스트림 전송 제어 프로토콜(SCTP)은 TCP와 유사한 연결 지향 프로토콜이지만 UDP와 유사한 메시지 지향 데이터 전송을 제공합니다.
일반적으로 SCTP는 안정적이면서도 메시지 지향적인 데이터 전송을 필요로 하는 VoIP(Voice over IP)와 같은 특정 애플리케이션에 대해 더 많은 유연성을 제공합니다. 이런 애플리케이션 범주에 대해 SCTP는 대부분 TCP나 UDP보다 더 적합합니다.

  • TCP는 신뢰 가능하며 엄격한 전송 순서의 데이터 전달을 제공합니다. 신뢰성이 필요로 하지만 순서화되지 않았거나 부분적으로만 순서화된 데이터 전달을 허용하는 애플리케이션의 경우, TCP는 HOL(head-of-line) 블로킹으로 인해 불필요한 지연을 초래할 수 있습니다. 단일 연결 내에 여러 스트림 개념을 사용하는 SCTP는 데이터를 다른 스트림으로부터 논리적으로 분리하는 동시에 하나의 스트림 내에서 엄격한 순서로 전달할 수 있습니다.
  • SCTP는 바이트 지향인 TCP와는 달리 메시지 지향입니다. TCP의 바이트 지향 특성으로 인해 애플리케이션은 메시지 경계를 유지하려면 고유한 레코드 표시를 추가해야 합니다.
  • SCTP는 멀티-홈 기능을 사용하여 어느 정도의 결함 허용치를 제공합니다. 호스트는 동일하거나 다른 네트워크에서 둘 이상의 네트워크 인터페이스가 접속된 경우 멀티-홈으로 간주됩니다. 두 개의 멀티-홈 호스트 사이에 SCTP 연관을 설정할 수 있습니다. 이 경우 두 엔드포인트의 모든 IP 주소가 연관 시작 시에 교환됩니다. 이를 통해 각 엔드포인트는 인터페이스 중 하나가 어떤 이유에서이건 작동 중지된 경우 대체 인터페이스를 통해 피어에 연결 가능한 한 남은 연결 기간 동안 이러한 주소를 사용할 수 있습니다.
  • SCTP는 TCP 및 UDP는 제공하지 않는 추가 보안 기능을 제공합니다. SCTP에서 연결 설정 시 자원 할당은 쿠키 교환 메커니즘을 사용하여 클라이언트의 ID를 검증하기 전까지 지연되어 서비스 거부 공격 가능성을 줄입니다.

프로토콜간 차이점

표 하나로 설명합니다.

차이점(어디서 가져온지 기억이 잘..)

부트스트랩이란?

제가 아는 웹 프론트엔드 프레임워크가 아니라 Netty에서 가장 처음 수행되는 부분으로써 동작과 설정을 지정하여 가장 기본이 되는 부분이라 합니다.
API Doc의 Bootstrap 클래스에서는 다음과 같이 얘기하고 있습니다.

부트스트랩은 채널을 쉽게 사용할 수 있도록 해주는 것

구조

논리적 구조는 다음과 같습니다.

  • 소켓 모드 및 I/O종류
  • 이벤트 루프
  • 채널 파이프라인 설정
  • 소켓 주소 및 포트 등의 옵션 설정

빌더 패턴을 사용하여 선택적인 옵션을 파악하기 쉽도록 추가할 수 있습니다.

시작해보기

가장 처음에 작성했던 에코 서버 코드를 가져와서 조금 수정 후 확인해봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
EventLoopGroup loopGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();

ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(loopGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new EchoServerHandler());
}
});

ChannelFuture f =bootstrap.bind(8888).sync();
f.channel().closeFuture().sync();

먼저 new NioEventLoopGroup()를 보면 생성자의 인수가 없는데 이럴 경우 사용할 스레드 수를 하드웨어의 스레드 수로 한다고 합니다.
스레드 수는 하드웨어가 가지고 있는 CPU 코어 수의 2배를 사용한다 합니다. 4C 8T의 경우 하드웨어(CPU)에서 지원하는 8T의 두 배인 16개죠. 그리고 loopGroup 변수는 사용할 스레드 수를 1개로 지정하였습니다.
ServerBootstrap.group() 메소드는 NioEventLoopGroup을 인수로 받는데 첫번째는 연결에 사용할 스레드이고 두 번째 인수는 연결된 소켓에 대한 I/O처리를 담당합니다.
저는 연결은 한번만 하고 끝나니 하나만 사용했고, I/O의 경우 어떻게 몇개가 들어올지 모르니 여러개의 스레드를 사용했다고 생각했습니다.
.channel() 메소드는 API명세에 따르면 Channel 인터페이스를 구현한 객체나 채널팩토리를 사용할 수 있고 이 예제에서는 NioServerSocketChannel 클래스를 설정했기 때문에 Nio모드를 사용합니다.
.childHandler 메소드는 API에 채널에서 요청이 왔을 때 제공할 ChannelHandler를 설정합니다.

연결 방법을 바꾸고 싶으면 .channel() 부분에서 제공하는 클래스를 변경하면 입출력 모드를 쉽게 바꿀 수 있도록 Netty에서는 제공하고 있습니다.
지원하는 클래스는 다음과 같습니다.(안보이면 클릭하셔서 보시기 바랍니다)

지원클래스 with Netty

이 중 Epoll은 *nix에서 동작한다고 합니다.

부트스트랩 내 간단한 API 소개

channelFactory - 소켓 입출력 모드 설정

channel 메소드와 동일한 기능을 수행합니다.

handler - 이벤트 핸들러 설정

소켓 채널에서 발생한 이벤트를 수신하여 처리합니다.

childHandler - 소켓 채널의 데이터 가공 핸들러 설정

소켓 채널로 송수신 되는 데이터를 가공하는 핸들러입니다. ChannelHandler 인터페이스를 구현한 클래스를 인수로 입력 가능합니다.

option - 소켓의 옵션을 설정합니다.

소켓의 옵션이란 소켓의 동작 방식을 지정하는 것을 말합니다.
소켓 옵션은 어플리케이션의 값을 바꾸는 것이 아니라 커널에서 사용되는 값을 변경한다는 의미입니다.

childOption - 소켓의 옵션을 설정합니다.

option 메소드는 서버의 옵션을, childOption 메소드는 클라이언트의 옵션을 설정합니다.

group - 이벤트 루프 설정

소켓 채널의 이벤트 처리를 위한 루프 객체를 생성합니다.
클라이언트는 단 하나의 이벤트 루프만 설정할 수 있습니다.

channel - 입출력모드 설정

서버 - 클라이언트의 입출력 채널을 설정합니다.
서버와 클라이언트는 설정할 수 있는 입출력 모드가 상이합니다.

결론

부트스트랩으로 서버와 클라이언트에서 각자 연결하고 처리할 수 있는 방법을 쉽게 제공합니다.
여러가지 준비해 놓았으니 상황에 맞춰 가장 최적의 연결방법을 찾아서 해야 겠지요…

네티 알아보기 1일차 - 기본 환경설정

그동안 사놓고 안읽어봤던 자바 네트워크 소녀 Netty를 익히면서 쓰는 글입니다.
github blog + hexo로 만들어보는 첫번쨰 블로그이기도 하죠

1장

네티란?

Netty는 유지 보수 가능한 높은 성능 프로토콜 서버 & 클라이언트를 빠르게 개발할 수 있는 비동기적인 이벤트 기반 네트워크 어플리케이션 프레임워크라고 합니다.

시작해보기

1탄으로 Java 1.8 + maven 환경에서 작업해 보겠습니다. (추후에 생각나면 스프링도…)

라이브러리를 다운받아서 직접 lib 폴더에 넣으셔도 되지만 저는 그냥 maven으로 하겠습니다.

가장 먼저 프로젝트를 만들고 pom.xml에 netty 레포지토리 태그를 집어넣습니다.

개발환경은 5.0 alpha가 있지만 4.1 final버전으로 하겠습니다.

pom.xml에 다음과 같이 넣어주세요.

1
2
3
4
5
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.32.Final</version>
</dependency>

그리고 maven install을 하셔서 lib에 netty.jar가 정상적으로 들어있는지 확인해주세요.

intellij IDEA를 예로 들어서 정상적으로 라이브러리가 들어왔는지 확인 하려면 External Libraries에 들어있는지 확인하시면 되겠습니다.

라이브러리 확인

그럼 이제 Netty 프레임워크로 책내용을 따라서 만들어보겠습니다.

에코서버 만들기

일단 무조건 따라쳐봅시다.

서버를 만들어 봅시다.

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
public class EchoServer {

public void echo() throws InterruptedException {
EventLoopGroup loopGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();

try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(loopGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new EchoServerHandler());
}
});

ChannelFuture f =bootstrap.bind(8888).sync();

f.channel().closeFuture().sync();
} finally {
loopGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}

따라치다보면 같은 클래스 명을 가진 라이브러리를 만날 수도 있습니다만…(안만날수도있고요) 무조건 io.netty.이하로 선택합니다.

살펴보니 EventLoopGroup이라는 것을 만들어서 ServerBootStrap이라는 것에 집어넣습니다.
채널에 클래스를 뭔가 하나 담고 밑에서 일할 핸들러를 하나 만들고 파이프라인이라는 것을 만들어서 마지막에 핸들러를 하나 담았습니다.
그리고 8888 포트를 만들어서 sync메소드로 동기화같은 작업을 하나 봅니다.

당연히 실행은 new EchoServer.echo()로 하겠지요.

이번에는 파이프 라인에 들어갈 핸들러를 만들어 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String readMsg = ((ByteBuf)msg).toString(Charset.defaultCharset());
System.out.println("수신한 문자열 ["+readMsg + "]");
ctx.write(msg);
}

@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
ctx.close();
}
}

이벤트 기반의 프레임워크라고 소개했듯이 핸들러를 통해 이벤트에 맞춰 어떻게 작업할 것인지 선택합니다.
여기서는 ChannelInboundHandlerAdapter를 상속받아서 이벤트 콜백을 설정합니다.
channelRead와 모두 완료됐을때 호출될 channelReadComplete를 오버라이드 해서 완료했을때의 이벤트를 설정합니다.

그 다음 자바를 실행하고 telnet으로 접속해서 날려보면 자바 콘솔창에 확인이 가능합니다.

한글자씩 안찍히고 엔터키를 입력시 입력한 문자가 그대로 텔넷 클라이언트에 한번 더 찍힌다면 연결 종류가 Raw라서 그렇다고 한다. 리눅스 쓰는데 해당 증상이 나타나버렸습니다…

에코 클라이언트 만들기

같이 만들어 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class EchoClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf msgBuffer = Unpooled.buffer();
byte[] sendMsgByte = "반가워요".getBytes();
msgBuffer.writeBytes(sendMsgByte);
ctx.writeAndFlush(msgBuffer);
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String readMsg = ((ByteBuf)msg).toString(Charset.defaultCharset());
System.out.println("수신한 문자열 ["+readMsg + "]");
ctx.write(msg);
}

@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
}

책에 나온 차이점이라면 처음에 채널이 연결되었을때 이벤트를 추가해준것 밖에 없습니다.
실행해봅시다.

2장

인바운드, 아웃바운드

인바운드란?

당사자 입장에서 들어오는 것

아웃바운드란?

당사자 입장에서 나가는 것

예시

내가 친구에게 안녕이라는 메시지를 보냈다면, 친구입장에서는 안녕이라는 메시지가 인바운드 된 것이고 나에게는 안녕메시지가 아웃바운드 된 것이다.

동기, 비동기

이 것은 굳이 Netty가 아니더라도 수 많은 곳에서 사용합니다. 사전에 정의된 해설은 다음과 같습니다.

동기

데이터 전송에 있어서 일정 클록 신호에 맞추어 데이터의 송수신을 하는 방법으로 통신을 시작할 때, 데이터의 최초에 통신 속도와 통신 방법을 식별하기 위한 동기 캐릭터(synchronous character)를 첨부하여 송신하는 방법

비동기

통신을 하는 양쪽 장치가 데이터를 주고받을 때 일정한 속도를 유지하는 것이 아니라, 약정된 신호에 기준하여 동기를 맞추는 통신 방법

블로킹, 논블로킹

소켓통신에 주로 쓰이는 용어입니다. 물론 다른곳에도 쓰일수도 있지요.

블로킹

요청한 작업이 완료되거나 에러가 발생해서 끝날 때 까지 응답을 돌려주지 않는 방법

논 블로킹

작업 완료 여부와 상관없이 결과를 돌려주는 방법

블로킹의 경우 관공서 민원실이나 은행 업무처리정도로 생각하면 쉬울 것 같고 논 블로킹의 경우 공항이나 티켓교환소의 인포메이션 센터 정도로 생각하면 되지 않을까 싶습니다.
은행이나 관공서의 경우 1:1로 고객의 모든 일이 끝날때 까지 붙어서 처리하고 인포메이션 센터는 나에게 정보를 알려주고 내가 이해하는 동안 다른사람이 물어보면 바로 그 사람에게 응답을 해주고 내가 다시 질문 할 경우 알려주는 등 동시에 여러가지 여러명에게 업무를 한다고 생각하면 쉬울 것 같습니다.

이벤트기반 프로그래밍

내가 설정한 이벤트가 발생하면 이벤트에 직접 등록한 루틴이 실행되는 방식입니다.
즉, 이벤트 발생 -> 이벤트를 받아서 처리할 것이 있는지 검색 -> 실행 순이겠죠.

Netty에서는 ChannelInboundHandlerAdapter로 여러 이벤트 기반 프로그래밍을 지원합니다.
여기서 채널은 소켓 채널입니다.

Future(java)

간단하게 알아보면 어떠한 로직을 처리했을때 처리한 값을 받아 작업하는 방식입니다. 비동기에서 쓰이며 결과값을 얻을 때 까지 필요에 의해 블록처리가 될 수 있습니다.
작업이 정상적으로 완료/취소 되었는지 확인을 위해 방법을 제공하며 성공적으로 수행시 get 취소했을경우 cancel 메소드로 수행됩니다.(인터페이스 기준)

여기서 1일차를 마치겠습니다. 2일차에서 만나요.

about

나는 누구인가요?

부족한걸 알아서 어떻게든 채우려는 사람입니다. 취미는 게임과 컴퓨터예요.

일하면서 해봤던 것

frontend : HTML, CSS(with LESS), Javascript(with jQuery)
backend : JAVA(Spring), Classic ASP, ASP.NET & PHP(문자메시지 모듈만 연동해본 수준)
Database : oracle, mysql
Server : Apache-Tomcat, IIS, AWS(EC2, S3)

스터디에서 해봤던 것

Javascript(~ES6), Node.JS, Typescript(진행중)

경력사항

  1. SI회사(2015.11 ~ 2018.02)
  2. 인테리어 관련 쇼핑몰(2018.07 ~ 재직중)

오타수정, 오류수정 등등 contact

kyozzang20@gmail.com