netty (3) 채팅 서버 만들어보자!

이번에는 배운걸 기준으로 채팅 서버를 만들어 볼 예정이다.
아주 간단하게 메시지를 보내는거와 귓속말을 할 수 있는 서비스를 만들어보자.

public class ChatNettyServer {

  private static final ChatNettyServiceHandler SHARED = new ChatNettyServiceHandler();

  public static void main(String[] args) {
    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>() {
          protected void initChannel(SocketChannel ch) throws Exception {
            ch.pipeline()
              .addLast(new StringDecoder(CharsetUtil.UTF_8), new StringEncoder(CharsetUtil.UTF_8))
              .addLast(new ChatNettyMessageCodec(), new LoggingHandler(LogLevel.INFO))
              .addLast(SHARED);
          }
        });

      Channel ch = b.bind(8080).sync().channel();
      ch.closeFuture().sync();

    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      workerGroup.shutdownGracefully();
      workerGroup.shutdownGracefully();
    }
  }
}

메인은 따로 설명할 필요는 없을 거 같다.

ChatNettyMessageCodecStringDecoder, StringEncoder 만 설정해줬다. 각자의 맞게 커스텀하게 코덱을 설정 할 수 있다.
한번 codec 클래스를 보자. 그럼 대충은 이해가 갈 듯하다.

public class ChatNettyMessageCodec extends MessageToMessageDecoder<String> {

  @Override
  protected void decode(ChannelHandlerContext ctx, String msg, List<Object> out) throws Exception {
    String command = msg.substring(0, 2);
    String message = msg.substring(2, msg.length() - 1) + "\n";
    Herder herder = new Herder();
    herder.setCommand(command);
    out.add(new Message(herder, message));
  }
}

MessageToMessageDecoder를 상속 받았다. 클래스 시그네처는 다음과 같다.
MessageToMessageDecoder<I>
인바운드를 설정 해 줄 수 있다. 인바운드를 String으로 설정 한 후에 필자가 만든 Message로 넣어뒀다.
Message는 자바빈처럼 만들었다.

@Data
@AllArgsConstructor
public class Message {
  private Herder herder;
  private String text;

}

@Data
public class Herder {
  private String command;
}

일반적인 자바 빈이다.
다음으로 이벤트 핸들러 이다.

@ChannelHandler.Sharable
public class ChatNettyServiceHandler extends SimpleChannelInboundHandler<Message> {

  private final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
  final AttributeKey<Integer> id = AttributeKey.newInstance("id");
  private static final AtomicInteger count = new AtomicInteger(0);

  @Override
  public void channelActive(ChannelHandlerContext ctx) throws Exception {
    int value = count.incrementAndGet();
    ctx.channel().attr(id).set(value);
    ctx.writeAndFlush("your id : "+ String.valueOf(value) + "\n");
    channels.writeAndFlush(String.valueOf(value) + " join \n");
    channels.add(ctx.channel());
  }

  @Override
  public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    ctx.channel().attr(id).remove();
    channels.remove(ctx.channel());
  }

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

  protected void channelRead0(ChannelHandlerContext ctx, Message msg) throws Exception {

    if ("10".equals(msg.getHerder().getCommand())) {
      channels.writeAndFlush(ctx.channel().attr(id).get() + " :" + msg.getText());;
    } else if ("20".equals(msg.getHerder().getCommand())) {

      String text = msg.getText();
      String substring = text.substring(0, 2).trim();
      String message = text.substring(2, text.length());

      channels.stream().filter(i -> i.attr(id).get() == Integer.parseInt(substring))
        .forEach(i -> i.writeAndFlush(i.attr(id).get() + " : " +message));

      ctx.writeAndFlush(ctx.channel().attr(id).get() + " : " + message);

    } else if("30".equals(msg.getHerder().getCommand())){
      ctx.disconnect();
    }
  }

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

여기서 중요한건 SimpleChannelInboundHandler 상속받았다.
ChannelInboundHandlerAdapter 클래스를 상속 받아도 상관없다.
SimpleChannelInboundHandler 또한 ChannelInboundHandlerAdapter를 상속받은 아이다.

public void channelActive(ChannelHandlerContext ctx) throws Exception

이때는 접속 정보를 넣어 뒀다. 그리고 전체 사용자에게 사용자가 입장했다고 전송해줬다.

public void channelInactive(ChannelHandlerContext ctx) throws Exception 

이때에는 클라이언트가 종료를 한 후라 접속정보를 삭제해줬다.

protected void channelRead0(ChannelHandlerContext ctx, Message msg) throws Exception

원래 ChannelInboundHandlerAdapter에선 Object 이지만 SimpleChannelInboundHandler은 타입을 지정해 줄수 있다.
그래서 이걸로 선택했다.
커멘드가 10 일때에는 전체 채팅 20 일때는 귓속말 30일때는 종료 한다.
얼추 소스를 보면 이해가 갈듯 싶다.

telnet으로 접속을 해보자

telnet localhost 8080

Trying ::1...
Connected to localhost.
Escape character is '^]'.
your id : 1

위와 같이 your id : 1이 출력 될 것이다.
창을 한개 더 띄어 telnet을 접속하자
그럼 다음에

10 hello

라는 커멘드를 써보자.
그럼 양쪽에 1 : hello 다음같이 출력 될 것이다.
다음은 귓속말을 해보자
1번 사용자에서

202 hello2

입력하면 2번 사용자에게만 전달 될 것이다.
20은 커멘드 2는 id(두자리라 스페이스) 그런다음에 채팅 문자로 입력 하면 된다.
마지막으로 30을 치고 나가자!

30
Connection closed by foreign host.

다음과 같이 출력된다면 성공적으로 되었다.
얼추 netty의 채팅에 대해서 알아봤다.