아파치 톰캣 서블릿/JSP 컨테이너

아파치 톰캣 7

Version 7.0.28-dev, Oct 2 2013
Apache Logo

Links

User Guide

참고

아파치 톰캣 개발

고급 IO와 톰캣

Table of Contents
Introduction

APR 또는 NIO API를 커넥터의 기반으로 사용할 때, 톰캣은 서블릿 API를 지원하는데 제공되는 일반적인 블로킹 IO 위에서 수많은 확장을 제공할 수 있습니다.

중요 노트: 이 기능을 이용하려면 APR 또는 NIO HTTP커넥터를 사용해야 합니다. 클래식 java.io HTTP 커넥터와 AJP 커넥터는 지원되지 않습니다.

Comet support

Comet 지원은 서블릿에서 비동기적으로 IO 처리를 가능하게 하고, 연결된 상태에서 데이터를 읽기가 가능한 경우에 (블로킹 읽기를 항상 사용하는 대신에)이벤트를 받고, 비동기적으로 연결 상에서 데이터를 써서 보낼 수 있게 합니다(대부분 어떤 다른 소스에서 발생한 어떤 이벤트에 응답하는 형태).

CometEvent

org.apache.catalina.comet.CometProcessor 인터페이스를 구현한 서블릿은 일반적인 서비스 메소드가 아닌, 발생한 이벤트에 따라 해당되는 이벤트 메소드를 호출하게 됩니다. 이벤트 객체는 일반적인 요청과 응답 객체에 접근할 수 있어서, 일반적인 방법으로도 사용할 수 있게 합니다. 중요한 차이점은 이 객체들은 END 또는 ERROR 이벤트 처리를 할 때까지 BEGIN 이벤트 처리 사이에서 유효하고 모든 기능을 다 사용할 수 있는 상태로 남아있게 됩니다. 다음은 이벤트 타입입니다:

  • EventType.BEGIN: 연결 처리의 시작에 호출됩니다. 요청과 응답 객체를 사용하는 연관된 필드를 초기화하는데 사용됩니다. 이 이벤트의 처리 마지막과 종료 또는 에러 이벤트 처리의 시작 사이에서, 열려있는 연결에 응답 객체를 사용해서 데이터를 보내는 것이 가능합니다. 응답 객체와 의존적인 OutputStream과 Writer는 여전히 동기화되지 않기 때문에, 멀티 쓰레드에서 접근될 때는 동기화는 절대적입니다. 초기 이벤트를 처리한 뒤, 요청은 커밋된 것으로 간주됩니다.
  • EventType.READ: 입력 데이터가 사용 가능하고, 블로킹 없이 읽을 수 있는 상태임을 지시합니다. InputStream 또는 Reader의 사용 가능하고 준비된 함수들을 블로킹의 위험이 있는지 결정하는데 사용됩니다: 서블릿은 데이터가 사용가능하다고 알려져 있는 동안 읽을 수 있습니다. 읽기 에러가 발생하면, 서블릿은 적절하게 예외를 전파함으로 리포트해야 합니다. 예외를 던지면 에러 이벤트가 발생되고, 연결은 닫히게 됩니다. 대안적으로, 어떤 예외라도 잡아서, 서블릿이 사용하고, 이벤트 종료 메소드를 사용해서, 데이터 구조를 클린 업하는 것도 가능합니다. 이 메소드 실행의 외부에서 요청 객체로부터 데이터 읽기를 시도하는 것은 허락되지 않았습니다.
    윈도우 같은 어떤 종류의 플랫폼에서는, 클라이언트 단절이 READ 이벤트에 의해서 지정되기도 합니다. 스트림을 읽을 때 IOException 또는 EOFException이 발생한다면 -1이 결과로 나옵니다. 이 모든 세 가지 경우를 적절하게 다뤄야 합니다. IOException을 잡지 않는다면, 톰캣은 대신해서 에러를 잡아주고, 발생 시간에 에러를 알려주면서 즉시 ERROR로 이벤트 체인을 호출합니다.
  • EventType.END: 요청 처리의 마지막에 End가 호출됩니다. 시작 메소드에서 초기화된 필드들이 리셋됩니다. 이 이벤트가 처리된 후에, 요청과 응답 객체와 모든 관련 객체들이 재활용되고 다른 요청을 처리하는데 사용될 것입니다. 데이터가 사용 가능하고, 요청 입력의 파일끝에 도달하게 되면 호출될 것입니다. (이것은 보통 클라이언트가 요청을 파이프라인으로 했을 경우에 나타납니다.)
  • EventType.ERROR: Error는 IO 예외나 비슷한 복구 불가능한 에러가 연결에서 발생한 경우에 컨테이너에 의해서 호출됩니다. 시작 메소드에서 초기화된 필드들이 리셋됩니다. 이 이벤트가 처리된 후에, 요청과 응답 객체와 모든 관련 객체들이 재활용되고 다른 요청을 처리하는데 사용될 것입니다. 데이터가 사용 가능하고, 요청 입력의 파일끝에 도달하게 되면 호출될 것입니다.

이벤트 처리를 미세하게 할 수 있는 약간의 이벤트 서브타입이 있습니다. (노트: 어떤 이벤트는 org.apache.catalina.valves.CometConnectionManagerValve 밸브를 사용해야 합니다.):

  • EventSubType.TIMEOUT: 연결 시간 초과 (ERROR의 서브 타입); 이 ERROR 타입은 치명적이 아니며, 서블릿이 이벤트의 close 메소드를 사용하지 않으면 종료되지 않습니다.
  • EventSubType.CLIENT_DISCONNECT: 클라이언트 연결이 닫혔습니다. (ERROR의 서브 타입)
  • EventSubType.IOEXCEPTION: IO 예외가 발생했습니다. 예를 들어 유효하지 않은 청크 블록같은 콘텐트 (ERROR의 서브 타입).
  • EventSubType.WEBAPP_RELOAD: 웹 애플리케이션이 리로드되었습니다 (ERROR의 서브 타입).
  • EventSubType.SESSION_END: 서블릿이 세션을 종료했습니다 (ERROR의 서브 타입).

위 설명처럼, 전형적인 코멧 요청의 생명주기는 다음과 같은 이벤트의 연속으로 이루어졌습니다: BEGIN -> READ -> READ -> READ -> ERROR/TIMEOUT. 어느 때이든, 서블릿은 이벤트 객체의 종료 메소드를 사용해서 요청 처리를 종료할 수 있습니다.

CometFilter

일반적인 필터와 마찬가지로, 필터 체인은 코멧 이벤트가 처리될 때 호출됩니다. 이 필터는 CometFilter 인터페이스(보통의 Filter 인터페이스처럼 동일한 방법으로 동작합니다)를 구현해야 하고, 일반적인 필터처럼 배치 설명자에 선언되고 매핑되어야 합니다. 이벤트를 처리할 때 필터 체인은 모든 일반적인 매핑 룰에 매칭하는 필터만을 포함하고, CometFilter 인터페이스를 구현해야 합니다.

Example code

아래의 유사 코드 서블릿은 앞서 설명한 API를 사용해서 비동기 채팅 기능을 구현했습니다:

public class ChatServlet
    extends HttpServlet implements CometProcessor {

    protected ArrayList<HttpServletResponse> connections =
        new ArrayList<HttpServletResponse>();
    protected MessageSender messageSender = null;

    public void init() throws ServletException {
        messageSender = new MessageSender();
        Thread messageSenderThread =
            new Thread(messageSender, "MessageSender[" + getServletContext().getContextPath() + "]");
        messageSenderThread.setDaemon(true);
        messageSenderThread.start();
    }

    public void destroy() {
        connections.clear();
        messageSender.stop();
        messageSender = null;
    }

    /**
     * Process the given Comet event.
     *
     * @param event The Comet event that will be processed
     * @throws IOException
     * @throws ServletException
     */
    public void event(CometEvent event)
        throws IOException, ServletException {
        HttpServletRequest request = event.getHttpServletRequest();
        HttpServletResponse response = event.getHttpServletResponse();
        if (event.getEventType() == CometEvent.EventType.BEGIN) {
            log("Begin for session: " + request.getSession(true).getId());
            PrintWriter writer = response.getWriter();
            writer.println("<!doctype html public \"-//w3c//dtd html 4.0 transitional//en\">");
            writer.println("<head><title>JSP Chat</title></head><body bgcolor=\"#FFFFFF\">");
            writer.flush();
            synchronized(connections) {
                connections.add(response);
            }
        } else if (event.getEventType() == CometEvent.EventType.ERROR) {
            log("Error for session: " + request.getSession(true).getId());
            synchronized(connections) {
                connections.remove(response);
            }
            event.close();
        } else if (event.getEventType() == CometEvent.EventType.END) {
            log("End for session: " + request.getSession(true).getId());
            synchronized(connections) {
                connections.remove(response);
            }
            PrintWriter writer = response.getWriter();
            writer.println("</body></html>");
            event.close();
        } else if (event.getEventType() == CometEvent.EventType.READ) {
            InputStream is = request.getInputStream();
            byte[] buf = new byte[512];
            do {
                int n = is.read(buf); //can throw an IOException
                if (n > 0) {
                    log("Read " + n + " bytes: " + new String(buf, 0, n)
                            + " for session: " + request.getSession(true).getId());
                } else if (n < 0) {
                    error(event, request, response);
                    return;
                }
            } while (is.available() > 0);
        }
    }

    public class MessageSender implements Runnable {

        protected boolean running = true;
        protected ArrayList<String> messages = new ArrayList<String>();

        public MessageSender() {
        }

        public void stop() {
            running = false;
        }

        /**
         * Add message for sending.
         */
        public void send(String user, String message) {
            synchronized (messages) {
                messages.add("[" + user + "]: " + message);
                messages.notify();
            }
        }

        public void run() {

            while (running) {

                if (messages.size() == 0) {
                    try {
                        synchronized (messages) {
                            messages.wait();
                        }
                    } catch (InterruptedException e) {
                        // Ignore
                    }
                }

                synchronized (connections) {
                    String[] pendingMessages = null;
                    synchronized (messages) {
                        pendingMessages = messages.toArray(new String[0]);
                        messages.clear();
                    }
                    // Send any pending message on all the open connections
                    for (int i = 0; i < connections.size(); i++) {
                        try {
                            PrintWriter writer = connections.get(i).getWriter();
                            for (int j = 0; j < pendingMessages.length; j++) {
                                writer.println(pendingMessages[j] + "<br>");
                            }
                            writer.flush();
                        } catch (IOException e) {
                            log("IOExeption sending message", e);
                        }
                    }
                }

            }

        }

    }

}
  
Comet timeouts

만약 NIO 커넥터를 사용한다면, 다른 코멧 연결을 위한 개별적인 타임아웃 시간을 설정할 수 있습니다. 타임아웃을 설정하려면, 간단히 다음 코드처럼 요청 속성을 설정하면 됩니다:

CometEvent event.... event.setTimeout(30*1000);
또는
event.getHttpServletRequest().setAttribute("org.apache.tomcat.comet.timeout", new Integer(30 * 1000));
이렇게 하면 타임아웃이 30초로 설정됩니다. 중요 노트: 이렇게 타임아웃을 설정하려면, BEGIN 이벤트에서 해야 됩니다. 기본 값은 soTimeout입니다.

만약 APR 커넥터를 사용한다면, 모든 코멧 연결은 동일한 타임아웃 값을 갖게 됩니다. soTimeout*50 입니다.

Asynchronous writes

APR 또는 NIO가 가능하면, 톰캣은 큰 용량의 정적인 파일을 보내기 위해서 sendfile 사용을 지원합니다. 시스템 부하가 증가하자마자, 가장 효율적인 방법으로 비동기적으로 실행되어 보내집니다. 블로킹 쓰기를 통해서 대용량의 응답을 보내는 대신, 정적인 파일로 콘텐트를 쓰는 것이 가능하고, sendfile 코드를 이용해서 쓸 수 있습니다. 캐싱하는 값은 메모리에 저장하는 대신 응답 데이터를 파일에 캐싱하는 장점이 있습니다. Sendfile 지원은 요청 속성 org.apache.tomcat.sendfile.support 값이 Boolean.TRUE로 설정되어야 사용할 수 있습니다.

적당한 요청 속성을 설정함으로 톰캣이 sendfile 호출을 실행하게 어떤 서블릿에서도 가능합니다. 응답에 정확한 콘텐츠 길이를 설정하는 것도 역시 필요합니다. sendfile을 사용할 때, 응답 내용이 커넥터에 의해 보내지기 때문에, 필터링 될 수 없습니다. (When using sendfile, it is best to ensure that neither the request or response have been wrapped, since as the response body will be sent later by the connector itself, it cannot be filtered.) 3가지 필요한 요청 속성을 설정하기 보다는, 서블릿은 어떤 응답 데이터도 보내면 안됩니다. 그러나 응답 헤더를 수정하는 결과를 사용하는 어떤 메소드도 사용할 수 있습니다(쿠키 설정과 같은).

  • org.apache.tomcat.sendfile.filename: 문자열로 보내질 파일의 캐노니컬 파일명
  • org.apache.tomcat.sendfile.start: Long 형태의 시작 오프셋
  • org.apache.tomcat.sendfile.end: Long 형태의 종료 오프셋

Copyright © 1999-2013, Apache Software Foundation