본문 바로가기

카테고리 없음

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.jsxhr.jsjson_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

<%@ page language="java" contentType="text/html; charset=EUC-KR"
    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>