Echo 서버 만들기
모든 네티 서버에는 다음 항목이 필요하다.
- 하나 이상의 ChannelHandler : 이 컴포넌트는 클라이언트로부터 받은 데이터를 서버측에서 처리하는 비즈니스 논리를 구현한다.
- 부트스트랩 : 서버를 구성하는 시동 코드를 의미한다. 최소한 서버가 연결 요청을 수신하는 포트 서버와 바인딩하는 코드가 있어야 한다.
ChannelHandler와 비즈니스 논리
Echo 서버는 들어오는 메시지에 반응해야 하므로 인바운드 이벤트에 반응하는 메서드가 정의된 ChannelInboundHandler 인터페이스를 구현해야 한다.
관심을 가질 메서드는 다음과 같다.
- channelRead() : 메시지가 들어올 때마다 호출된다.
- channelReadComplete() : channelRead()의 마지막 호출에서 현재 일괄 처리의 마지막 메시지를 처리했음을 핸들러에 통보한다.
- exceptionCaught() : 읽기 작업 중 예외가 발생하면 호출된다.
EchoServerHandler
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
@ChannelHandler.Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf)msg;
System.out.println("Server received : " + in.toString(CharsetUtil.UTF_8));
ctx.write(in); // 받은 메시지를 발신자에게로 Echo 시킨다.
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("Bye");
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER) // 대기중인 메시지를 플러시하고 채널을 닫음
.addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close(); // 채널 닫기
}
}
ServerHandler 코드를 작성하면서 기억해둘 점은 다음과 같다.
- ChannelHandler는 네 가지 이벤트 유형에 의해 호출된다.
- 애플리케이션은 ChannelHandler를 구현하거나 확장해 이벤트 수명주기를 후크하고 커스텀 애플리케이션 논리를 제공한다.
서버 부트스트랩
지금까지 EchoServerHandler가 구현하는 핵심 비즈니스 논리를 살펴봤다. 이제 다음 과정을 포함하는 서버의 부트스트랩을 진행해야 한다.
- 서버가 수신할 포트를 바인딩하고 들어오는 연결 요청을 수락한다.
- EchoServerHandler 인스턴스에 인바운드 메시지에 대해 알리도록 Channel을 구성한다.
EchoServer 클래스
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.net.InetSocketAddress;
public class EchoServer {
private final int port;
public EchoServer() {
this.port = 8888;
}
public static void main(String[] args) throws Exception {
new EchoServer().start(); // 서버의 start() 메소드 호출
}
private void start() throws Exception {
final EchoServerHandler serverHandler = new EchoServerHandler();
EventLoopGroup group = new NioEventLoopGroup(); // EventLoopGroup 생성
try {
ServerBootstrap b = new ServerBootstrap(); // ServerBootstrap 생성
b.group(group)
.channel(NioServerSocketChannel.class) // NIO 전송채널을 이용하도록 지정
.localAddress(new InetSocketAddress(port)) // 지정된 포트로 소켓 주소 설정
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception { // EchoServerHandler 하나를 채널의 Channel Pipeline 으로 추가
ch.pipeline().addLast(serverHandler);
}
});
ChannelFuture f = b.bind().sync(); // 서버를 비동기식으로 바인딩
f.channel().closeFuture().sync(); // 채널의 CloseFuture를 얻고 완료될 때까지 현재 스레드를 블로킹
} finally {
group.shutdownGracefully().sync(); // EventLoopGroup을 종료하고 모든 리소스 해제
}
}
}
지금 완성한 서버 구현에서 중요한 단계를 검토해보자.
- EchoServerHandler는 비즈니스 논리를 구현한다.
- main() 메서드는 서버를 부트스트랩한다.
부트스트랩에는 다음 단계가 필요하다.
- 서버를 부트스트랩하고 바인딩하는 데 이용할 ServerBootstrap 인스턴스를 생성한다.
- 새로운 연결 수락 및 데이터 읽기/쓰기와 같은 이벤트 처리를 수행할 NioEventLoopGroup 인스턴스를 생성하고 할당한다.
- 서버가 바인딩하는 로컬 InetSocketAddress를 지정한다.
- EchoServerHandler 인스턴스를 이용해 새로운 각 Channel을 초기화한다.
- ServerBootstrap.bind()를 호출해 서버를 바인딩한다.
Echo 클라이언트 만들기
Echo 클라이언트는 다음과 같은 일을 한다.
- 서버로 연결한다.
- 메시지를 하나 이상 전송한다.
- 메시지마다 대기하고 서버로부터 동일한 메시지를 수신한다.
- 연결을 닫는다.
ChannelHandler를 이용한 클라이언트 논리 구현
- channelActive() : 서버에 대한 연결이 만들어지면 호출된다.
- chanelRead0() : 서버로부터 메시지를 수신하면 호출된다.
- exceptionCaught() : 처리 중에 예외가 발생하면 호출된다.
이제 클라이언트의 ChannelHandler를 만들어 보자.
EchoClientHandler
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty Connect()", CharsetUtil.UTF_8)); // 채널 활성화 시 메시지 전송
}
@Override
public void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
System.out.println("Client receive : " + msg.toString(CharsetUtil.UTF_8)); // 수신한 메시지 로깅
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace(); // 예외 시 오류를 로깅하고 채널 닫기
ctx.close();
}
}
우선 연결이 만들어지면 호출되는 ChannelActive()를 재정의했다.
이 예제에서는 "Netty Connet()"라는 문자열을 인코딩한 바이트 버퍼를 전송한다.
그런 다음, 데이터를 수신할 때 호출되는 channelRead0() 메서드를 재정의했다.
주의할 점은 서버가 전송한 메시지가 여러 청크로 수신될 수 있다는 점이다. 즉, 서버가 5바이트를 전송할 때 5바이트가 모두 한 번에 수신된다는 보장이 없다. 다만 TCP는 스트림 기반 프로토콜이므로 서버가 보낸 순서대로 바이트를 수신할 수 있게 보장한다.
EchoClient
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.net.InetSocketAddress;
public class EchoClient {
public EchoClient(){
}
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap(); // bootstrap 생성
b.group(group) // 클라이언트 이벤트 처리할 EventLoopGroup을 지정.
.channel(NioSocketChannel.class) // 채널 유형 NIO 지정
.remoteAddress(new InetSocketAddress(8888)) // 서버의 InetSocketAddress를 설정
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception { // 채널이 생성될 때 파이프라인에 EchoClientHandler 하나를 추가
ch.pipeline().addLast(new EchoClientHandler());
}
});
ChannelFuture f = b.connect().sync(); // 원격 피어로 연결하고 연결이 완료되기를 기다림
f.channel().closeFuture().sync(); // 채널이 닫힐 때까지 블로킹함.
} finally {
group.shutdownGracefully().sync(); // 스레드 풀을 종료하고 모든 리소스를 해제함
}
}
public static void main(String[] args) throws Exception {
new EchoClient().start();
}
}
Client 과정 정리
클라이언트를 초기화하기 위한 Bootstrap 인스턴스를 생성한다.
새로운 연결을 생성하고 인바운드와 아웃바운드 데이터를 처리하는 것을 포함하는 이벤트 처리를 제어할 NioEventLoopGroup 인스턴스를 만들고 할당한다.
서버로 연결하기 위한 InetSocketAddress를 생성한다.
연결이 만들어지면 파이프라인에 EchoClientHandler 하나를 추가한다.
모든 준비가 완료되면 Bootstrap.connect()를 호출해 원격 서버로 연결한다.
빌드 및 실행
빌드 및 실행을 위해서는 Netty를 먼저 다운받아야 한다. 다운 받은 뒤, 프로젝트에 외부 라이브러리에 Netty를 추가해주면 된다.
이후 소스를 작성하고 Server와 Client를 각각 실행시켜 보자.
IntelliJ 환경에서 실행된 모습
EchoServer
EchoClient
이상으로 JAVA Netty 에코 서버-클라이언트 구현을 마치겠습니다.
정리
@Sharable
: 이 어노테이션이 붙은 클래스의 인스턴스는 여러 Channel에서 공유할 수 있음을 나타낸다.
SimpleChannelInboundHandler와 ChannelInboundHandler 비교
Client에서는 SimpleChannelInboundHandler를 사용했다.
클라이언트에서 channelRead0()이 완료된 시점에서 들어오는 메시지는 이미 확보된 상태이며 메서드가 반환될 때 ByteBuf에 대한 메모리 참조를 해제한다.
반면 서버에서는 ChannelRead()가 반환될 때까지 비동기의 write() 작업이 완료되지 않았을 수 있다. 이 메시지는 channelReadComplete()에서 writeAndFlush()가 호출될 때 해제된다.
이처럼 서로 메모리 해제하는 구간이 다르고, 메서드의 약간의 차이가 있으므로
Client에서는 SimpleChannelInboundHandler를, Server에서는 ChannelInboundHandler를 사용하는 것으로 기억해두고 넘어가자.
참고 : https://book.naver.com/bookdb/book_detail.nhn?bid=10462610
'IT' 카테고리의 다른 글
Netty 전송 API (0) | 2020.12.28 |
---|---|
Netty 컴포넌트와 설계 (2) | 2020.12.10 |
Netty의 개념과 아키텍처 (0) | 2020.12.09 |
[ERROR] JSON 순환 참조 문제 해결하기 (0) | 2020.09.19 |
[ERROR] Error running '{ClassName}': Command line is too long. Shorten command line for {ClassName} IntelliJ 실행 에러 (0) | 2020.09.13 |