자바 네트워크 소녀 네티( 정경석 저 / 이희승 감수, 한빛미디어 ) 를 읽고 정리합니다.
📘2부 4장. 네티 상세 - 채널 파이프라인과 코덱
🔸네티의 이벤트 실행
- 네티의 이벤트 루프가 채널 파이프라인에 등록된 첫 번째 이벤트 핸들러를 가져온다.
- 이벤트 핸들러에 데이터 수신 이벤트 메서드가 구현되어있으면 실행한다
- 데이터 수신 이벤트가 구현되어있지 않으면 다음 이벤트 핸들러를 가져온다.
- 2를 수행한다.
- 채널 파이프라인에 등록된 마지막 이벤트 핸들러에 도달할 때까지 1을 반복한다.
데이터를 처리하는 입출력은 네티가 이벤트로 관리하므로 해당 이벤트에 해당하는 코드만 구현하면 된다.
🔸채널 파이프라인
채널 파이프라인은 네티의 채널과 이벤트 핸들러 사이의 연결 통로 역할을 한다.
🔹채널 파이프라인 구조
네티는 이벤트 처리를 위한 추상화 모델로서 채널 파이프라인을 사용하는데 소켓 채널에서 발생한 이벤트가 이를 통로로 하여 이동한다.
🔹채널 파이프라인 동작
public class EchoServer {
public static void main(String[] args) {
...
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new EchoServerHandler());
}
});
...
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
- 클라이언트 연결에 대응하는 소켓 채널 객체를 생성하고 빈 채널 파이프라인 객체를 생성해서 소켓 채널에 할당한다
=> ( .childHandler(new ChannelInitializer<SocketChannel>()) - 소켓 채널에 등록된 ChannelInitializer 인터페이스 구현체를 가져와서 initChannel 메서드를 호출한다.
- 소켓 채널 참조로부터 1에서 등록한 파이프라인 객체를 가져오고 채널 파이프라인에 입력된 이벤트 핸들러의 객체를 등록한다. (p.addLast(new EchoServerHandler());)
위 단계가 끝나면 패널이 등록되었다는 이벤트가 발생하고 이때부터 클라이언트와 서버 간의 데이터 송수신을 위한 이벤트 처리가 시작된다.
🔸이벤트 핸들러
네티는 비동기 호출을 지원하는 두가지 패턴을 제공한다.
- 퓨처 패턴
- 리액터 패턴의 구현체인 이벤트 핸들러
- 소켓 채널의 이벤트를 인터페이스로 정의하고 이 인터페이스를 상속받은 이벤트 핸들러를 작성하여 채널 파이프라인에 등록한다.
- 채널 파이프라인으로 입력되는 이벤트를 이벤트 루프가 가로채어 이벤트에 해당하는 메서드를 수행하는 구조를 말한다.
🔹 채널 인바운드 이벤트
인바운드 이벤트는 소켓 채널에서 발생한 이벤트 중에서 연결 상대방이 어떤 동작을 취했을 때 발생한다.(채널 활성화, 데이터 수신)
클라이언트가 서버에 접속한 상태에서 서버로 데이터를 전송하는 상태이다.
- 소켓 채널에서 읽을 데이터가 있다는 이벤트를 채널 파이프라인으로 흘려보내고,
- 채널 파이프라인에 등록된 이벤트 핸들러중 인바운드 이벤트 핸들러가 해당 이벤트에 해당하는 메서드를 수행한다.
public interface ChannelInboundHandler extends ChannelHandler {
void channelRegistered(ChannelHandlerContext ctx) throws Exception;
void channelUnregistered(ChannelHandlerContext ctx) throws Exception;
void channelActive(ChannelHandlerContext ctx) throws Exception;
void channelInactive(ChannelHandlerContext ctx) throws Exception;
void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception;
void channelReadComplete(ChannelHandlerContext ctx) throws Exception;
void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception;
void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception;
@Override
@SuppressWarnings("deprecation")
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}
네티는 인바운드 이벤트를 ChannelInboundHandler 인터페이스를 제공한다.
서버 측의 소켓채널에서 발생하는 이벤트는 채널 등록, 채널 활성화, 데이터 읽기, 데이터 읽기 완료, 채널 비활성화, 채널 등록 해제 순서로 발생한다.
🔸channelRegistered
채널이 이벤트 루프에 등록되었을 때 발생한다.
이벤트루프는 네티가 이벤트를 실행하는 스레드로써 부트 스트랩에 설정한 이벤트 루프다.
channelRegistered는 서버, 클라이언트 상관 없이 새로운 채널이 생성되는 시점에 발생한다.
🔸channelActive
channelRegistered 이벤트 이후에 발생한다. 채널이 생성되고 이베트 루프에 등록된 이후에 네티 API를 사용해 채널 입출력을 수행할 상태가 되었음을 알려주는 이벤트다.
서버 또는 클라이언트가 상대방에 연결한 직후에 한 번 수행할 작업을 처리하기에 적합하다
🔸channelRead
데이터가 수신되었음을 알려준다. 수신된 데이터는 네티의 ByteBuf 객체에 저장되어있으며 이벤트 메서드의 두번째 인자인 msg를 통해 접근 가능하다.
🔸channelReadComplete
데이터 수신이 완료되었음을 알려준다. channelRead 는 데이터가 있을 때 발생하고 채널의 데이터를 다 읽어서 더 이상 데이터가 없을때는 channelReadComplete가 발생한다.
🔸channelInactive
채널이 비활성화되었음을 알린다. 이 이벤트 이후에 채널에 대한 입출력 작업을 수행할 수 없다 .
🔸channelUnregistered
채널이 이벤트 루프에서 제거되었을 때 발생한다.
🔹아웃바운드 이벤트
소켓 채널에서 발생한 이벤트 중에서 네티 사용자가 요청한 동작에 해당하는 이벤트를 말한다.
네티는 아웃바운드 이벤트를 ChannelOutboundHandler인터페이스로 제공한다.
public interface ChannelOutboundHandler extends ChannelHandler {
void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception;
void connect(
ChannelHandlerContext ctx, SocketAddress remoteAddress,
SocketAddress localAddress, ChannelPromise promise) throws Exception;
void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception;
void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception;
void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception;
void read(ChannelHandlerContext ctx) throws Exception;
void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception;
void flush(ChannelHandlerContext ctx) throws Exception;
}
🔸bind
서버 소켓 채널이 클라이언트의 연결을 대기하는 IP와 포트가 설정되었을 때 발생한다.
🔸connect
클라이언트 소켓 채널이 서버에 연결되었을 떄 발생한다.
🔸disconnect
클라이언트 소켓 채널의 연결이 끊어져을때 발생
🔸close
클라이언트 소켓 채널의 연결이 닫혔을 때 발생
🔸write
소켓 채널에 데이터가 기록되었을 때 발생, 데이터 버퍼가 인자로 입력된다.
🔸flush
소켓 채널에 대한 flush 메서드가 호출되었을 때 발생한다.
🔹이벤트 이동 경로와 이벤트 메서드 실행
채널 파이프라인에 여러 이벤트 헨들러가 등록되어있을 때 어떻게 이벤트 메서드가 실행되는지 찾아보자
public class EchoServer{
public static void main(String[] args) {
...
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new EchoServerFirstHandler());
p.addLast(new EchoServerSecondHandler());
}
});
public class EchoServerFirstHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String readMessage = ((ByteBuf) msg).toString(Charset.defaultCharset());
System.out.println("FirstHandler ChannelRead [" + readMessage + ']');
ctx.write(msg);
}
}
public class EchoServerSecondHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelReadComplete 발생");
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
다음과 같이 두 이벤트 핸들러를 등록했다.
EchoServer에 클라이언트 채널이 생성되고 해당 채널의 채널 파이프라인에 channelActive 이벤트가 발생했다.
-> 하지만 등록된 이벤트 핸들러에 channelActive가 구현되어있지 않으므로 해당 이벤트는 무시된다.
다음으로 channelRead 이벤트가 발생하고 채널 파이프라인에 등록된 EchoServerFirstHandler의 channelRead 이벤트 메서드가 수행된다.
마지막으로 EchoServerSecondHandler 에 구현된 channelReadComplete이벤트 메서드가 수행된다.
⇒ 여러개의 이벤트 핸들러가 등록되어있을 때 이벤트에 해당하는 이벤트 메서드가 수행된다.
| 🤔 동일한 이벤트 메서드를 구현한 이벤트 핸들러가 여럿이면 어떻게 동작할까?
public class EchoServerSecondHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String readMessage = ((ByteBuf) msg).toString(Charset.defaultCharset());
System.out.println("SecondHandler ChannelRead [" + readMessage + ']');
ctx.write(msg);
}
...
}
EchoServerSecondHandler 에 channelRead를 다음과 같이 추가하고 실행해보았다.
첫번째 핸들러만 실행되고 두번째 이벤트 핸들러는 실행되지 않았다.
⇒ 이벤트에 해당하는 이벤트 메서드가 수행되면서 이벤트가 사라졌기 때문이다.
만약 다음 이벤트 핸들러로 이벤트를 넘기려면 ChannelHandlerContext 인터페이스를 사용해서 채널 파이프라인에 이벤트를 발생하면 된다.
public class EchoServerFirstHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String readMessage = ((ByteBuf) msg).toString(Charset.defaultCharset());
System.out.println("FirstHandler ChannelRead [" + readMessage + ']');
ctx.fireChannelRead(msg);
}
}
write 처리하지 않고 다음 핸들러에게 넘기는 역할만 수행하기위해 ctx.write()를 제거하고 fireChannelRead()만 사용했다.
다음과 같이 두번째 핸들러의 ChannelRead 이벤트 코드가 발생하는 것을 확인할 수 있다.
'Netty' 카테고리의 다른 글
[Netty] 네티의 이벤트 루프와 비동기 I/O 처리 (1) | 2025.03.26 |
---|---|
[Netty] 코덱을 이용한 HTTP 서버 구현과 사용자 정의 코덱 작성 (0) | 2025.03.25 |
[Netty] 부트스트랩이란, 부트스트랩 구조 및 설정(ServerBootStrap, BootStrap) (0) | 2025.03.22 |
[Netty] 네티의 주요특징 - 이벤트 기반 프로그래밍 (1) | 2025.03.15 |
[Netty] 네티의 주요특징 - 동기와 비동기, 블로킹과 논블로킹 (0) | 2025.03.14 |