Apache MINA로 간단한 HTTP 서버 만들기

나는 주로 Java로 시스템 프로그래밍을 한다. 시스템 프로그래밍은 대부분 외부 시스템과의 통신을 필요로 하고 따라서 네트워크 프로그래밍을 많이 한다. 

C에서 socket 프로그래밍을 하다가 Java로 넘어온 나로서, 오리지널 Java의 스트림 기반의 네트워크 프로그래밍은 매우 생경했다. 그런데 Java 2에서 NIO(Non-blocking I/O)가 소개되면서 그나마 좀 편해졌다.

NIO는 socket 프로그래밍을 현대적으로 계승한 것 같은 API 세트를 가지고 있기 때문이다.

그런데 Apache MINA라고 하는 네트워크 프레임웍을 우연히 알게 되었는데, 너무도 매력적이었다.  마치 OSI 7레이어를 모델링한 것처럼 필터들을 쌓아서 네트워크 어플리케이션을 조립식(?)으로 만들 수 있었기 때문이다.

By Apache MINA
특히 더 놀란 점은 이 MINA의 창시자가 이희승이라는 한국인이라는 것이었다. 아마도 한국인이 주도로 만든 오픈소스 프로젝트가 Apache 재단의 주요 프로젝트로 승격된 것은 최초가 아닐까 싶다.

그런데 이희승씨는 MINA를 Apache에 공개한 뒤에 홀연히 거기를 떠나 Netty라는 좀 더 개선된 네트워크 프레임웍을 만들고 있다. 나도 따라 Netty로 갈까 했지만, 이미 상당히 많은 수의 어플리케이션들이 MINA로 만들어져 있었고, 특히나 시리얼통신을 지원하는 프레임웍은 MINA가 유일했기 때문에 계속 MINA에 남아 있었다.

물론 Netty도 MINA처럼 RXTX를 포팅할 수도 있겠지만, 무엇보다 귀찮았다. 시리얼 통신에 목숨 걸 이유도 없었고...

Apache MINA가 요즘 소강상태인 것 같다. 내가 처음 MINA를 쓴게 아마도 5년도 넘은 1.7 버전일 때였던 것 같은데, 이제 겨우 2.0.9 버전이다. 2.0 버전까지는 활발하게 버전업이 되더니 그 이후로는 매우 뜸하다.  특히 AsyncWeb 같은 서브-프로젝트는 담당자를 찾는다는 공고까지 붙어있는 정도니...

서설이 길었는데... 이 글을 쓰게 된 이유는 Apache MINA로 간단한 HTTP서버를 만들 일이 있었는데, 소스 코드를 보니 코드는 다 준비되어 있는데, 제대로 된 예제나 설명이 없었기 때문이다. 그래서 Apache MINA에 준비된 그러나 아직 제대로 소개되지 않은 HTTP 관련된 코드들의 사용법을 간단하게 예제 중심으로 정리하고자 한다.

이 예제는 MINA 2.0.9 버전을 사용하여 만들어졌다.

HTTP 서버 클래스

MINA로 서버 어플리케이션을 만드는 것은 매우 간단하다. 어떤 종류의 Acceptor를 만들고, 거기에 필터들을 차곡차곡 쌓은 다음 bind하면 끝이다. HTTP 서버로서의 기능을 위한 필터는 이미 MINA가 HttpServerCodec이라는 이름으로 제공하고 있다.

이 HTTP에 관련된 클래스들은 mina-core-2.0.X.jar에 없고 mina-http-2.0.X.jar로 따로 분리되어 있으므로, 사용을 위해서는 이 jar도 포함시켜야 한다.

public class MinaHttpServer {
    public static void main(String[] args) throws IOException {
        NioSocketAcceptor acceptor = new NioSocketAcceptor();
        acceptor.getFilterChain().addLast("logger", new LoggingFilter());  // 이것도 생략 가능 
        acceptor.getFilterChain().addLast("httpCodec", new HttpServerCodec());  // MINA에서 제공하는 HTTP Server Codec 
        acceptor.setHandler(  new MinaHttpHandler() );  // 이 핸들러를 정의해야 함 
        
        //acceptor.getSessionConfig().setReuseAddress(true);
        acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10);  // 10초동안 idle이면 접속 끊기
        acceptor.setResuseAddress(true);
        acceptor.bind(new InetSocketAddress(8123));  // 8123 포트 사용 
    }
}

HTTP 핸들러 클래스

앞의 소스코드에서 MinaHttpHandler라는 클래스를 사용했는데, 이를 정의해야 한다. 이 클래스는 HttpServerCodec에 의해 파싱된 오브젝트를 사용하므로 복잡한 코드는 필요없다. 마치 Servlet 코딩하는 것과 유사하다. 하지만 비동기 방식이라는 점을 주의해야 한다.

public class MinaHttpHandler extends IoHandlerAdapter {
    @Override
    public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
        cause.printStackTrace();  // 예외처리 
    }

    @Override
    public void messageReceived(IoSession session, Object message) throws Exception {
        if (message instanceof HttpRequest) {  // 헤더 수신이 다 되었을때
            System.out.println("Receive HttpRequest ==>");
            HttpRequest req = (HttpRequest)message;
            System.out.println("HttpRequest = " + req);
            System.out.println("path=" + req.getRequestPath());
        } else if (message instanceof IoBuffer) {   // Body 수신 
            System.out.println("Receive IoBuffer ==>");
            IoBuffer buf = (IoBuffer)message;
            System.out.println("BODY = " + buf.getHexDump());
        } else if (message instanceof HttpEndOfContent) {  // 요청 패킷 완료 
            System.out.println("Receive HttpEndOfContent ==>");
            InetSocketAddress addr = (InetSocketAddress)session.getRemoteAddress();
            System.out.println("FROM " + addr.getAddress().getHostAddress() + ":" + addr.getPort());
            
            // 예제에서는 고정 응답이지만, 실제는 다르겠지?
            this.writeResponse(session, "<html><body><h1>Hello, MINA~</h1></body></html>");
        }
    }

    @Override
    public void sessionIdle(IoSession session, IdleStatus status) throws Exception {        
        // 일정 시간 통신이 없으면 연결 끊음
        System.out.println("IDLE " + session.getIdleCount(status));  
        session.close(true);
    }
    
    void writeResponse(IoSession session, String respText) throws Exception {
        IoBuffer body = IoBuffer.wrap(respText.getBytes("utf-8"));
        
        Map headers = new HashMap();
        headers.put("Server", "Apache MINA Sample HTTP Server/1.0");
        headers.put("Content-Type", "text/html; charset=utf-8");
        headers.put("Pragma", "no-cache");
        headers.put("Date", DateUtil.getCurrentAsString());  // Date:Thu, 22 Oct 2015 20:21:13 GMT 형식 
        headers.put("Content-Length", String.valueOf(body.remaining()));  // Content-Length는 써줘야. 
        
        HttpResponse resp = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SUCCESS_OK, headers);
        session.write(resp);
        session.write(body);
        session.write(new HttpEndOfContent());
    }
}

어떤 요청이 와도 동일한 응답을 하는 간단한 HTTP서버라 코드도 단순하다. 하지만 이런 간단한 코드로 HTTP서버를 만들 수 있다는 건 굉장히 매력적이다. 그것도 비동기적으로...

코드의 핵심은 messageReceived() 함수의 구현에 있다. 보면 세 단계로 이 함수가 불림을 알 수 있다. 하나는 HttpRequest가 날아오는 경우인데 이때는 헤더를 모두 수신하여 파싱한 상태이다.  이 HttpRequest는 요청한 URL, Method, Query String, 파라미터, 헤더 등의 정보를 얻을 수 있어 유용하다.

다음으로 IoBuffer가 날아오는데 이것은 Body를 의미한다. 이 Body는 한번이 아니라 여러번 날아올 수 있음에 유의해야 한다.

그리고 마지막으로 HttpEndOfContent가 날아오는데, 이건 단지 요청이 끝났음을 나타내는 빈 클래스이다. 그래서 여기서 최종적으로 어떤 일을 할지를 정해서 응답을 session.write를 이용하여 보내면 된다.  응답은 writeResponse() 에 구현된 것처럼 HttpResponse 구조를 만들어 보내고, Body를 IoBuffer로 만들어 보내고, 마지막으로 HttpEndOfContent를 보내면 된다.

여기서 중요한 것은 이 모든 호출이 비동기라는 점이다. 그러므로 messageReceived로 들어왔던 HttpRequest 객체는 session.setAttribute를 이용하여 저장해 두어야 한다.  비슷하게 IoBuffer로 들어오는 Body도 session에 차곡차곡 더하면서 쌓아두어야 한다.

그래서 마지막 HttpEndOfContent가 전달되었을 때, 세션에서 이들을 꺼내어 약속된 처리를 하면 된다.

이런 사항들을 굳이 코드로 보이진 않았는데... 상상의 여지를 남겨두기 위해서이다.

여하튼 Apache MINA의 숨겨진 이 클래스들 덕에 최근 조그만 프로젝트 하나를 깔끔하게 끝낼 수 있었다.  고맙다. MINA

댓글 없음:

댓글 쓰기

인기글