앞서 '[Java] Netty 프레임워크 소개'에서 Netty 프레임워크에 대해서 간단하게 알아봤다. 백문이 불여일견이라고 실제로 동작하는 Netty 애플리케이션 코드를 보고 눈으로 확인하는게 더 중요할 수도 있다.
이번 포스트에서는 클라이언트의 입력을 그대로 응답으로 돌려주는 에코(Eco) 서버를 Netty 프레임워크로 구현해보겠다. 동작하는 전체 에코서버 코드는 다음과 같다.
우선 메인 클래스다.
package SimpleNettyServer;
import java.net.InetSocketAddress;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.util.concurrent.DefaultThreadFactory;
import io.netty.util.concurrent.GlobalEventExecutor;
public class EchoServer {
private static final int SERVER_PORT = 11011;
private final ChannelGroup allChannels = new DefaultChannelGroup("server", GlobalEventExecutor.INSTANCE);
private EventLoopGroup bossEventLoopGroup;
private EventLoopGroup workerEventLoopGroup;
public void startServer() {
bossEventLoopGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("boss"));
workerEventLoopGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("worker"));
// Boss Thread는 ServerSocket을 Listen
// Worker Thread는 만들어진 Channel에서 넘어온 이벤트를 처리
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossEventLoopGroup, workerEventLoopGroup);
// Channel 생성시 사용할 클래스 (NIO 소켓을 이용한 채널)
bootstrap.channel(NioServerSocketChannel.class);
// accept 되어 생성되는 TCP Channel 설정
bootstrap.childOption(ChannelOption.TCP_NODELAY, true);
bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
// Client Request를 처리할 Handler 등록
bootstrap.childHandler(new EchoServerInitializer());
try {
// Channel 생성후 기다림
ChannelFuture bindFuture = bootstrap.bind(new InetSocketAddress(SERVER_PORT)).sync();
Channel channel = bindFuture.channel();
allChannels.add(channel);
// Channel이 닫힐 때까지 대기
bindFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
close();
}
}
private void close() {
allChannels.close().awaitUninterruptibly();
workerEventLoopGroup.shutdownGracefully().awaitUninterruptibly();
bossEventLoopGroup.shutdownGracefully().awaitUninterruptibly();
}
public static void main(String[] args) throws Exception {
new EchoServer().startServer();
}
}
그 다음으로 클라이언트로부터 메시지를 받았을 때, 처리할 Handler 클래스다
package SimpleNettyServer;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
String message = (String)msg;
Channel channel = ctx.channel();
channel.writeAndFlush("Response : '" + message + "' received\n");
if ("quit".equals(message)) {
ctx.close();
}
}
}
마지막으로 서버측 채널 파이프라인을 구성하는 Initializer 클래스다
package SimpleNettyServer;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class EchoServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LineBasedFrameDecoder(65536));
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StringEncoder());
pipeline.addLast(new EchoServerHandler());
}
}
요 소스코드들을 하나씩 리뷰해보면서 동작원리를 알아보자.
Netty Server 생성
Netty TCP 서버를 생성하기 위해서는 다음 과정을 수행해야한다.
- EventLoopGroup을 생성
- ServerBootstrap을 생성하고 설정
- ChannelInitializer 생성
- 서버 시작
EventLoopGroup 생성
bossEventLoopGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("boss"));
workerEventLoopGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("worker"));
NIO 기반의 EventLoop를 생성해주자. bossEventLoopGroup은 서버 소켓을 listen할 것이고, 여기서 만들어진 Channel에서 넘어온 데이터는 workerEventLoopGroup에서 처리된다.
ServerBootStrap 생성 및 설정
ServerBootstrap bootstrap = new ServerBootstrap();
// EventLoopGroup 할당
bootstrap.group(bossEventLoopGroup, workerEventLoopGroup);
// Channel 생성시 사용할 클래스 (NIO 소켓을 이용한 채널)
bootstrap.channel(NioServerSocketChannel.class);
// accept 되어 생성되는 TCP Channel 설정
bootstrap.childOption(ChannelOption.TCP_NODELAY, true);
bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
// Client Request를 처리할 Handler 등록
bootstrap.childHandler(new EchoServerInitializer())
netty 서버를 생성하기 위한 헬퍼 클래스인 ServerBootstrap 인스턴스를 만들어준다.
우선 만들어둔 EventLoopGroup을 bootstrap의 group() 메소드로 세팅해준다. 채널을 생성할 때 NIO 소켓을 이용한 채널을 생성하도록 channel() 메소드에 NioServerSocketChannel.class를 인자로 넘겨준다.
그리고 TCP 설정을 childOption()으로 설정해둔다. TCP_NODELAY, SO_KEEPALIVE 설정은 이 서버 소켓으로 연결되어 생성되는 Connection의 특성이다.
마지막으로 채널 파이프라인을 설정하기 위해 EchoServerInitializer 객체를 할당한다. 서버 소켓에 연결이 들어오면 이 객체가 호출되어 소켓 채널을 초기화해준다.
try {
// Channel 생성후 기다림
ChannelFuture bindFuture = bootstrap.bind(new InetSocketAddress(SERVER_PORT)).sync();
Channel channel = bindFuture.channel();
allChannels.add(channel);
// Channel이 닫힐 때까지 대기
bindFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
close();
}
그 다음으로 bootstrap의 bind() 메소드로 서버 소켓에 포트를 바인딩한다. sync() 메소드를 호출해서 바인딩이 완료될 때까지 기다린다. 이 코드가 지나가면 서버가 시작된다.
ChannelInitializer
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LineBasedFrameDecoder(65536));
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StringEncoder());
pipeline.addLast(new EchoServerHandler());
}
이번 서버에서 구현한 ChannelInitializer의 핵심은 initChannel() 메소드다. 이 메소드의 역할은 채널 파이프라인을 만들어주는 것이다. TCP 연결이 accept 되었을 때 실행된다. 이전 Netty의 개요를 설명한 포스트에서 언급했던 파이프라인이 바로 이 메소드에서 만들어진다.
대충 메소드를 보면 알겠지만 Inbound와 Outbound가 섞여있다. 채널에 이벤트(메시지)가 발생하면 소켓 채널에서 읽어 들이는 것인지 소켓 채널로 쓰는 것인지에 따라서 파이프라인의 핸들러가 수행된다.
이 파이프라인에서는 LineBasedFrameDecoder()를 통해 네트워크에서 전송되는 바이트 값을 읽어 라인 문자열로 만들어주고, 필요하다면 디코딩을 한 다음 EchoServerHandler를 호출해준다. 이후 write()가 되면 StringEncoder()를 통해 네트워크 너머로 데이터를 전송하게 된다.
여기서는 new를 통해 매번 객체를 생성해서 파이프라인을 구축했지만 ReadOnly인 경우 등에는 하나의 객체를 여러 채널이 공유해서 쓰는 것도 가능하다. 한마디로 응용하기 나름
EcoServerHandler
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
String message = (String)msg;
Channel channel = ctx.channel();
channel.writeAndFlush("Response : '" + message + "' received\n");
if ("quit".equals(message)) {
ctx.close();
}
}
결국 클라이언트에서 메시지가 날라오면 실행되는 메소드다. 문자열을 전달받아서 채널에 "Response : " 문자열과 "' received\n" 문자열을 앞뒤에 붙여서 다시 전달해준다.
여기에 붙어서 문자열을 전송한 클라이언트는 다음과 같은 응답을 받게 된다.
quit 문자열을 입력하면 서버와의 연결이 종료된다.
댓글