Skip to content

Latest commit

 

History

History
131 lines (98 loc) · 5.96 KB

netty server 예제.md

File metadata and controls

131 lines (98 loc) · 5.96 KB

클라이언트의 입력을 그대로 응답으로 돌려주는 서버를 Netty 프레임워크로 구현하고 이해해보자.

메인 클래스

class EchoServer {
    
    private val allChannels: ChannelGroup = DefaultChannelGroup("server", GlobalEventExecutor.INSTANCE)
    private var bossEventLoopGroup: EventLoopGroup? = null
    private var workerEventLoopGroup: EventLoopGroup? = null

    fun startServer() {

        // (1)
        bossEventLoopGroup = NioEventLoopGroup(1, DefaultThreadFactory("boss"))
        workerEventLoopGroup = NioEventLoopGroup(1, DefaultThreadFactory("worker"))

        // (2)
        val bootstrap = ServerBootstrap()
        bootstrap.group(bossEventLoopGroup, workerEventLoopGroup)

        // Channel 생성시 사용할 클래스 (NIO 소켓을 이용한 채널)
        bootstrap.channel(NioServerSocketChannel::class.java)

        // accept 되어 생성되는 TCP Channel 설정
        bootstrap.childOption(ChannelOption.TCP_NODELAY, true)
        bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true)

        // (3)
        // Client Request를 처리할 Handler 등록
        bootstrap.childHandler(EchoServerInitializer())

        // (4)
        try {
            // Channel 생성후 기다림
            val bindFuture = bootstrap.bind(InetSocketAddress(SERVER_PORT)).sync()
            val channel: Channel = bindFuture.channel()
            allChannels.add(channel)

            // Channel이 닫힐 때까지 대기
            bindFuture.channel().closeFuture().sync()
        } catch (e: InterruptedException) {
            throw RuntimeException(e)
        } finally {
            close()
        }
    }

    private fun close() {
        allChannels.close().awaitUninterruptibly()
        workerEventLoopGroup!!.shutdownGracefully().awaitUninterruptibly()
        bossEventLoopGroup!!.shutdownGracefully().awaitUninterruptibly()
    }

    companion object {
        private const val SERVER_PORT = 8080

        @Throws(Exception::class)
        @JvmStatic
        fun main(args: Array<String>) {
            EchoServer().startServer()
        }
    }
}

netty TCP 서버를 생성하기 위해서는 다음 과정을 수행해야한다.

  1. EventLoopGroup을 생성
  • NIO 기반의 EventLoop를 생성해서, 비동기처리할 수 있도록 헀다. bossEventLoopGroup은 서버 소켓을 listen하고, workerEventLoopGroup은 만들어진 Channel에서 넘어온 이벤트를 처리하는 역할을 할 것이다. 각각 1개의 쓰레드를 할당해줬다.
  1. ServerBootstrap을 생성하고 설정
  • netty 서버를 생성하기 위한 헬퍼 클래스인 ServerBootstrap 인스턴스를 만들어준다.
  • 우선 만들어둔 EventLoopGroup을 group() 메서드로 세팅해주고, 채널을 생성할 때 NIO 소켓을 이용한 채널을 생성하도록 channel() 메소드에 NioServerSocketChannel.class를 인자로 넘겨준다.
  • 그리고 TCP 설정을 childOption()으로 설정해준다. TCP_NODELAY, SO_KEEPALIVE 설정이 이 서버 소켓으로 연결되어 생성되는 connection에 적용될 것이다.
  1. ChannelInitializer 생성
  • 채널 파이프라인을 설정하기 위해 EchoServerInitializer 객체를 할당한다. 서버 소켓에 연결이 들어오면 이 객체가 호출되어 소켓 채널을 초기화해준다.
  1. 서버 시작
  • 마지막으로 bootstap의 bind() 메서드로 서버 소켓에 포트를 바인딩한다. sync() 메서드를 호출해서 바인딩이 완료될 때까지 기다리고, 서버가 시작된다.

서버의 채널 파이프라인을 정의하는 클래스

class EchoServerInitializer : ChannelInitializer<SocketChannel>() {

    @Throws(Exception::class)
    override fun initChannel(ch: SocketChannel) {
        val pipeline: ChannelPipeline = ch.pipeline()
        pipeline.addLast(LineBasedFrameDecoder(65536))
        pipeline.addLast(StringDecoder())
        pipeline.addLast(StringEncoder())
        pipeline.addLast(EchoServerHandler())
    }
}

initChannel() 메서드의 역할은 채널 파이프라인을 만들어주는 것이다. TCP 연결이 accept 되면 이 파이프라인을 따라 각 핸들러에 해당하는 동작이 수행된다.

inbound와 Outboud 핸들러가 섞여있는 것을 볼 수 있는데, 채널에 이벤트(메시지)가 발생하면 소켓 채널에서 읽어들이는 것인지 소켓 채널로 쓰는 것인지에 따라서 파이프라인의 핸들러가 수행된다.

이 파이프라인에서는 LineBasedFrameDecoder()를 통해 네트워크에서 전송되는 바이트 값을 읽어 라인 문자열로 만들어주고, 필요하다면 디코딩을 한 다음 EchoServerHandler를 호출해준다. 이후 write()가 되면 StringEncoder()를 통해 네트워크 너머로 데이터를 전송하게 된다.

채널이 생성될때마다 호출되는 메서드이기 때문에, 이 코드에서는 각 핸들러 메서드의 객체가 매번 새로 생성된다. 원한다면 핸들러 객체를 싱글톤으로 해서 공유하도록 할 수 있다. (물론 그렇게 하면 클래스를 무상태로 설계해야한다.)

마지막에 추가한 EchoServerHandler는 우리가 정의할 클래스이고, 나머지는 네티에 정의되어있는 코덱이다. (io.netty.handler.codec)

클라이언트로부터 메시지를 받았을때 처리할 클래스

class EchoServerHandler : ChannelInboundHandlerAdapter() {

    override fun channelRead(
        ctx: ChannelHandlerContext,
        msg: Any
    ) {
        val message = msg as String
        val channel = ctx.channel()
        channel.writeAndFlush("Response : '$message' received\n")
        if ("quit" == message) {
            ctx.close()
        }
    }
}

전달받은 msg를 가지고 원하는 값으로 변환해서 writeAndFlush() 해주면 클라이언트에게 그 데이터를 그대로 반환한다.