자바 네트워크 소녀 네티 책을보고 Netty의 정리의 필요성을 느껴 정리해봤다.
네티는 비동기 이벤트 기반 네트워크 어플리케이션 프레임워크로써 유지보수를 고려한 고성능 프로토콜 서버와 클라이언트를 빠르게 개발할 수 있다.
즉, TCP 통신을 위해 무조건 Netty를 써야하는 건 아니지만 유지보수하기도 쉽고, 비동기 이벤트 기반이기 때문에 고성능도 보장하게 된다.
네티로 작성한 네트워크 어플리케이션의 동작 방식과 환경을 설정하는 도우미 클래스, 주로 클라이언트 어플리케이션에 사용된다.
추상화가 잘 돼있어서 블로킹 모드에서 논블로킹 모드로 바꾸는 등의 설정이 매우 쉬우며 아래 설정이 가능하다.
설정항목
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true) //클라이언트 소켓 입출력 모드를 NIO로 설정
.option(ChannelOption.SO_KEEPALIVE, true) //클라이언트 소켓 모드를 KEEPALIVE로 설정
bootstrap.remoteAddress(tcpIp, tcpPort);
bootstrap.connect().addListener(new ConnectionListener(this));
Bootstrap 중에 서버의 설정을 돕기 위한 클래스, 주로 서버 어플리케이션에 사용된다. 아래 설정이 가능하다.
설정항목
.channel(NioServerSocketChannel.class) //서버 소켓 입출력 모드를 NIO로 설정
.handler(new LoggingHandler(LogLevel.INFO)) //서버 소켓 채널 핸들러 등록
ChannelFuture channelFuture = sb.bind(tcpPort).sync();
channelFuture.channel().closeFuture().sync();
EventLoop를 그룹핑한 것이다.
여러 EventLoop가 존재하는데, NioEventLoopGroup를 주로 사용한다.
EventLoop는 이벤트가 올 때까지 무한 반복을 도는 쓰레드이다.
NioEventLoop는 단일 쓰레드 이벤트 루프이다. (하나의 이벤트 루프에 하나의 쓰레드 할당)
// 클라이언트 -> 서버 설정
Bootstrap bootstrap = new Bootstrap();
EventLoopGroup eventLoop = new NioEventLoopGroup();
bootstrap.group(eventLoop)
// 서버 -> 클라이언트 설정
ServerBootstrap sb = new ServerBootstrap();
// 클라이언트 연결을 수락하는 부모 스레드 그룹
EventLoopGroup bossGroup = new NioEventLoopGroup(bossCount);
// 연결된 클라이언트의 소켓으로 부터 데이터 입출력 및 이벤트를 담당하는 자식 스레드
EventLoopGroup workerGroup = new NioEventLoopGroup();
sb.group(bossGroup, workerGroup)
자바의 바이트 버퍼 클래스(java.nio.ByteBuffer)와 유사하지만 더 나은 성능과 편의성을 가진 Netty의 버퍼 클래스
// channelRead() 함수에서 데이터를 읽고/쓸때 사용
ByteBuf byteBuf = (ByteBuf) msg;
byte[] btLength = new byte[4];
byte[] bytes;
int length = byteBuf.readableBytes();
if (byteBuf.hasArray()) {
bytes = byteBuf.array();
}
if (length >= 4) {
// 최종적으로 btLength 배열에 bytes배열의 0부터 btLength 길이의 값을 복사함
System.arraycopy(bytes, 0, btLength, 0, 4);
}
일반적인 소켓 프로그래밍에서 말하는 소켓과 같다고 보면 된다고 함.
네티의 채널과 이벤트 핸들러 사이에서 연결 통로 역할을 수행.
채널에서 발생한 이벤트가 채널 파이프라인을 타고 흘러가고, 이벤트 핸들러는 이벤트를 수신한 후에 본인이 처리해야하는 이벤트인지 판단하고 처리한다.
(동작구조의 핵심인듯함..)
//Bootstrap
public static final SendHandler sendHandler = new SendHandler();
.handler(new ChannelInitializer<SocketChannel>() { //송수신 되는 데이터 가공 핸들러
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IntegerHeaderFrameDecoder());
pipeline.addLast(new LoggingHandler(LogLevel.INFO));
pipeline.addLast(sendHandler);
}
});
//ServerBootstrap
private ReceiveHandler RECEIVE_HANDLER; // = new ReceiveHandler();
.childHandler(new ChannelInitializer<SocketChannel>() { //송수신 되는 데이터 가공 핸들러
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IntegerHeaderFrameDecoder());
pipeline.addLast(new LoggingHandler(LogLevel.INFO));
pipeline.addLast(RECEIVE_HANDLER);
}
});
연결 상대방이 어떤 동작을 취했을 때 발생함
channelRegistered - 채널이 이벤트 루프에 등록되었을 때 발생
서버 소켓 채널의 channelRegistered 이벤트는 서버 소켓 채널이 생성됐을 때 발생하고, 클라이언트 소켓 채널의 channelRegistered 이벤트는 새로운 클라이언트가 서버에 접속하며 클라이언트 소켓 채널이 생성될 때 발생한다.
channelActive - channelRegistered 이후에 발생
채널이 생성되고 이벤트 루프에 등록된 이후에 네티 API를 사용하여 입출력을 수행할 상태가 되었음을 알려주는 이벤트
channelRead - 데이터가 수신될 때마다 발생하는 이벤트
channelReadComplete - 데이터 수신이 완료됐을 때 발생하는 이벤트
channelRead 이벤트는 채널에 데이터가 있을 때 발생하고, channelReadComplete는 채널의 데이터를 다 읽어서 더 이상 데이터가 없을 때 발생한다.
channelInactive - 채널이 비활성화되었을 때 발생
channelUnregistered - 채널이 이벤트 루프에 제거되었을 대 발생
프로그래머가 요청한 동작에 해당하는 이벤트
bind - 서버 소켓 채널이 클라이언트의 연결을 대기하는 IP와 포트가 설정되었을 때 발생
connect - 클라이언트 소켓 채널이 서버에 연결되었을 때 발생
disconnect - 클라이언트 소켓 채널의 연결이 끊어졌을 때 발생
close - 클라이언트 소켓 채널의 연결이 닫혔을 때 발생
write - 소켓 채널에 데이터가 기록되었을 때 발생
flush - 소켓 채널에 flush 메서드가 호출되었을 때 발생
Event Handler
이벤트가 발생했을 때 이벤트를 처리하는 역할을 담당한다.
크게 이벤트 유형에 따라 ChannelInboundHandler, ChannelOutboundHandler 인터페이스로 나눌 수 있다.
채널에 대한 입출력 처리 및 채널 파이프라인에 대한 상호작용을 도와주는 인터페이스
ChannelHandlerContext의 writeAndFlush 메서드로 채널에 데이터를 기록하거나 close 메서드로 채널의 연결을 종료할 수 있다.
(writeAndFlush() 기록시 바이트버퍼 형태로변환 Unpooled.wrappedBuffer(param.getBytes("euc_kr")))
또한 ChannelHandlerContext는 채널이 초기화될 때 설정된 채널 파이프라인을 가져오는 메서드를 제공하기 때문에 채널 파이프라인을 수정할 수 있다.
private final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
// ex) channelActive(ChannelHandlerContext ctx)
// 원격호스트 주소확인
String remoteAddr = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress();
// 서버연결이 활성화되면 channelActive()가 실행되고, 채널그룹에 채널등록 후 추후에 데이터 들어왔을때 채널그룹의 채널을 가져와서 데이터를 쓰고/기록 함
if (channels.size() > 0) {
channels.close();
channels.clear();
}
channels.add(ctx.channel());
// 채널 활성화시 클라이언트-서버간 폴링 설정
...(생략)
그 외
// 데이터 수신이 완료시
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}