Thursday, October 8, 2009

comet

자바 서블릿 컨테이너의 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 설정 파일을 다음과 같이 수정하면 된다.



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 messages =
new LinkedBlockingQueue();
private final Map sessions =
new ConcurrentHashMap();
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)

트랙백 주소 : http://corund.egloos.com/tb/1910202
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Tracked from 나는 해피엔딩이 좋아 at 2009/06/02 19:25

제목 : 자바 서블릿 컨테이너의 Comet 지원 2 - To..
자바 서블릿 컨테이너의 Comet 지원 2 - Tomcat Corund 님의 글을 보고 테스트한 샘플코드를 올립니다. * 스크립트라이브러리로 jQuery를 사용했습니다. * chat.jsp 의 pollMessage 의 url을 조금 수정했습니다. - IE(7.0)에서 url을 캐싱하는 바람에 ajax request가 안가서요.. * Corund 님 설명대로 ChatMessageSenderListener 를 만들고 web.xml에서......more

more

Linked at 점프와 쉼없는 나아감 : 자바.. at 2008/08/01 09:35

... 이전글 Comet 에 대하여 자바 서블릿 컨테이너의 Comet 지원 1 - Jetty자바 서블릿 컨테이너의 Comet 지원 2 - Tomcat Resin 은 버전 3.1에서 Comet 모델을 지원한다. Tomcat 과 마찬가지로 javax.servlet.Servlet 을 확장한 인 ... more

Commented by 민예원 at 2009/05/29 11:15
좋은글 잘 보고 갑니다. 감사합니다.

No comments: