2013년 8월 1일 목요일

The first WebSocket.

HTML5의 주 기능 중에  “Websocket”이 있다는 건 웬만한 개발자라면 다 아는 사실이다. 하지만 사실상 적용된 사례는 드문 것 같다. 이유야 여러가지가 있겠지만 환경 부족과 개발자 인식의 부족인 문제인 것 같다. 또한 최근에는 모든 것이 다 되는(?) 모바일에 그 필요성이 없다고 생각 할 수도 있겠다.
“Websocket”의 특징을 말한다면 “Web브라우저를 기반으로 서버에서 클라이언트로 돈 안들이고 Message을 보낼수 있는 유일한 기술”라고 할 수 있을 것이다.


WebSocket개발에는 뭐가 필요할까?
WebSocket을 http”//로 시작하지 않고 ws://로 시작하는 URL형태를 가진다. 이는 HTTP 프로토콜을 기반으로 확장된 별도의 프로토콜을 가지기 때문이다.
“JSR 356(JavaTM API for WebSocket)”으로 명명된  WebSocket을 지원하는 WAS는 따로있으며
Tomcat의 경우 7이상, Jetty는 9이상의 버전부터 지원하고 있다.(다른 건 잘 모르겠습니다...)


Tomcat 7의 example로 포함된 WebSocket 예를 살펴보면


Server Side: EchoMessage
package websocket.echo;


import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.Date;


import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;


import org.apache.catalina.websocket.MessageInbound;
import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WebSocketServlet;
import org.apache.catalina.websocket.WsOutbound;


@WebServlet("/echo")
public class EchoMessage extends WebSocketServlet implements Servlet {


private static final long serialVersionUID = 1L;
private volatile int byteBufSize;
private volatile int charBufSize;


@Override
public void init() throws ServletException {
super.init();
byteBufSize = getInitParameterIntValue("byteBufferMaxSize", 2097152);
charBufSize = getInitParameterIntValue("charBufferMaxSize", 2097152);
}


public int getInitParameterIntValue(String name, int defaultValue) {
String val = this.getInitParameter(name);
int result;
if (null != val) {
try {
result = Integer.parseInt(val);
} catch (Exception x) {
result = defaultValue;
}
} else {
result = defaultValue;
}


return result;
}


@Override
protected StreamInbound createWebSocketInbound(String subProtocol,
HttpServletRequest request) {
return new EchoMessageInbound(byteBufSize, charBufSize);
}


private static final class EchoMessageInbound extends MessageInbound {


public EchoMessageInbound(int byteBufferMaxSize, int charBufferMaxSize) {
super();
setByteBufferMaxSize(byteBufferMaxSize);
setCharBufferMaxSize(charBufferMaxSize);
}


@Override
protected void onBinaryMessage(ByteBuffer message) throws IOException {
getWsOutbound().writeBinaryMessage(message);
}


@Override
protected void onTextMessage(CharBuffer message) throws IOException {
System.out.println("From Client:" + message.toString());
getWsOutbound().writeTextMessage(message);
}


@Override
protected void onClose(int status) {
super.onClose(status);
System.out.println("종료:" + status);
}


@Override
protected void onOpen(WsOutbound outbound) {
super.onOpen(outbound);
System.out.println("연결:" + outbound);
CharBuffer buffer = CharBuffer.allocate(100);
try {
String string = "Current time:"+ new Date()+", WebSocket 연결 하였습니다.";


for (int i = 0; i < string.length(); i++) {
buffer.put(string.charAt(i));
}


buffer.flip();
outbound.writeTextMessage(buffer);
buffer.clear();
} catch (IOException e) {
e.printStackTrace();
}
}


}


}
onOpen, onClose 콜백 메서드을 오버라이드해서 적절하게 사용 가능합니다.(여기서는 로그만..)
Inbound 타입은 여러가지가 있어 적절하게 골라 사용하면 됩니다.


Client Side: echo.html
<!DOCTYPE html>
<html>
<head>
   <title>Apache Tomcat WebSocket Examples: Echo</title>
   <style type="text/css">
       #connect-container {
           float: left;
           width: 400px
       }


       #connect-container div {
           padding: 5px;
       }


       #console-container {
           float: left;
           margin-left: 15px;
           width: 400px;
       }


       #console {
           border: 1px solid #CCCCCC;
           border-right-color: #999999;
           border-bottom-color: #999999;
           height: 170px;
           overflow-y: scroll;
           padding: 5px;
           width: 100%;
       }


       #console p {
           padding: 0;
           margin: 0;
       }
   </style>
   <script type="text/javascript">
       var ws = null;


       function setConnected(connected) {
           document.getElementById('connect').disabled = connected;
           document.getElementById('disconnect').disabled = !connected;
           document.getElementById('echo').disabled = !connected;
       }


       function connect() {
           var target = document.getElementById('target').value;
           if (target == '') {
               alert('Please select server side connection implementation.');
               return;
           }
           if ('WebSocket' in window) {
               ws = new WebSocket(target);
           } else if ('MozWebSocket' in window) {
               ws = new MozWebSocket(target);
           } else {
               alert('WebSocket is not supported by this browser.');
               return;
           }
           ws.onopen = function () {
               setConnected(true);
               log('Info: WebSocket connection opened.');
           };
           ws.onmessage = function (event) {
               log('Received: ' + event.data);
           };
           ws.onclose = function () {
               setConnected(false);
               log('Info: WebSocket connection closed.');
           };
       }


       function disconnect() {
           if (ws != null) {
               ws.close();
               ws = null;
           }
           setConnected(false);
       }


       function echo() {
           if (ws != null) {
               var message = document.getElementById('message').value;
               log('Sent: ' + message);
               ws.send(message);
           } else {
               alert('WebSocket connection not established, please connect.');
           }
       }


       function updateTarget(target) {
           if (window.location.protocol == 'http:') {
               document.getElementById('target').value = 'ws://' + window.location.host + target;
           } else {
               document.getElementById('target').value = 'wss://' + window.location.host + target;
           }
       }


       function log(message) {
           var console = document.getElementById('console');
           var p = document.createElement('p');
           p.style.wordWrap = 'break-word';
           p.appendChild(document.createTextNode(message));
           console.appendChild(p);
           while (console.childNodes.length > 25) {
               console.removeChild(console.firstChild);
           }
           console.scrollTop = console.scrollHeight;
       }
   </script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websockets rely on Javascript being enabled. Please enable
   Javascript and reload this page!</h2></noscript>
<div>
   <div id="connect-container">
       <div>
           <span>Connect using:</span>
           <!-- echo example using streams on the server side -->
           <input id="radio1" type="radio" name="group1" value="/FirstWebSocket/websocket/echoStream"
                  onclick="updateTarget(this.value);"> <label for="radio1">streams</label>
           <!-- echo example using messages on the server side -->
           <input id="radio2" type="radio" name="group1" value="/FirstWebSocket/websocket/echoMessage"
                  onclick="updateTarget(this.value);"> <label for="radio2">messages</label>
       </div>
       <div>
           <input id="target" type="text" size="40" style="width: 350px"/>
       </div>
       <div>
           <button id="connect" onclick="connect();">Connect</button>
           <button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
       </div>
       <div>
           <textarea id="message" style="width: 350px">Here is a message!</textarea>
       </div>
       <div>
           <button id="echo" onclick="echo();" disabled="disabled">Echo message</button>
       </div>
   </div>
   <div id="console-container">
       <div id="console"></div>
   </div>
</div>
</body>
</html>


기타 배포에 필요한 Code
EchoStream.java
package websocket.echo;


import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;


import javax.servlet.http.HttpServletRequest;


import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WebSocketServlet;
import org.apache.catalina.websocket.WsOutbound;


public class EchoStream extends WebSocketServlet {


   private static final long serialVersionUID = 1L;


   @Override
   protected StreamInbound createWebSocketInbound(String subProtocol,
           HttpServletRequest request) {
       return new EchoStreamInbound();
   }


   private static final class EchoStreamInbound extends StreamInbound {


       @Override
       protected void onBinaryData(InputStream is) throws IOException {
           // Simply echo the data to back to the client.
           WsOutbound outbound = getWsOutbound();


           int i = is.read();
           while (i != -1) {
               outbound.writeBinaryData(i);
               i = is.read();
           }


           outbound.flush();
       }


       @Override
       protected void onTextData(Reader r) throws IOException {
           // Simply echo the data to back to the client.
           WsOutbound outbound = getWsOutbound();


           int c = r.read();
           while (c != -1) {
               outbound.writeTextData((char) c);
               c = r.read();
           }


           outbound.flush();
       }
   }
}


Web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>FirstWebSocket</display-name>
<servlet>
<servlet-name>wsEchoMessage</servlet-name>
<servlet-class>websocket.echo.EchoMessage</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>wsEchoMessage</servlet-name>
<url-pattern>/websocket/echoMessage</url-pattern>
</servlet-mapping>


<servlet>
<servlet-name>wsEchoStream</servlet-name>
<servlet-class>websocket.echo.EchoStream</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>wsEchoStream</servlet-name>
<url-pattern>/websocket/echoStream</url-pattern>
</servlet-mapping>
</web-app>



결론
만일 여러분이 Dashboard 형태의 Web Application을 만든다면 “WebSocket”은 강력한 도구가 될 것입니다.

그 외 브라우저 환경에서 알림을 받는다든지, 웹 화면 Refresh 없이 데이터 변경을 위한 모든 상황에도 활용이 가능할 것입니다.

댓글 1개:

  1. 위에서 처럼 했는데 웹소켓이 왜 connect 되지 않을까요?
    spring 3.0.1에서 tomcat 7.0.42를 사용했습니다. 서버 콘솔상에서 아무 메시지도 안나옵니다.
    화면상에 "Info: WebSocket connection closed." 메시지만 출력되네요.
    도움 말씀 부탁드림니다.

    답글삭제

ETL 솔루션 환경

ETL 솔루션 환경 하둡은 대용량 데이터를 값싸고 빠르게 분석할 수 있는 길을 만들어줬다. 통계분석 엔진인 “R”역시 하둡 못지 않게 관심을 받고 있다. 빅데이터 역시 데이터라는 점을 볼때 분산처리와 분석 그 이전에 데이터 품질 등 데이...