웹에서 프로그래밍을 하다보면 결국 채팅프로그램을 만드는 예제를 많이 하게된다.
채팅을 하기위해서 여러분이 선택하는 방법은 사실상 웹소켓으로 기결된다.
브라우저상에서 사용하지 않는다면 TCP소켓을 사용하는 경우도 있다.
일단 만고의 진리부터 한번보고 지나가자.
http는 절대 절대 절대 양방향이 되지 않는다.
http는 양방향이 되지 않는 모델이다.
request, response형태로 단방향만 가능하다.
또한 불변의 진리,
http는 서버에서 원하는 타이밍에 클라이언트에게 데이터를 보낼 수 없다.
문제는 http에서 서버에서 클라이언트로 역으로 요청하는건 불가능 하다는 것이다.
애당초 Client만이 Server로 연락할 수 있고 Server는 Client의 요청을 응답하는것만 가능하다는 것이다.
과거에는 이래도 상관없었다.. 그 과거가 꽤 많이 올라가야하긴 하다는 점이 문제.
요즘에는 둘의 통신이 매우 중요한 시기가 되었기 때문에 Server에서 반대로 클라이언트에 요청을 하고싶다는 생각을 하게 되었다.
하지만 http프로토콜은 원래부터 단방향으로 만들어졌기 때문에,
http프로토콜을 뜯어 고치지 않는한 불가능하다.
그러나 http프로토콜은 웹에서 사용되는 표준 프로토콜이고 이를 이용해서 양방향을 사용해야하는 상황에 봉착하게됬다.
막말로 웹에서는 http말고는 방법이 없었다. 지금이야 웹소켓이 있었지만 과거 웹은 오직 http만 가능했다.
그래서 마치 통신하는 것처럼 느끼게 만드는 방법을 고안해냈다.
가장 초기모델이 바로 polling이라는 기법이다.
polling이란 - 나는 너를 계속해서 관찰하겠어.
가장 무식한 방법이다.
스토킹 마냥 계속해서 일정 시간 간격으로 물어보는 방법이다.
무식하고 별로라고 생각할 수 있겠지만 여러가지 장점이 있는 방식이다.
http polling의 특징
1.주기적으로 물어보므로 응답 간격을 일정하게 할 수 있다.
2.주기적으로 몰아서 물어보는게 가능하므로 자동으로 배치프로세싱(일괄처리)되어서 db튜닝을 하는 효과가 나온다.
3.실시간으로 주는건 불가능하다. 실시간 효과를 내려면 간격을 줄여야 하지만 서버와 클라이언트 모두에게 부담이다.
4.보낼데이터가 없어도 계속해서 데이터를 줘야하므로 서버의 리소스를 낭비하게된다.
일단 계속해서 일괄적으로 물어보는점은 장점이다.
그래서 그래프를 그리거나 대용량의 데이터를 처리해야한다면 http polling은 아직도 유효하며 오히려 매우 간단하고 최적화된 방식이다.
문제는 실시간성이다. 이 방식은 어떻게 봐도 실시간성으로 좋지는 않은 방법이다.
위에도 설명을 했지만 http polling은 딜레마에 빠지게되는데 시간간격을 늘리면 실시간성이 떨어진다.
그래서 실시간 느낌을 주기위해서 시간간격을 줄이면 어떻게 될까?
http통신이 매우많이 마구마구보내지게 될 것이다.
http는 단발성 통신이기에 header가 매우 무거운 프로토콜중 하나이다.
이 프로토콜이 마구마구 보내진다면 서버에 매우 무거운 부하를 주게된다.
그렇다고 다시 시간을 늘리자니.... 이하 문제가 무한반복하게 된다.
그래서 새로운 기법을 고안하게 되었다.
바로 http long polling기법이다.
long polling - 나는 너랑 연결이 끊기지 않을꺼야
기본방식은 polling처럼 무한히 물어보는 것이다.
하지만 차이점이 있다면 일반 polling은 주기적으로 물어본다면,
long polling은 일단 보내고 time out될 때까지 무한정 기다린다는 것이다.
서버가 만약 "너 너무 나랑 오래 연결 되있어, 그럼 이만 끊을께" 하고 답을 보내면
바로 client는 집착녀,집착남 마냥 "헐.. 다시 연결할께"가 되는 것이다.
반대로 답을 주게 된다면 바로 이를 client에게 전달한다.
그리고 client는 정보를 받자말자 바로 다시 서버에 요청을 보낸다.
이 결과 연결은 무한히 지속되게 된다.
그리고 client는 마치 실시간으로 데이터를 받는 느낌을 받게 된다.
http long polling의 특징
1.항상 연결이 유지 되어 있다.
2.변경에 매우 민감하게 반응한다. 사실상 실시간으로 통신이 가능하다.
3.데이터가 주어지는 즉시 바로바로 반응하고 보내므로 요청간격이 줄어든다면 polling보다 훨씬 데이터를 많이 보내게된다.
long polling은 실시간으로 데이터를 핸들링 할수 있다는 polling에 없는 장점을 얻게 된다.
그래서 채팅을 구현할 때 많이 사용하는 기술중 하나이다.
다만 polling보다 좋은점만 있는것은 아니다.
일단 일반적으로 생각하기에는 polling보다 데이터를 적게 보낼꺼 같지만 만약 데이터 이동이 활발하다면??
그러면 오히려 주기적으로 한번에 보내는 polling보다 훨씬 더 많은 데이터를 보내게 된다.
위에서도 언급했지만 http는 헤더의 크기가 큰편에 속한다.
그래서 많은 데이터를 보낸다는것은 엄청난 비용이된다.
일단 구동원리는 알았으니 코드 예제를 보도록하자.
코드의 예제는 jsp로 만들었는데 필자가 만든건 아니고 필자가 찾았던 예제이다.
수정해서 쓰려고 헀는데 예제가 너무 괜찮은것 같아서 그냥 쓴다.
출처를 적으려고 했으나 다시 검색해도 출처가 나오지 않아서 부득이하게 출처를 적지 못한다.
혹시 이 코드의 출처가 되시는 분(외국분이겠지만)이 댓글을 남겨주시면 감사하겠다.
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>JSP Page</title>
</head>
<body onload="getMessages();">
<h1>SHOUT-OUT!</h1>
<form>
<table>
<tr>
<td>Your name:</td>
<td><input type="text" id="name" name="name"/></td>
</tr>
<tr>
<td>Your shout:</td>
<td><input type="text" id="message" name="message" /></td>
</tr>
<tr>
<td><input type="button" onclick="postMessage();" value="SHOUT" /></td>
</tr>
</table>
</form>
<h2> Current Shouts </h2>
<div id="content">
<% if (application.getAttribute("messages") != null) {%>
<%= application.getAttribute("messages")%>
<% }%>
</div>
<script>
function postMessage() {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("POST", "shoutServlet?t="+new Date(), false);
xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
var nameText = escape(document.getElementById("name").value);
var messageText = escape(document.getElementById("message").value);
document.getElementById("message").value = "";
xmlhttp.send("name="+nameText+"&message="+messageText);
}
var messagesWaiting = false;
function getMessages(){
if(!messagesWaiting){
messagesWaiting = true;
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange=function(){
if (xmlhttp.readyState==4 && xmlhttp.status==200) {
messagesWaiting = false;
var contentElement = document.getElementById("content");
contentElement.innerHTML = xmlhttp.responseText + contentElement.innerHTML;
}
}
xmlhttp.open("GET", "shoutServlet?t="+new Date(), true);
xmlhttp.send();
}
}
setInterval(getMessages, 1000);
</script>
</body>
</html>
package example;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import javax.servlet.AsyncContext;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(urlPatterns = {"/shoutServlet"}, asyncSupported=true)
public class ShoutServlet extends HttpServlet {
private List<AsyncContext> contexts = new LinkedList<>();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
final AsyncContext asyncContext = request.startAsync(request, response);
asyncContext.setTimeout(10 * 60 * 1000);
contexts.add(asyncContext);
System.out.println("Here is Get");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
List<AsyncContext> asyncContexts = new ArrayList<>(this.contexts);
this.contexts.clear();
String name = request.getParameter("name");
String message = request.getParameter("message");
String htmlMessage = "<p><b>" + name + "</b><br/>" + message + "</p>";
ServletContext sc = request.getServletContext();
if (sc.getAttribute("messages") == null) {
sc.setAttribute("messages", htmlMessage);
} else {
String currentMessages = (String) sc.getAttribute("messages");
sc.setAttribute("messages", htmlMessage + currentMessages);
}
for (AsyncContext asyncContext : asyncContexts) {
try (PrintWriter writer = asyncContext.getResponse().getWriter()) {
writer.println(htmlMessage);
writer.flush();
asyncContext.complete();
} catch (Exception ex) {
}
}
}
}
참고로 jsp를 사용하므로 jsp지식이 없다면 해당 포스트들을 참고하라.
코드를 분석하도록 하겠다.
<div id="content">
<% if (application.getAttribute("messages") != null) {%>
<%= application.getAttribute("messages")%>
<% }%>
</div>
해당 부분이 jsp로 되어있는데 db를 사용하지 않는 예제라서 그렇다.
이때까지의 채팅부분을 content에서 보여주겠다는 것이다.
껏다 켜도 application객체에 저장되어있으므로 데이터가 유지된다.
function postMessage() {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("POST", "shoutServlet?t="+new Date(), false);
xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
var nameText = escape(document.getElementById("name").value);
var messageText = escape(document.getElementById("message").value);
document.getElementById("message").value = "";
xmlhttp.send("name="+nameText+"&message="+messageText);
}
여러분이 친 메시지를 서버로 보내는 예제이다.
browser내장객체의 XMLHttpRequest를 사용해서 ajax를 보낸다.
사실 특이할거 없다.
function getMessages(){
if(!messagesWaiting){
messagesWaiting = true;
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange=function(){
if (xmlhttp.readyState==4 && xmlhttp.status==200) {
messagesWaiting = false;
var contentElement = document.getElementById("content");
contentElement.innerHTML = xmlhttp.responseText + contentElement.innerHTML;
}
}
xmlhttp.open("GET", "shoutServlet?t="+new Date(), true);
xmlhttp.send();
}
}
setInterval(getMessages, 1000);
이는 롱풀링 예제이다. 나머지는 ajax코드인데 맨 밑에 보면 아시겠지만 setInterval로 1초간격으로 getMessgae가 스케줄링된다.
쉽게 이야기하면 1초간격으로 get요청을 하는것이다.
이거까지 보면마치 polling같다. 사실 위코드는 polling코드로 봐도 손색이 없다.
하지만 서버를 보면 이는 polling코드가 아니라는것을 알 수 있다.
private List<AsyncContext> contexts = new LinkedList<>();
일단 여러 비동기처리들을 모을 배열을 선언한다.
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
final AsyncContext asyncContext = request.startAsync(request, response);
asyncContext.setTimeout(10 * 60 * 1000);
contexts.add(asyncContext);
System.out.println("Here is Get");
}
AsyncConext를 사용한다. AsyncContext에 대해서 잘 모른다면 이 포스팅을 참조하라.
보면 Timeout은 10분으로 되어있다.
그렇다. 즉 연결이 되면 10분동안 데이터가 없다면 연결되게 된다.
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
List<AsyncContext> asyncContexts = new ArrayList<>(this.contexts);
this.contexts.clear();
String name = request.getParameter("name");
String message = request.getParameter("message");
String htmlMessage = "<p><b>" + name + "</b><br/>" + message + "</p>";
ServletContext sc = request.getServletContext();
if (sc.getAttribute("messages") == null) {
sc.setAttribute("messages", htmlMessage);
} else {
String currentMessages = (String) sc.getAttribute("messages");
sc.setAttribute("messages", htmlMessage + currentMessages);
}
for (AsyncContext asyncContext : asyncContexts) {
try (PrintWriter writer = asyncContext.getResponse().getWriter()) {
writer.println(htmlMessage);
writer.flush();
asyncContext.complete();
} catch (Exception ex) {
/*pass*/
}
}
}
만약 데이터가 생긴되면 각각의 Async객체에서의 데이터를 제거해주면된다.
위 코드는 만든사람이 잘만들어서 배치처리프로스세까지 함께 있다.
long polling은 모든 브라우저에서 사용가능하므로 매우 자주쓰는 기법이다.
기업 면접등에서도 자주 물어보는 질문이므로 반드시 알아두길 바란다.