태그 : tomcat

자바 서블릿 컨테이너의 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 지원은 일반 네트워크 서버 스타일에 가깝다고 볼 수 있다. 좀더 웹 프로그래밍 스타일에 가깝게 만들었으면 좋지 않았을까 하는 생각이 든다.

by Corund | 2008/07/30 19:31 | 트랙백(1) | 핑백(1) | 덧글(1)

◀ 이전 페이지다음 페이지 ▶