IT

Netty 에코 서버-클라이언트 구현 (feat. 네트워크 소녀 Netty)

코딩하는 너구리 2021. 1. 7. 12:34
반응형

에코 서버 구현

 

Netty 를 이용하여 에코 서버를 만들어보자.

 

통상적으로 네트워크 프로그램을 배울 때 가장 처음 예제로 에코 서버를 사용하는데

 

그 이유는 프로그램의 구현이 간단할 뿐만 아니라 입출력 또는 송수신이라는 기본적인 동작 방식을 이해하는 데 유용하기 때문이다.

 

 

먼저 Server 쪽 코드를 작성해보자.

 

8888번 포트를 사용하여 클라이언트의 연결을 대기하고,

 

클라이언트 접속 요청에 의해 소켓 채널을 만들고, 소켓으로 데이터가 들어온다면 지정된 EchoServerHandler가 되돌려주는 간단한 예제이다.

 

 

// 에코 서버

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class EchoServer {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) {
                            ChannelPipeline p = ch.pipeline();
                            p.addLast(new EchoServerHandler());
                        }
                    });
            ChannelFuture f = b.bind(8888).sync();
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

 

 

다음은, 서버로 입력된 데이터를 클라이언트에게 에코시키기 위해 필요한 에코 서버 핸들러 코드이다.

 

channelRead 메서드에서 수신된 메시지를 write()하고,

write() 작업이 모두 완료된 후 channelReadComplete() 메서드를 호출하여 클라이언트로 메시지 데이터를 보내준다.

 

package com.netty.test;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.concurrent.EventExecutorGroup;

import java.nio.charset.Charset;

public class EchoServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String readMessage = ((ByteBuf) msg).toString(Charset.defaultCharset());
        System.out.println("수신한 문자열 [" + readMessage + "]");
        ctx.write(msg);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }
}

 

구현한 에코 서버 핸들러의 주요 기능은 다음과 같다.

 

  • 입력된 데이터를 처리하는 이벤트 핸들러인 ChannelInboundHandlerAdapter를 상속받게 지정한다.
  • channelRead는 데이터 수신 이벤트 처리 메서드다. 클라이언트로부터 데이터의 수신이 이루어졌을 때 네티가 자동으로 호출하는 이벤트 메서드다.
  • 수신된 데이터를 가지고 있는 네티의 바이트 버퍼(ByteBuf) 객체로부터 문자열 데이터를 읽어온다.
  • 수신된 문자열을 콘솔로 출력한다.
  • ctx는 ChannelHandlerContext 인터페이스의 객체로서 채널 파이프라인에 대한 이벤트를 처리한다.
    • 여기서는 서버로 연결된 클라이언트 채널로 입력받은 데이터를 그대로 전송하는 역할을 한다.
  • channelRead 이벤트의 처리가 완료된 후 자동으로 수행되는 이벤트 메서드인 channelReadComplete() 메서드를 이용해 채널 파이프라인에 저장된 버퍼를 전송하는 flush 메서드를 호출한다.

 

이렇게 에코 서버 구현을 완료하였습니다.

 

이제 에코 클라이언트도 구현해보도록 하겠습니다.

 

 

클라이언트 프로그램은 에코 서버 8888번 포트로 접속하여 "Hello Netty !"라는 문자열을 전송하고 서버의 응답을 수신하도록 해보자.

 

이어서 수신한 데이터를 화면에 출력하고 연결된 소켓을 종료한다.

클라이언트 코드도 서버와 마찬가지로 main 메서드가 포함된 EchoClient와 데이터 핸들러인 EchoClientHandler로 구성된다.

 

 

// 에코 클라이언트

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class EchoClient {
    public static void main(String[] args) throws Exception{
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();
                            p.addLast(new EchoClientHandler());
                        }
                    });
            ChannelFuture f = b.connect("localhost", 8888).sync();
            f.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

클라이언트의 main 코드는 서버의 구조와 거의 같다.

 

 

이벤트 루프, 부트스트랩, 채널 파이프라인, 핸들러 등을 초기화하는 부분은 서버 부분과 거의 동일하다.

  • 서버와 다르게 이벤트 루프 그룹하나만 설정했다.
    • 클라이언트 애플리케이션은 서버에 연결된 채널 하나만 존재하기 때문에 이벤트 루프 그룹이 하나다.
  • 클라이언트 애플리케이션이 생성하는 채널의 종류를 설정한다. 여기서는 NIO 소켓 채널인 NioSocketChannel을 설정했다.
  • 채널 파이프라인의 설정에 일반 소켓 채널 클래스인 SocketChannel을 설정한다.
  • 비동기 입출력 메서드인 connect()를 호출한다. connect 메서드는 메서드의 호출 결과로 ChannelFuture 객체를 돌려주는 이 객체를 통해서 비동기 메서드의 처리 결과를 확인할 수 있다.
    • ChannelFuture 객체의 sync 메서드는 ChannelFuture 객체의 요청이 완료될 때까지 대기한다. 단, 실패시에는 예외를 던진다.

 

다음으로 에코 클라이언트 데이터 핸들러를 구현해보자.

 

 

// 클라이언트 핸들러

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.concurrent.EventExecutorGroup;

import java.nio.charset.Charset;

public class EchoClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        String sendMessage = "Hello, Netty !";

        ByteBuf messageBuffer = Unpooled.buffer();
        messageBuffer.writeBytes(sendMessage.getBytes());

        StringBuilder builder = new StringBuilder();
        builder.append("전송한 문자열 [");
        builder.append(sendMessage);
        builder.append("]");

        System.out.println(builder.toString());
        ctx.writeAndFlush(messageBuffer);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String readMessage = ((ByteBuf)msg).toString(Charset.defaultCharset());

        StringBuilder builder = new StringBuilder();
        builder.append("수신한 문자열 [");
        builder.append(readMessage);
        builder.append("]");

        System.out.println(builder.toString());
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
        ctx.close();
    }
}

 

서버와 클라이언트 두 핸들러 모두 ChannelInboundHandlerAdapter를 상속하며 구현했는데,

 

클라이언트 핸들러에는 channelActive 를 추가했다. 주요 설명은 다음과 같다.

  • ChannelActive 이벤트는 소켓 채널이 활성화되었을 때 실행된다.
  • writeAndFlush 메서드는 내부적으로 데이터 기록과 전송의 두 가지 메서드를 호출한다.
  • 서버로부터 수신된 데이터가 있을 때 channelRead가 호출된다.
  • 수신된 데이터를 모두 읽었을 때 channelReadComplete가 호출된다.
  • 수신된 데이터를 모두 읽은 후 서버와 연결된 채널을 닫는다.

 

이로써 서버와 클라이언트 코드 작성을 완료했다.

// 에코 서버 실행 결과
수신한 문자열 Hello, netty !

// 에코 클라이언트 실행 결과
전송한 문자열 Hello, netty !
수신한 문자열 Hello, netty !

 

 

네티에서 데이터 송신을 아웃바운드 이벤트, 데이터 수신을 인바운드 이벤트로 정의하고 있다.

 

인바운드 이벤트와 아웃바운드 이벤트는 모두 프로그램을 기준으로 정의된다.

 

 

 

 

출처 : book.naver.com/bookdb/book_detail.nhn?bid=9608322

 

자바 네트워크 소녀 Netty

자바 네트워크 프로그래밍의 최고의 선택 NETTY!이 책은 안정성과 성능을 세계적으로 인정받아 카카오톡, 애플, 트위터, 페이스북, 네이버 라인 등에서 사용하는 자바 네트워크 프레임워크 네티

book.naver.com

 

반응형

'IT' 카테고리의 다른 글

Flutter DEBUG mark 지우기  (0) 2022.06.25
Flutter - const Constructor  (0) 2022.06.12
Netty 코덱  (0) 2021.01.05
Netty ChannelHandler와 ChannelPipeline  (0) 2021.01.03
Netty ByteBuf - 바이트 버퍼  (0) 2020.12.29