AJAX, JSP Chat Example
AJAX, JSON, JSP 를 사용한 채팅 프로그램의 예
개요
데이터베이스와 연동하지 않고 서버측의 Vector객체에 이용자의 메시지를 순서대로 저장하면서 메시지에 순번을 지정하여 설정해 둔다. 클라이언트는 AJAX와 타이머를 이용하여 2초마다 JSP에 요청을 하거나 메시지를 입력하고 엔터를 쳐서 요청을 할 수 있다. 클라이언트가 요청할 때는 서버측에서 메시지를 전달할 때 전달했던 메시지 ID를 다시 전송하여 현재 클라이언트가 가지고 있는 메시지와 서버에 있는 메시지가 얼마나 차이가 있는지 확인할 수 있다. 서버는 클라이언트에서 전송된 클라이언트측의 현재 메시지 ID를 확인하여 그 이후의 메시지를 모두 선택하여 해당 클라이언트에게 전송할 수 있다.
클라이언트와 서버간의 메시지는 JSON배열을 문자열(jsonObj.toJSONString())로 표현하여 텍스트 상태로 전송하고 그 텍스트를 받은 서버나 클라이언트는 다시 JSON 객체로 변환하여 편리하게 속성을 추출할 수 있다.
클라이언트가 서버로 전송하는 메시지는 JSONObject 의 문자열 표현이고, 서버측에서 클라이언트측으로 응답하는 메시지는 JSONArray의 문자열 표현이다. XML문서 안에 JSON문자열을 포함할 때 <![CDATA[ JSON String ]]> 표현을 사용했다.
클라이언트 측에서 JSON객체를 텍스트 표현으로 추출하기 위해서 json2.js 를 사용하고, 서버측에서 JSON문자열을 파싱할 목적으로 json-simple 라이브러리를 사용했다. xhr.js는 XMLHttpRequest객체를 생성하기 위한 자바스크립트 함수를 정의했다.
서버측에서 Vector에 이용자의 메시지를 저장할 때 최대 100개만 저장하고 그 이상의 메시지가 들어오면 오래된 메시지부터 삭제하는 방법을 사용하여 메시지가 무한정 메모리를 차지하는 위험성을 방지하려고 했다. 중요한 메시지일 경우에는 그냥 메시지를 삭제할 것이 아니라 DB에 백업해 두고 Vector에서는 삭제하는 방법으로 보완할 수 있다고 생각한다.
json2.js
xhr.js
json_simple-1.1.jar
chat_client.html
<!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=EUC-KR">
<title>AJAX Chat Client</title>
<style>
#chat_msg { font-family: fantasy; }
</style>
<script type="text/javascript" src="xhr.js"></script>
<script type="text/javascript" src="json2.js"></script>
<script type="text/javascript">
var getReq = createRequest();
var sendReq = createRequest();
var msgid = 0;
var timerId = 0;
/* 메시지입력란에 입력된 메시지를 JSON문자열로 변환하여 서버로 전송*/
function send(){
var sm = form1.send_msg.value;
if(sm==''){
alert("입력한 메시지가 없습니다");
form1.send_msg.focus();
return;
}
var url = "chat_server.jsp";
sendReq.open("POST", url, true);
sendReq.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
sendReq.onreadystatechange = sendResultHandler;
var jobj = {};
jobj.msgid=msgid;
jobj.content=sm;
var params = jobj.toJSONString();
clearTimeout(timerId);
sendReq.send("msg="+params);
form1.send_msg.value='';
}
/* 메시지입력후 엔터키를 친 경우 폼의 전송을 막고 send()호출 */
function keyDownHandler(e){
if(window.event.keyCode==13) {
if(window.event) event.returnValue = false;
else e.preventDefault();
send();
}
}
/* 프로그램 시작시 2초후에 getChatText()호출하여 서버상의 최근 메시지 ID를 가져옴*/
function onLoadHandler(){
form1.send_msg.focus();
timerId = window.setTimeout("getChatText()", 2000);
}
/* 주기적으로 호출되어 새로운 메시지를 가져옴 */
function getChatText(){
var jsonObj = {};
jsonObj.msgid=msgid;
var url = "chat_server.jsp?msg="+jsonObj.toJSONString();
getReq.open("GET", url, true);
getReq.onreadystatechange = msgReceivedHandler;
getReq.send(null);
}
/* getChatText()으로 요청결과로 서버측 응답이 도착했을 때 데이터 처리*/
function msgReceivedHandler(){
if(getReq.readyState==4 && getReq.status==200){
var xmlDoc = getReq.responseXML;
var jsonStr = null;
if(xmlDoc!=null){
try{
jsonStr = xmlDoc.documentElement.firstChild.nodeValue;
}catch(xmlDocNull){/*alert(jsonStr); */}
}
if(jsonStr!=null && jsonStr.length!=0) {
var jsonArray = eval('('+jsonStr+')');
var jsonObj = null;
for(var i=0;i<jsonArray.length;i++){
jsonObj = jsonArray[i];
if(jsonObj.content==null)break;
var div = document.getElementById("chat_msg");
div.innerHTML += jsonObj.userId+": "+jsonObj.content+"<br/>";
div.scrollTop = div.scrollHeight;
}
msgid = jsonObj.msgid;
}
timerId = setTimeout("getChatText()",2000);
}
}
/* 메시지입력 후 엔터나 버튼을 눌러서 send()메소드의 요청결과 */
function sendResultHandler(){
if(sendReq.readyState==4 && sendReq.status==200){
clearTimeout(timerId);
getChatText();
}
}
</script>
<style type="text/css">
#chat_msg {
width:99%; height:99%; overflow: auto;
background-color: #eeeeee; text-align: left;
font-size: 9pt;
margin-bottom: 20px;
}
div.outer {
background-color:#dddddd;
border-color 1px solid black;
width:460px; height:400px;
text-align: center;
}
input.chatbox { width:340px; }
</style>
</head>
<body onLoad="onLoadHandler();"><br/><br/><center>
<div class="outer">
<div id="chat_msg"></div>
<form name="form1">
메시지 <input class="chatbox" type="text" name="send_msg" onKeyDown="keyDownHandler(event);"/>
<input type="button" value="전 송" onClick="send();"/>
</form>
</div>
</center>
</body>
</html>
chat_server.jsp
<?xml version="1.0" encoding="utf-8"?>
<%@page import="java.util.*"%>
<%@page import="chat.*"%>
<%@ page contentType="text/xml; charset=utf-8" pageEncoding="EUC-KR"%>
<jsp:useBean id="chatMgr" class="chat.ChatMgr" scope="session">
<jsp:setProperty name="chatMgr" property="servletContext" value="<%=application%>"/>
</jsp:useBean>
<jsp:setProperty name="chatMgr" property="request" value="<%=request%>"/>
<%
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
%>
<msgs>
<![CDATA[${sessionScope.chatMgr.resMsg}]]>
</msgs>
ChatMgr.java
package chat;
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.json.simple.*;
public class ChatMgr {
private static Vector<MsgBean> msgList;
//private int lastMsgId;
private HttpServletRequest request;
private ServletContext servletContext;
public ChatMgr() {}
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
synchronized(servletContext){
if(servletContext.getAttribute("msgList") == null){
msgList = new Vector<MsgBean>();
MsgBean mb = new MsgBean(1,"채팅이 시작되었습니다","Admin");
msgList.add(mb);
servletContext.setAttribute("msgList", msgList);
}
}
msgList = (Vector<MsgBean>)servletContext.getAttribute("msgList");
}
public String getResMsg(){
String msg = request.getParameter("msg");
String jarrStr = null;
if(msg!=null && !msg.equals("")) {
long userLast = this.addMsg(msg);
if(userLast>=0) {
try{
jarrStr = this.getMsgAfterLast(msg);
}catch(Exception e){e.printStackTrace();}
}
}
return jarrStr;
}
/** JSON 문자열을 받아서 MsgBean에 저장하고 MsgBean은 Vector에 저장
* @param msg: 이용자가 전달한 메시지로서 JSON 문자열
* @return 이용자화면에 출력된 마지막 메시지 번호를 리턴한다.
* 만약 msg에 값이 없으면 -1을 리턴한다
*/
public long addMsg(String msg){
if(msg==null || msg.equals("")) return -1;
JSONObject jobj = (JSONObject)JSONValue.parse(msg);
long userLast = (Long)jobj.get("msgid");
String content = (String)jobj.get("content");
if(content==null || content.equals("")) return userLast;
String ip = request.getRemoteAddr();
/*100개 까지만 메시지를 저장하고 초과할 경우 오래된 메시지를 삭제한다 */
synchronized(servletContext){
while(msgList.size()>=100){
msgList.remove(0);
}
}
HttpSession session = request.getSession();
String userId = (String)session.getAttribute("userId");
if(userId==null) userId = ip; // 로그인하지 않았다면 IP주소로 대신
synchronized(servletContext){
int msgid = msgList.get(msgList.size()-1).getMsgid()+1;
msgList.add(new MsgBean(msgid,content,ip,userId));
}
return userLast;
}
/**
* 이용자의 화면에 출력된 마지막 메시지 번호를 받아서 그 이후의 메시지를
* JSON문자열 배열로 리턴한다
* @param userLast
* @return
*/
public String getMsgAfterLast(String msg){
if(msgList.size()==0) return null;
JSONObject usrObj = (JSONObject)JSONValue.parse(msg);
long userLast = (Long)usrObj.get("msgid");
/* 채팅에 접속하여 첫 요청인 경우
* 가장 최근의 메시지 ID를 브라우저에 전송한다
*/
if(userLast==0) {
JSONArray jarr = new JSONArray();
JSONObject jobj = new JSONObject();
jobj.put("msgid", msgList.get(msgList.size()-1).getMsgid());
//jobj.put("content", msgList.get(msgList.size()-1).getContent());
//jobj.put("ip", request.getRemoteAddr());
jarr.add(jobj);
return jarr.toJSONString();
}
String jsonStr = "";
JSONArray jarr = new JSONArray();
long i = 0;
for(i=msgId2Index(userLast)+1;i<msgList.size();i++){
MsgBean mb = (MsgBean)msgList.get((int)i);
JSONObject jobj = new JSONObject();
jobj.put("msgid",mb.getMsgid());
jobj.put("content",mb.getContent());
jobj.put("userId", mb.getUserId());
jarr.add(jobj);
}
if(jarr.size()==0) return null;
else return jarr.toJSONString();
}
private int msgId2Index(long msgid){
int topId = this.msgList.get(0).getMsgid();
int dif = (int)(msgid - topId);
return dif;
}
public void setRequest(HttpServletRequest request) {
try {
request.setCharacterEncoding("utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
this.request = request;
}
public Vector<MsgBean> getMsgList() {
return msgList;
}
}
MsgBean.java
package chat;
public class MsgBean {
private int msgid;
private String content;
private String ip;
private String userId;
public MsgBean() {}
public MsgBean(int msgid, String content, String ip) {
this.msgid = msgid;
this.content = content;
this.ip = ip;
}
public MsgBean(int msgid, String content, String ip, String userId) {
this.msgid = msgid;
this.content = content;
this.ip = ip;
this.userId = userId;
}
public int getMsgid() {
return msgid;
}
public String getContent() {
return content;
}
public void setMsgid(int msgid) {
this.msgid = msgid;
}
public void setContent(String content) {
this.content = content;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
}
chatLogin.jsp
pageEncoding="EUC-KR"%>
<%
request.setCharacterEncoding("euc-kr");
String userId = request.getParameter("userId");
if(userId!=null && !userId.equals("")){
session.setAttribute("userId", userId);
response.sendRedirect("chat_client.html");
}
%>
<!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=EUC-KR">
<title>로그인</title>
<style>
div{border:1px solid blue;
padding: 20px 20px 20px; text-align: center;
vertical-align:middle;
width:300px; height:100px;
font-family: cursive;
}
</style>
</head>
<body>
<br/><br/><center>
<h3>탑골 채팅방에 오신것을 환영합니다<br/>
채팅에 참여하기 위해서는 아래의 폼을 작성해야 합니다</h3>
<div>
<form name="form1" method="post" action="chatLogin.jsp"
onSubmit="return form1.userId.value!=''? return true: false;">
채팅에 사용할 닉네임(Nick Name)<br/>
<input type="text" name="userId" />
<input type="submit" value="로그인"/>
</form>
</div>
</center>
</body>
</html>