자바 서블릿 컨테이너의 Comet 지원 2 - Tomcat

이전글

Tomcat 에서는 버전 6.0의 Advanced IO 지원을 통해 Comet 모델을 효과적으로 구현할 수 있게 되었다. Tomcat 6.0 에서는 Jetty 와는 달리 서블릿을 확장하는 형태로 지원하고 있다.

javax.servlet.Servlet 인터페이스를 확장한 org.apache.catalina.CometProcessor 가 그것인데 이 인터페이스는 Servlet 인터페이스를 상속한 것이다. 이 인터페이스에는 event(CometEvent) 메서드가 있는데 이 인터페이스를 구현한 클래스가 서블릿으로 등록되어 있으면 Tomcat 은 요청을 처리할 때 기존 서블릿의 service(...) 메서드를 호출하지 않고 이 event(...) 메서드를 호출하게 된다. 이 메서드는 한 요청 처리 트랜잭션에서 여러번 호출될 수 있으며 이벤트 정보가 인자로 넘어오게 된다. 이 인자로 넘어온 객체를 통해 여러 정보를 알 수 있고 IO도 처리할 수 있다. 입력(input)은 이 이벤트 처리 메서드 안에서 이루어져야 하지만, 출력(output)은 이 이벤트 처리 메서드 밖에서도 처리할 수 있다.

CometProcessor 를 사용하기 위해서는 Tomcat 설정을 조금 손봐야 한다. CometProcessor 는 Tomcat connector 가 Native 또는 NIO connector 여야 한다. 자바 1.4 버전 이상이라면 NIO 를 쓰면 되며 Tomcat 설정 파일을 다음과 같이 수정하면 된다.

<!-- 기존 설정. 아래와 같이 바꾼다.
<connector connectiontimeout="20000" port="8080" protocol="HTTP/1.1" redirectport="8443"/>
-->
<connector connectiontimeout="20000" port="8000">
     protocol="org.apache.coyote.http11.Http11NioProtocol" useComet="true" redirectPort="8443"/>

event 메서드는 BEGIN, READ, END, ERROR 이벤트가 발생할 때 호출되는데 이때 인자로 org.apache.catalina.CometEvent 객체가 넘어온다. 이 객체에는 위의 Event Type 을 얻는 메서드와 HttpServletRequest, HttpServletResponse 객체를 얻는 메서드 등이 있다.

이전 Jetty 의 경우와 마찬가지로 채팅 예제로 실제 사용법을 알아본다. 먼저 ChatServlet 과 chat.jsp 는 이전 jetty 의 경우와 같다. BroadcasterServlet 은 CometProcessor 를 사용해야 하는데 코드는 다음과 같다.

public class BroadcasterServlet extends HttpServlet implements CometProcessor {
  // ...
  @Override
  public void event(CometEvent event) throws ServletException, IOException {
    HttpServletRequest request = event.getHttpServletRequest();
    HttpServletResponse response = event.getHttpServletResponse();
    String sessionId = request.getSessionId();

    if (CometEvent.EventType.BEGIN == event.getEventType()) {
      // 요청을 최초로 처리할 때 호출됨.
      response.setContentType("text/html; charset=utf-8");
      messageSender.addSession(sessionId, event);
    } else if (CometEvent.EventType.ERROR == event.getEventType()) {
      // IO 에러가 발생했을 때.
      messageSender.removeSession(sessionId);
      event.close(); // 요청 처리 완료.
    } else if (CometEvent.EventType.END == event.getEventType()) {
      // 요청 처리가 완료되었을 때
      log("End event");
    } else if (CometEvent.EventType.READ == event.getEventType()) {
      log("Read event");
    }
  }
  // ...
}

요청을 최초로 처리할 때 BEGIN 이벤트가 발생하며 이때 Request 정보를 읽는 작업을 한다. 여기서는 채팅 메시지가 있을 때 메시지를 보낼 수 있도록 event 객체를 messageSender 객체에 저장해 놓는 작업도 한다.

요청에서 읽을 데이타가 준비되었을 때 (즉 request.getInputStream() 등) READ 이벤트가 발생하며, 에러가 발생했을 때 ERROR 이벤트가 요청 처리가 완료되면 END 이벤트가 발생한다. 여기서는 ERROR 이벤트가 발생하였을 때 관련 정보를 messageSender 에서 삭제하고 요청을 마치는 작업을 한다.

MessageSender 클래스는 다음과 같다. Jetty 의 경우와 조금 다른데 메시지를 실제로 보내는 작업도 여기서 처리하고 있다. CometProcessor 에 원하는 때 이벤트를 발생시킬 수 없기 때문이다. 따라서 이벤트가 발생할 때 출력을 하기 위해 미리 CometEvent 객체를 저장하여 HttpServletResponse 객체를 얻어 출력을 한다.

// MessageSender.java
public class MessageSender implements Runnable {
  private volatile boolean running = true;
  private final BlockingQueue<String> messages =
                                          new LinkedBlockingQueue<String>();
  private final Map<String, CometEvent> sessions =
                                          new ConcurrentHashMap<String, CometEvent>();
  private final ExecutorService executor = Executors.newFixedThreadPool(5);

  public void send(String message) {
    try {
      messages.put(message);
    } catch (InterruptedException ignore) {
      // ignore
    }
  }

  public void addSession(String id, CometEvent event) {
    sessions.put(id, event);
  }

  public void removeSession(String id) {
    sessions.remove(id);
  }

  public void stop() {
    this.running = false;
    this.executor.shutdown();
  }

  @Override
  public void run() {
    while (running) {
      String message = null;
      try {
        message = messages.take();
      } catch (InterruptedException ignore) {
        // ignore
      }
      for (String id : sessions.keySet()) {
        executor.submit(new Task(id, message));
      }
    }
  }

  private class Task implements Runnable {
    private String sessionId;
    private String message;

    public Task(String id, String msg) {
      sessionId = id;
      message = msg;
    }

    public void run() {
      CometEvent event = sessions.get(sessionId);
      if (null == event) {
        return;
      }
      HttpServletResponse response = event.getHttpServletResponse();
      PrintWriter out = null;
      try {
        out = response.getWriter();
        out.println(message);
        out.flush();
        response.flushBuffer();
      } catch (IOException naive) {
        naive.printStackTrace();
      } finally {
        try { out.close(); } catch (Exception ignore) {}
        try { event.close(); } catch (Exception ignore) {}
        sessions.remove(sessionId);
      }
    }
  }
}

Jetty 의 경우와 달리 Tomcat 은 Servlet 인터페이스를 확장하여 웹 요청 단계에 따라 이벤트를 발생시키는 방식으로 작동한다. 네트워크 서버가 많이 취하고 있는 방식이라 쉽게 이해할 수 있다. 그러나 실제 프로그래밍을 하려면 번거로운 점이 있다. Jetty 처럼 원하는 때 resume 을 시킬 수 없기 때문에 long polling 처럼 원하는 때 출력을 하려면 서블릿 코드 밖에서 출력을 해야 하며 따라서 직접 스레드를 관리할 필요가 생긴다. 또한 Error 시에 적절히 자원을 정리하는 코드도 필요하다.

정리하자면 Tomcat 의 Comet 지원은 일반 네트워크 서버 스타일에 가깝다고 볼 수 있다. 좀더 웹 프로그래밍 스타일에 가깝게 만들었으면 좋지 않았을까 하는 생각이 든다.

2008-07-30 19:31 | Permlink | Comments
blog comments powered by Disqus

About

Laser Keyboard

I'm Jin Kim. Live in Seoul, Korea. Software Developer. Java, Perl, Scala, Common Lisp. Mainly programming in web program and its backend data store. Interested in data processing and distributed computing.

Archive

RSS