"자바 네트워크 소녀 Netty" 공부하면서 정리한 내용입니다.
1. 채널 파이프라인
a. 채널 파이프라인 구조
채널파이프라인은 채널과 이벤트 핸들러 사이에서 연결 통로 역할은 한다.
b. 채널 파이프라인 구성
EventLoopGroup bossEventLoopGroup = new NioEventLoopGroup(1);
EventLoopGroup workerEventLoopGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossEventLoopGroup, workerEventLoopGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() { // 1
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline p = socketChannel.pipeline(); // 2
p.addLast(new EchoServerHandler()); // 3
}});
} finally {
workerEventLoopGroup.shutdownGracefully();
bossEventLoopGroup.shutdownGracefully();
}
1. ChannelInitializer는 채널이 생성될 때 자동으로 호출된다.
2, 3. 파이프라인 객체를 생성하고, 파이프라인에 이벤트 핸들러를 등록한다.(빌더패턴으로 여러개의 핸들러를 등록할 수 있다.)
2. 이벤트 핸들러
이벤트 핸들러는 채널에서 발생하는 이벤트를 처리하는 인터페이스이다.
채널에서 발생하는 이벤트는 채널활성화, 데이터 입출력, 채널 종료 등을 이벤트로 관리하기 때문에, 본인 입맛대로 이벤트 핸들러만 구현해주면 된다.
네티에서는채널에서 발생하는 이벤트를 크게 인바운드 이벤트와 아웃바운드 이벤트로 추상화했다.
a. 채널 인바운드 이벤트
연결된 상대방이 어떤 동작을 취했을 때 발생하는 이벤트이다.
채널 활성화, 데이터 수신 등의 이벤트가 인바운드 이벤트에 해당된다.
데이터를 수신하여 이벤트가 발생하는 과정이다.
1. 네티의 이벤트 루프가 채널 파이프라인에 등록된 첫번째 이벤트 핸들러를 가져온다.
2. 이벤트 핸들러에 데이터 수신 이벤트 메서드가 구현되어 있으면 실행한다.
3. 데이터 수신 이벤트가 구현되어 있지 않으면 다음 이벤트 핸들러를 가져온다.
4. 마지막 핸들러가 도달할 때 까지 2번, 3번 과정을 반복한다.
인바운드 이벤트 발생 순서
1. 이벤트 루프에 채널 등록(channelRegistered)
- 채널이 이벤트 루프에 등록되었을 때 발생
- 서버는 처음 서버소켓 채널이 생성될 때와 클라이언트가 접속하여 클라이언트 채널이 생성될 때 발생
- 클라이언트는 서버에 connect()를 시도할 때 발생
2. 채널 활성화(channelActive)
- 채널 생성과 이벤트 루프에 등록된 후 채널 입출력이 가능한 상태가 되었을 때 발생
- 연결 즉시 한번 수행할 작업에 적합
3. 데이터 수신(channelRead)
- 데이터가 수신될 때 발생
- ByteBuf(msg) 객체로 전달
4. 데이터 수신완료(channelReadComplete)
- 데이터 수신이 완료되었을 때 발생
- 채널에 데이터가 있을 때는 channelRead가 발생하고, 채널에 데이터를 다읽어서 더이상 데이터가 없을 때 channelReadComplete 발생
5. 채널 비활성화(channelInactive)
- 채널이 비활성화 되어 입출력이 불가능한 상태가 되었을 때 발생
6. 이벤트루프에서 채널 제거(channelUnregistered)
- 이벤트루프에서 채널이 제거되었을 때 발생
인바운드 이벤트를 제공하는 ChannelInboundHandlerAdapter를 상속한 EchoServerHandler이다.
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
public EchoServerHandler() {
super();
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
super.channelRegistered(ctx);
// 채널이 이벤트 루프에 등록되었을 때 수행할 코드
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
// 채널 생성과 이벤트 루프에 등록된 후 채널 입출력이 가능한 상태가 되었을 때 수행할 코드
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 데이터가 수신될 때 수행할 코드
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
super.channelReadComplete(ctx);
// 데이터 수신이 완료되었을 때 수행할 코드
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
// 채널이 비활성화 되어 입출력이 불가능한 상태가 되었을 때 수행할 코드
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
super.channelUnregistered(ctx);
// 이벤트루프에서 채널이 제거되었을 때 수행할 코드
}
}
b. 채널 아웃바운드 이벤트
요청한 동작에 해당하는 이벤트를 말한다.
연결요청, 데이터 전송, 소켓 닫기 등이 해당된다.
ChannelHandlerContext 역할
1. 채널에 대한 입출력 처리
- writeAndFlush()로 채널에 데이터를 기록하고, close() 채널의 연결을 종료할 수 있다.
2. 채널 파이프라인과 상호작용
- 파이프라인에 등록된 이벤트 핸들러를 동적으로 변경할 수 있다.
- 논리적인 오류가 발생했을 때 fireExceptionCaught를 호출하면 exceptionCaught 이벤트가 발생하기 때문에 exceptionCaught 메서드에 오류를 처리하는 로직을 작성하면 된다.
아웃바운드 이벤트 발생 순서
1. bind
- 서버소켓 채널이 클라이언트 연결을 대기하는 ip와 port가 설정할 때 발생
2. connect
- 클라이언트 소켓 채널이 서버에 연결할 때 발생(서버 주소가 필요)
3. disconnect
- 소켓 채널의 연결을 끊을 때 발생
4. close
- 소켓 채널의 연결을 닫을 때 발생
5. write
- 소켓 채널에 데이터를 기록할 때 발생
6. flush
- 소켓 채널에 대해 flush할 때 발생
서버
channel = bootstrap.bind(8888).sync().channel();
channel.write("데이터");
channel.flush();
channel.disconnect();
channel.close();
클라이언트
channel = bootstrap.connect("127.0.0.1", 8888).channel();
channel.write("데이터");
channel.flush();
channel.disconnect();
channel.close();
c. 이벤트 이동 경로와 이벤트 메서드 실행
channelRead를 구현한 두개의 핸들러가 파이프라인에 등록되어 있다. 이런 경우에는 이벤트가 어떻게 발생할까?
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline p = socketChannel.pipeline();
p.addLast(new FirstHandler());
p.addLast(new SecondHandler());
}
});
public class FirstHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf readMsg = (ByteBuf) msg;
System.out.println("FirstHandler ReadMsg : " + readMsg.toString(Charset.defaultCharset()));
ctx.write(msg); // 받은 데이터를 되돌려주기 위해 채널에 기록
}
}
public class SecondHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf readMsg = (ByteBuf) msg;
System.out.println("SecondHandler readMsg : " + readMsg.toString(Charset.defaultCharset()));
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("ReadComplete 발생");
ctx.flush(); // 채널에 기록된 데이터를 flush
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
실행결과
FirstHandler ReadMsg : a
ReadComplete 발생
FirstHandler ReadMsg : b
ReadComplete 발생
FirstHandler ReadMsg : c
ReadComplete 발생
FirstHandler ReadMsg : f
ReadComplete 발생
실행결과를 보면 첫번째로 등록된 이벤트 핸들러의 channelRead만 호출되는 것을 확인할 수 있다. 두번째 핸들러의 channelRead 이벤트가 발생하지 않은 이유는 첫번째 등록된 이벤트 핸들러를 수행하면서 이벤트가 사라졌기 때문이다.
두번째 핸들러에게 이벤트를 전달하고 싶으면 fireChannelRead를 호출하여 이벤트를 발생시키면 된다.
public class FirstHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf readMsg = (ByteBuf) msg;
System.out.println("FirstHandler ReadMsg : " + readMsg.toString(Charset.defaultCharset()));
ctx.write(msg); // 받은 데이터를 되돌려주기 위해 채널에 기록
ctx.fireChannelRead(msg);
}
}
실행결과
FirstHandler ReadMsg : a
SecondHandler readMsg : a
ReadComplete 발생
FirstHandler ReadMsg : b
SecondHandler readMsg : b
ReadComplete 발생
FirstHandler ReadMsg : c
SecondHandler readMsg : c
ReadComplete 발생
실행결과를 보면 첫번째 channelRead에서 이벤트를 발생시켜 두번째 핸들러의 channelRead 이벤트가 발생하는 것을 확인할 수 있다.
3. 코덱(Codec)
코덱은 전송/수신되는 데이터를 전송 프로토콜에 맞춰 변환 작업을 수행하는 것이다.
encode: 전송할 데이터(아웃바운드)를 프로토콜에 맞춰서 변환
decode: 수신한 데이터(인바운드)를 프로토콜에 맞춰서 변환
네티는 자주사용되는 프로토콜에 대한 코덱을 제공한다.
- base64 코덱
- bytes 코덱
- compression 코덱
- http 코덱
- marshalling 코덱
- protobuf 코덱
- rtsp 코덱
- sctp 코덱
- spdy 코덱
- string 코덱
- searialization 코덱
제공하는 코덱을 사용하지 않고 직접 구현해서 사용할 수도 있다.
'Programming > etc' 카테고리의 다른 글
[Netty] Netty 단위 테스트 작성하기 (0) | 2024.01.17 |
---|---|
[Netty] 바이트버퍼(ByteBuffer) (0) | 2024.01.15 |
[Netty] 이벤트 모델 (0) | 2024.01.15 |
[Netty] Netty의 Bootstrap(부트스트랩) (0) | 2024.01.12 |
[Netty] 네트워크 프레임워크 Netty란? (1) | 2024.01.11 |