네트워크를 통해 전송되는 데이터는 모두 동일한 형식(바이트)이다. 바이트가 전송되는 구체적인 방법은 데이터 전송의 기본 메커니즘을 추상화하도록 도와주는 개념인 네트워크 전송에 의해 좌우된다. 사용자는 이러한 세부 사항에 신경 쓸 필요가 없고, 바이트를 안정적으로 전송 및 수신하는 방법만 알면 된다.
예제에 나온 기존 Socket 및 Selector를 이용한 응답 서버를 만드는 예제를 따라쳤었다.
github.com/jabel123/netty-study/tree/master
이 코드의 주 내용은 Socket과 Selector 를 이용하여 응답 서버를 구현하는 코드의 내용은 매우 다르지만, 네티를 이용할 경우 조금의 코드수정으로 OIO, NIO 서버의 변경이 가능하다는 점이었다.
전송 API
Channel 인터페이스는 모든 입출력 작업에 이용되므로 전송 API의 핵심이라고 할 수 있다. Channel에 ChannelPipeline과 ChannelConfig가 할단된다. ChannelConfig는 Channel에 대한 모든 구성 설정을 포함하며, 임시 변경을 지원한다. 특정한 전송에 고유 설정이 필요할 때는 ChannelConfig의 하위 형식을 구현할 수도 있다.
Channel은 고유하므로 정렬 순서를 보장하기 위해 java.lang.Comparable의 하위 인터페이스로 선언한다. 즉, 고유한 두 Channel 인스턴스가 동일한 해시 코드를 반환하는 경우 AbstractChannel의 compareTo()구현에서 Error가 발생한다.
ChannelPipeline은 인바운드와 아웃바운드 데이터와 이벤트에 적용될 ChannelHandler 인스턴스를 모두 포함한다. 이러한 ChannelHandler는 애플리케이션에서 상태 변경과 데이터 처리를 위한 논리를 구현한다.
ChannelHandler의 일반적인 용도는 다음과 같다.
-
데이터를 한 포멧에서 다른 포멧으로 변환
-
예외에 대한 알림 제공
-
Channel의 활성화 또는 비활성화에 대한 알림 제공
-
Channel을 EventLoop에 등록할 때 또는 등록 해제할 때 알림 제공
-
사용자 정의 이벤트에 대한 알림 제공
할당된 ChannelPipeline과 ChannelConfig에 접근하는 것 외에도 Channel의 메서드를 이용해 다음에 나오는 여러 중요한 작업을 할 수 있다.
메소드 이름 |
설명 |
eventLoop |
Channel에 할당된 eventLoop를 반환한다. |
pipeLine |
Channel에 할당된 ChannelPipeline을 반환한다. |
isActive |
Channel이 활성 상태일 떄 true를 반환한다. 활성의 의미는 기본 전송이 무엇인지에 따라 달라진다. 예를 들어, Socket전송은 원격 피어로 연결되면 활성 상태이지만, Datagram전송은 열리면 활성 상태이다. |
localAddress |
로컬 SocketAddress를 반환한다. |
remoteAddress |
원격 SocketAddress를 반환한다. |
write |
데이터를 원격 피어로 출력한다. 이 데이터는 ChannelPipeline으로 전달되며 플러시되기 전까지 큐에 저장된다. |
flush |
기반 전송으로 이전에 출력된 데이터를 플러시한다. |
writeAndFlush |
write()와 flush()를 모두 호출하는 편의 메서드 |
Channel로 기록하기
Channel channel = ..
ByteBuf buf = Unpooled.copiedBuffer(“your data”, CharsetUtil.UTF_8);
ChannelFuture cf = channel.writeAndFlush(but);
cf.addListener(new ChannelFutureListener(){
@Override
public void operationComplete(ChannelFuture future)
{
if (future.isSuccess()){
System.out.println(“Write Successful”);
}
else {
System.err.println(“Write error”);
}
}
});
네티의 Channel구현은 스레드에 대해 안전하므로 여러 스레드를 이용하는 경우에도 Channel의 참조를 저장하고 원격 피어에 뭔가를 출력할 때 이용할 수 있다. 다음 예제에서는 여러 스레드로 기록을 수행하는 예제가 나온다. 메시지는 코드에서 정한 순서에 따라 전송된다.
final Channel = ..
final ByteBuf but = Unpooled.copiedBuffer(“your data”, CharsetUtil.UTF_8).retain();
Runnable writer = new Runnable(){
@Override
public void run() {
channel.write(but.duplicate());
}
};
Executor executor = Executors.newCachedThreadPool(); // 스레드 풀 Executor에 대한 차몾를 얻음
// 한 스레드에서 기록
executor.execute(writer); // 쓰기 작업을 실행기에 전달해 한 스레드에서 실행
// 다른 스레드에서 기록
executor.execute(writer); // 쓰기 작업을 실행기에 전달해 다른 스레드에서 실행
포함된 전송
네티는 바로 사용할 수 있는 여러 전송을 기본 제공한다. 다만 이러한 전송이 모든 프로토콜을 지원하는 것은 아니므로 애플리케이션에서 이용하는 프로토콜과 호환되는 전송을 선택해야 한다. (여기서 말하는 프로토콜이 http 프로토콜과 같은 통신의 규약을 말하는 것인지 잘 이해를 못하겠다. 나와있는 전송의 경우 NIO, Epoll, OIO, 로컬 등인데 전송방식에 대한 방법을 프로토콜이라 부르는 것일까? ㅠ)
[네티가 제공하는 전송]
이름 |
패키지 |
설명 |
NIO |
java.nio.channels 패키지를 기반으로 이용(셀렉터 기반 방식) |
|
Epoll |
epoll()과 논블로킹 입출력을 위해 JNI를 이용한다. 전송은 S0_REUSEPORT와 마찬가지로 리눅스에서만 이용할 수 있으며, NIO전송보다 빠르고 완전한 논블로킹이다. |
|
OIO |
java.net 패키지를 기반으로 이용(블로킹 스트림 이용) |
|
로컬 |
VM에서 파이프를 통해 통신하는 데 이용되는 로컬 전송 |
|
임베디드 |
실제 네트워크 기반 전송 없이 ChannelHandler를 이용할 수 있게 해주는 임베디드 전송. 이 전송은 ChannelHandler 구현을 테스트 하는데 아주 유용하다. |
NIO : 논블로킹 입출력
NIO는 모든 입출력 자원의 완전한 비동기 구현을 제공하며, JDK 1.4에서 NIO하위 시스템이 도입된 이후 사용할 수 있게 된 셀렉터 기반 API를 활용한다.
셀렉터의 기본 개념은 Channel의 상태가 변경되면 요청이 알림을 받을 수 있는 레지스트리 역할을 하는 것이다. 지원되는 상태 변경은 다음과 같다.
- 새로운 Channel이 수락되고 준비됨
- Channel 연결이 완료됨
- Channel에 읽을 데이터가 있음
- Channel을 이용해 데이터를 기록할 수 있음
애플리케이션이 상태 변경에 반응한 후에는 셀렉터가 재설정되며 해당 스레드에서 변경을 검사하고 적절하게 반응하는 프로세스가 반복된다. java.nio.channels.SelectionKey 클래스에 정의된 비트 패턴에 해당하는 상수가 나온다. 이러한 패턴 조합에 애플리케이션이 알림을 요청하는 상태 변경의 집합을 지정할 수 있다.
이름 |
설명 |
ON_ACCEPT |
새로운 연결이 수락되고 Channel이 생성되면 알린다. |
ON_CONNECT |
연결되면 알린다. |
ON_READ |
데이터를 읽을 수 있으면 알린다. |
ON_WRITE |
Channel로 데이터를 기록할 수 있으면 알린다. 소켓 버퍼가 완전히 차는 상황을 처리한다. 이러한 상황은 원격 피어의 처리능력보다 데이터가 더자주 전송될떄 흔히 발생한다. |
NIO의 이러한 내부 사항은 네티의 모든 전송 구현에 공통적인 사용자 수준 API에 의해 숨겨진다.
Epoll : 리눅스용 네이티브 논블로킹 전송
네티의 NIO전송은 자바가 제공하는 비동기/논블로킹 네트워크의 공통 추상화에 기반을 둔다. 덕분에 네티의 논블로킹 API를 모든 플랫폼에서 이용할 수 있지만 JDK는 모든 시스템에서 동일한 기능을 제공하기 위해 기능을 절충해야 하므로 어느정도 제약을 감수해야 한다는 뜻이기도 하다.
고성능 네트워킹 플랫폼으로서 리눅스의 중요성이 높아지면서 확장성이 우수한 입출력 이벤트 알림 기능인 epoll을 비롯한 다양한 고급 기능이 개발됐다.
네티는 적은 부담으로 인터럽트를 수행하며, 자체 설계와 더 일관되게 epoll을 이용하는 리눅스용 NIO API를 제공한다. 제작중인 애플리케이션이 리눅스 플랫폼을 대상으로 한다면 이 버전을 이용하는것으 ㄹ고려한다. 고부하 조건에서 JDK의 NIO보다 훨씬 우수한 성능을 보이기 떄문이다.
OIO: 기존 블로킹 입출력
네티의 OIO전송 구현은 절충이 필요한 상황을 위한 것으로서, 공통적인 전송 API를 통해 접근할 수 있지만 java.net의 블로킹 구현에 기반을 두므로 비동기 방식은 아니지만 특수한 용도에 아주 적합하다.
JVM 내부 통신용 로컬 전송
네티는 동일한 JVM 내에서 실행되는 클라이언트와 서버 간 비동기 통신을 위한 로컬 전송을 제공한다. 이 전송도 모든 네티 전송 구현에 공통적인 API를 제공한다.
임베디드 전송
네티는 ChannelHandler를 다른 ChannelHandler안에 도우미 클ㄹ래스로 넣을 수 있는 임베디드 전송을 제공한다. 이 방식을 이용하면 내부 코드를 수정하지 않고 ChannelHandler의 기능을 확장할 수 있다.
음,, 뭔가 무슨 전송이 있고, 네티를 사용할 경우 어떠한 전송방식을 이용하던 거의 일관성을 보여줄 수 있다는 것을 이 챕터에서는 다룬 듯 하다. 계속 읽어나가다보면 자연스럽게 네티 라이브러리를 사용할 수 있을까 하는 생각을 하게 되며 이번 챕터의 공부를 마친다.
'IT > Netty' 카테고리의 다른 글
네티에서 제공하는 웹소켓관련 기능을 이용한 웹 채팅 구현 (0) | 2020.12.05 |
---|---|
네티 단위테스트를 위한 EmbeddedChannel (0) | 2020.12.03 |
네티 컴포넌트 (0) | 2020.11.25 |
Netty로 echo서버 만들기 (0) | 2020.11.23 |