Tomcat Advanced I/O 관련 서블릿 테스트를 해보려고 합니다.
맛보기 들어가 봅시다.. ㅎㅎ
[서블릿 프로젝트생성]
- Eclipse 에서 dynamic web project 로 생성 하시면 됩니다.
- 그 다음은 어떻게 따라 하는지 잘 모르시겠다면.. 아래 링크 참고하세요. ㅋㅋ
- 생성을 완료 하셨으면 WebContent 폴더 아래 index.jsp 파일을 하나 만들어서 HelloWorld 를 찍어 봅니다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
Hello World
</body>
</html>
- index.jsp 파일에서 우클릭 Run As -> Run on Server 를 실행해서 결과 화면을 봅니다.
자, 이제 Tomcat Advanced I/O 관련 코딩을 해보겠습니다.
- Servlet 을 하나 생성 합니다.
- 역시 그 다음을 잘 모르시겠다면 아래 링크 참고하세요.
- Servlet 코딩했으면 실행을 시켜야 겠죠.
http://localhost:8080/CONTEXT/SERVLET_CLASSNAME
- CONTEXT : Tomcat 설정 파일중 server.xml 에 보시면
<Context docBase="proto.dwp.comet" path="/proto.dwp.comet" ..../>
- SERVLET_CLASSNAME : 아래 코드로 예를 들었습니다. ChatServlet.java....
예)
http://localhost:8080/proto.dwp.comet/ChatServlet
[참고문서]
[Tomcat 설정변경]
- server.xml
[Before]
<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
[After]
<Connector connectionTimeout="20000" port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" useComet="true" redirectPort="8443"/>
[ChatServlet.java]
- 이 파일은 tomcat.apache.org 에 있는 예제 입니다.
- 이걸로 전체 테스트를 진행 하였습니다.
- event.getEventType 이 READ 부분에 보시면 빨간 부분만 추가했습니다.
- 추가한 이유는 server 에서 client 로 message 를 넘길 영역을 테스트 하기 위해서 입니다.
- 녹색 추가 및 주석은 그냥 한글 코드랑 HTML 코드 형태가 아닌 그냥 text 스탈로 처리 하려고 넣었습니다.
더보기 접기
package proto.dwp.comet;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.CometEvent;
import org.apache.catalina.CometProcessor;
/**
* Servlet implementation class ChatServlet
*/
public class ChatServlet extends HttpServlet implements CometProcessor {
private static final long serialVersionUID = 1L;
protected ArrayList<HttpServletResponse> connections = new ArrayList<HttpServletResponse>();
protected MessageSender messageSender = null;
/**
* @see HttpServlet#HttpServlet()
*/
public ChatServlet() {
super();
// TODO Auto-generated constructor stub
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
}
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());
response.setContentType("text/html; charset=utf-8");
/*
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());
for ( int i=0; i<10; i++ ) {
try {
messageSender.send("SYSTEM", "Loop Count : " + i);
Thread.sleep(5000);
} catch (InterruptedException e) {
}
}
} else if (n < 0) {
error(event, request, response);
return;
}
} while (is.available() > 0);
}
}
public void error(CometEvent event, HttpServletRequest request, HttpServletResponse response) {
log("error");
}
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);
}
}
}
}
}
}
}
접기
[index.jsp]
- ChatServlet 으로 메시지를 request 하고 Server 로 부터 response 를 받는 기능을 담당 합니다.
- XHR 에서 request method 는 POST 로 하셔야 합니다.
더보기 접기
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<script type="text/javascript">
var XHR = {
conn : null,
request : {
url : '',
method : 'GET',
data : '',
async : true
},
header : {
key : [],
value : {}
},
data : {
key : [],
value : {}
},
response : {
type : "XML"
},
createConnection : function() {
if (typeof window.XMLHttpRequest) {
this.conn = new XMLHttpRequest();
} else if (typeof window.ActiveXObject) {
this.conn = new ActiveXObject("Microsoft.XMLHTTP");
}
return this.conn;
},
createRequestData : function() {
var size = this.data.key.length;
for ( var i = 0; i < size; i++) {
this.request.data += this.data.key[i] + '='
+ this.data.value[this.data.key[i]] + '&';
}
this.request.data = this.request.data.substr(0,
this.request.data.length - 1);
return this.request.data;
},
createRequestHeader : function() {
var size = this.header.key.length;
for ( var i = 0; i < size; i++) {
this.conn.setRequestHeader(this.header.key[i],
this.header.value[this.header.key[i]]);
}
},
open : function() {
var requestUrl;
if (this.request.method == 'GET') {
requestUrl = this.request.url + '?' + this.request.data;
} else {
requestUrl = this.request.url;
}
this.conn.open(this.request.method, requestUrl, this.request.async);
},
send : function() {
if (this.request.method == 'GET') {
this.conn.send();
} else {
this.conn.send(this.request.data);
}
}
}
function xhrRequest() {
XHR.data.value = {message:document.getElementById('inputMessage').value};
XHR.request.data = '';
XHR.createRequestData();
XHR.conn.onreadystatechange = function () {
switch ( XHR.conn.readyState ) {
case 0 : // XHR.conn.UNSENT :
break;
case 1 : // XHR.conn.OPENED :
break;
case 2 : // XHR.conn.HEADERS_RECEIVED :
break;
case 3 : // XHR.conn.LOADING :
document.getElementById('divChat').innerHTML += "CASE 3 : " + XHR.conn.responseText + "<br>";
break;
case 4 : // XHR.conn.DONE :
doReadyStateChange();
break;
}
/*
별도로 수행해야 하는 함수 작성 필요
*/
function doReadyStateChange() {
if ( XHR.conn.status >= 200 && XHR.conn.status < 300 ) {
console.log("message send success");
document.getElementById('divChat').innerHTML += "CASE 4 : " + XHR.conn.responseText + "<br>";
document.getElementById('divChat').scrollTop = document.getElementById('divChat').scrollHeight;
xhrRequest();
} else if ( XHR.conn.status == 404 ) {
alert('서버를 찾을 수 없어, 메시지를 전송하지 못했습니다.');
} else {
alert('알수 없는 오류가 발생하여, 메시지를 전송하지 못했습니다..');
}
}
};
XHR.open();
XHR.createRequestHeader();
XHR.send();
}
function init() {
XHR.request.url = "http://localhost:8080/proto.dwp.comet/ChatServlet";
XHR.request.method = "POST"; // "POST";
XHR.header.key = ["Content-Type"];
XHR.header.value = {
"Content-Type":"application/x-www-url-form-encoded"
};
XHR.data.key = ["message"];
XHR.createConnection();
}
</script>
</head>
<body onload="init();">
Hello World
<hr>
<div id="divChat" style="font-size:10px; height:300px; overflow:auto"></div>
<input type="text" name="inputMessage" id="inputMessage"> <button onclick="xhrRequest(); return false;">초기전송</button>
</body>
</html>
접기
[BroadcasterServlet.java]
- 이넘은 Client 에서 Request 를 받는 역할을 하게 됩니다.
- 또한 response 를 하기 위한 작업도 이 넘이 하겠지요.
더보기 접기
package proto.dwp.comet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.CometEvent;
import org.apache.catalina.CometProcessor;
/**
* Servlet implementation class BroadcasterServlet
*/
public class BroadcasterServlet extends HttpServlet implements CometProcessor {
private static final long serialVersionUID = 1L;
private MessageSender messageSender;
/**
* @see HttpServlet#HttpServlet()
*/
public BroadcasterServlet() {
super();
// TODO Auto-generated constructor stub
this.messageSender = new MessageSender();
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
}
@Override
public void event(CometEvent event) throws ServletException, IOException {
HttpServletRequest request = event.getHttpServletRequest();
HttpServletResponse response = event.getHttpServletResponse();
String sessionId = request.getRequestedSessionId();
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");
}
}
}
접기
[MessageSender.java]
- client 로 message 를 전송 하는 역할을 합니다.
더보기 접기
package proto.dwp.comet;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.CometEvent;
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);
}
}
}
}
접기
[정리하면서]
- 일단 이걸 가지고 뭘 해야 좋을지는 좀더 고민을 해봐야겠다.
- 테스트 하면서 느낀건데 이넘을 테스트 하면서 READ 상태에서 END 가 되지 않을경우 이전 message 에 대한 buffer 를 다 유지하고 있는 것 같다.
- 틀린 내용이라 삭제 합니다.. ㅡ.ㅡ;; 화면 출력할때.. 잘 못 했더라구요..이런..
- 코드를 깊게 분석하지 않은 관계로 원인 규명은 다음으로.. ;;;;;
- Server Push 기능이나 Notification Chatting 같은걸 구현 하는데 사용이 가능해 보인다.
- 다만 이런 것들이 HTML5 로 넘어 오면서 많은 부분들이 브라우저에서 처리가 가능해 졌다는 점에서...
- 뭐.. 여기까지만.. ㅋㅋ