Framework & Platform/Spring

spring - WebSocket

linuxism 2014. 3. 27. 11:27


WebSocket is protocol that provides full-duplex communication, typically between a browser (or another client) and a webserver. This makes it suitable for highly interactive web applications such as chat, games, dashboards etc.

A websocket client connects to a websocket server and a handshake is performed. This handshake occurs over HTTP. Once the handshake is complete, the same connection is used for TCP based, bidirectional, socket communication. Since the handshake is done over HTTP, it is good bet that this connection will not be blocked by firewalls making it a good alternative to other RPC mechanisms.

Websocket Supporting Versions

Websocket is relatively new therefore only the latest web browsers, frameworks and application servers support this. You will be needing the following versions to run this example:

  • Spring framework - 4.0.1
  • Apache Tomcat – 7.0.52
  • Web browser – Chrome and Firefox has had support for quite a while. Take a look at http://caniuse.com/websockets and determine if your browser has websocket support.

For you to follow the example in this post, I am assuming you are already familiar with creating a Spring MVC application however, since at the time of writing of this post (March, 2014), Spring 4 is new, I’ll go through the steps you need to take in order to convert a Spring 3 application to a Spring 4.

Steps to convert a Spring 3 project to Spring 4

  1. Update servlet-api jar
    Websockets require servlet api version 3 or above.

    
            <dependency>
              <groupId>javax.servlet</groupId>
              <artifactId>javax.servlet-api</artifactId>
              <version>3.1.0</version>
            </dependency>
            
  2. Update web.xml namespaces,xsd files and version
    Open web.xml and take a look at the web-app tag. The namespaces, xsd and version should all be 3.0
  3. Update Spring framework jars.
    Update all spring framework jars to version 4.0.1.RELEASE. The artifacts you would require to upgrade are spring-core, spring-context, spring-web and spring-webmvc
  4. Update XML Schema locations
    In your Spring context XML files, update XML schema locations to point to version 4.0. For example,
    in your applicationContext.xml (or differently named content XML), the beans tag will have an attribute named xsi:schemaLocation. The values for this attribute are space separated URLs. Change the version of xsd files to 4. They will look something like http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
  5. Update Jackson libraries
    Jackson is the JSON library used by Spring. Version 1.x of the Jackson library has been deprecated by Spring so we need to upgrade to a later version.

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>2.3.0</version>
    </dependency>
    
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.3.0</version>
    </dependency>

 

Once these changes are made, you may still need to update some deprecated dependencies depending on which Spring features you are using after which your migration to Spring 4 would have been complete.

 

Websocket Implementation

Adding dependencies

Once you have successfully setup (or migrated to) a Spring 4 project, we can begin implementing websockets. We need the following dependencies in our classpath.


<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-websocket</artifactId>
   <version>4.0.1.RELEASE</version>
</dependency>
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-messaging</artifactId>
   <version>4.0.1.RELEASE</version>
</dependency>
Create a websocket handler class

A websocket handler is a class that will receive requests from the websocket client. In this example, we are creating a class called WebsocketEndPoint which extends the framework class TextWebSocketHandler. We are overriding a function (handleTextMessage) that will be called whenever a client sends a message to the server.

This function receives two parameters, the TextMessage object, which contains a String payload and a WebSocketSession object which we are using to send a message back to the client in the last line of the following snippet.


package co.syntx.example.websocket.handler;

import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

public class WebsocketEndPoint extends TextWebSocketHandler {

	@Override
	protected void handleTextMessage(WebSocketSession session,
			TextMessage message) throws Exception {
		super.handleTextMessage(session, message);
		TextMessage returnMessage = new TextMessage(message.getPayload()+" received at server");
		session.sendMessage(returnMessage);
	}
}
Create a handshake interceptor (Optional)

The websocket handshake interceptor is used to define and specify a class that intercepts the initial websocket handshake. Interceptors are purely optional. We are adding these here for debugging so that we know when and if a handshake took place.


package com.gemalto.dirserviceintegration.websocket.interceptor;

import java.util.Map;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

public class HandshakeInterceptor extends HttpSessionHandshakeInterceptor{

	@Override
	public boolean beforeHandshake(ServerHttpRequest request,
			ServerHttpResponse response, WebSocketHandler wsHandler,
			Map<String, Object> attributes) throws Exception {
		System.out.println("Before Handshake");
		return super.beforeHandshake(request, response, wsHandler, attributes);
	}

	@Override
	public void afterHandshake(ServerHttpRequest request,
			ServerHttpResponse response, WebSocketHandler wsHandler,
			Exception ex) {
		System.out.println("After Handshake");
		super.afterHandshake(request, response, wsHandler, ex);
	}

}
Configure Handler and Interceptor

We have two ways to configure handlers and interceptors
i) Create beans from within a java class that implements WebSocketConfigurer interface.(The new XML-less way)
ii) Define beans in spring application context via the applicationContext.xml (the traditional way).

We’ll use the later in this example.

Create the following beans via XML to configure the websocket handler and interceptor.


<bean id="websocket" class="co.syntx.example.websocket.handler.WebsocketEndPoint"/>

<websocket:handlers>
    <websocket:mapping path="/websocket" handler="websocket"/>
    <websocket:handshake-interceptors>
    <bean class="co.syntx.example.websocket.HandshakeInterceptor"/>
    </websocket:handshake-interceptors>
</websocket:handlers>

The first line creates an instance of the bean we created earlier.
The websocket mapping tag maps a URL pattern to the handler.
In the handler-interceptors tag, configure the interceptor bean.

At this point, our server configuration and code is complete so we can now proceed to write our client.

 

Websocket Client in Javascript


<script type="text/javascript">
        function setConnected(connected) {
            document.getElementById('connect').disabled = connected;
            document.getElementById('disconnect').disabled = !connected;
            document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
            document.getElementById('response').innerHTML = '';
        }

        function connect() {
        	if ('WebSocket' in window){
        		  console.log('Websocket supported');
        		  socket = new WebSocket('ws://localhost:8080//websocket');

        		  console.log('Connection attempted');

        		  socket.onopen = function(){
        			   console.log('Connection open!');
        			   setConnected(true);
        			}

        		  socket.onclose = function(){
        			  console.log('Disconnecting connection');
       			  }

        		  socket.onmessage = function (evt) 
        		     { 
        		        var received_msg = evt.data;
        		        console.log(received_msg);
        		        console.log('message received!');
        		        showMessage(received_msg);
        		     }

        		} else {
        		  console.log('Websocket not supported');
        		}
        }

        function disconnect() {
            setConnected(false);
            console.log("Disconnected");
        }

        function sendName() {
            var message = document.getElementById('message').value;
            socket.send(JSON.stringify({ 'message': message }));
        }

        function showMessage(message) {
            var response = document.getElementById('response');
            var p = document.createElement('p');
            p.style.wordWrap = 'break-word';
            p.appendChild(document.createTextNode(message));
            response.appendChild(p);
        }

</script>

The above javascript snippet gives you an idea of how to connect to and communicate with a websocket server.

  1. A new connection is attempted by new WebSocket(‘ws://localhost:8080//websocket’);
  2. During the attempt, the handshake with the server side is performed. If handshake is successful and connection is established, thesocket.onopen event handler is called.
  3. When a message from the server is received, the socket.onmessage event handler is called.
  4. In order to send a message to the server socket.send() function is used.



출처 - http://syntx.co/languages-frameworks/using-websockets-in-java-using-spring-4/






* 수정 될 사항

tomcat 8 이상 -> tomcat 7.0.47 이상

-  <artifactId>javax.websocket-api</artifactId>  dependency에서 

    <scope>provided</scope> 추가



Spring 4.0 에서 제공되는 핵심 기능 변경사항중 가장 주목할 점이 있다면 WebSocket의 지원입니다.


일단 Java에서는 Java EE 7에서 WebSocket에 대한 기본 API는 모두 구성이 마쳐져 있습니다. 당장 WebSocket을 지원하는 Web Page를 구성하기 위해서는 다음과 같은 조건들이 필요합니다.


1. Java EE 7 이상 지원

2. 최신 WAS 지원 (tomcat 8 이상, jetty 9.0.4 이상)


Spring에서는 WebSocket을 다음과 같은 방법으로 지원하고 있습니다.


1. @ServerEndPoint를 이용한 Java 기본 API

2. WebSocketHandler를 이용한 구성 - Spring WebSocket API


Spring WebSocket API의 경우, Java 기본 API와는 구성이 다릅니다. 이와 같은 구성을 갖게 되는 이유는 SocketJS와 같은 WebSocket을 이용하는 다른 API들을 사용할 수 있도록 한번 Wrapping을 거친 구성을 가지게 하는 것이 목표이기 때문입니다.. Spring WebSocket API의 경우, WebSocketHandler가 가장 핵심이 되고, 이를 이용한 코드 구성에 대해서 알아보도록 하겠습니다. 


먼저, pom.xml에 필요한 dependency를 설정합니다.

         <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>


websocket의 경우, servlet 3.0을 사용해야지 되고, websocket에 대한 dependency는 다음과 같습니다.


        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.websocket</groupId>
            <artifactId>javax.websocket-api</artifactId>
            <version>1.0</version>
        </dependency>


간단한 EchoService를 지원하는 WebSocket 지원 WebPage를 구성합니다. 그에 따른 WebSocketHandler는 TextWebSocketHandlerAdapter를 이용합니다. 여담이지만, Spring에서 Adapter라는 접미사가 붙은 객체들은 대부분 상속을 통해서 좀더 편하게 설정들을 할 수 있도록 도와주는 Spring에서 제공하는 일종의 Helper Class 또는 Parent Class 들이라고 할 수 있습니다.


WebSocketHandlerAdapter에서는 다음 3개의 method를 주목할 필요가 있습니다. 


1. afterConnectionEstablished(WebSocketSession session)

: WebSocket connection이 발생되었을 때, 호출되는 method입니다. connection open이 된 후기 때문에, 해줘야지 될 일들을 처리하면 됩니다. 


2. afterConnectionClosed(WebSocketSession session, CloseStatus status)

: WebSocket connection이 끊겼을 때, 호출되는 method입니다. connection close가 된 후, 일을 처리하면 됩니다. 


3. handleMessage(WebSocketSession session, WebSocketMessage<?> message)

: 핵심적인 method입니다. 실질적인 통신 method입니다. socket.accept() 후에 onMessage 에서 처리될 일들을 이곳에서 coding하면 된다고 생각하면 됩니다. 


한번 코드를 확인해보도록 하겠습니다. 


public class EchoWebSocketHandler extends TextWebSocketHandlerAdapter {

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        String payloadMessage = (String) message.getPayload();
        session.sendMessage(new TextMessage("ECHO : " + payloadMessage));
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // Connection이 구성된 후, 호출되는 method
        super.afterConnectionEstablished(session);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // Connection이 종료된 후, 호출되는 method
        super.afterConnectionClosed(session, status);
    }
}


WebSocket을 지원하는 Handler의 구성이 모두 마쳐진 후, WebSocketHandler를 Spring @MVC에 통합하기 위해서는 Config를 다음과 같이 구성합니다. 


@Configuration
@EnableWebMvc
@EnableWebSocket
public class WebConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // WebSocket을 /echo 에 연결합니다.
        registry.addHandler(echoHandler(), "/echo");

        // SocketJS 지원 url을 /socketjs/echo에 연결합니다.
        registry.addHandler(echoHandler(), "/socketjs/echo").withSockJS();
    }

    @Bean
    public WebSocketHandler echoHandler() {
        return new EchoWebSocketHandler();
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

매우 단순한 구조로 Config를 구성할 수 있습니다. Config에서 주목할 것은 WebSocketConfigurer interface입니다. 이 interface를 통해서 WebSocket Handler를 아주 쉽게 구성할 수 있습니다. 


마지막으로 web.xml을 대신할 DispatcherWebApplicationIntializer 입니다. 특별한 것은 없고, Servlet Configuration에서 만들어진 WebConfig.class를 반환합니다. 


public class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { WebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

    @Override
    protected void customizeRegistration(Dynamic registration) {
        registration.setInitParameter("dispatchOptionsRequest", "true");
    }

}


이제 WebSocket을 테스트 하기 위해 간단한 HTML Page를 구성해보도록 하겠습니다.


echo.html은 다음과 같이 구성할 수 있습니다. 


<!DOCTYPE html>
<html>
<head>
    <title>WebSocket/SockJS Echo Sample (Adapted from Tomcat's echo sample)</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 src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>

    <script type="text/javascript">
        var ws = null;
        var url = null;
        var transports = [];

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

        function connect() {
            if (!url) {
                alert('Select whether to use W3C WebSocket or SockJS');
                return;
            }

            ws = (url.indexOf('socketjs') != -1) ? 
                new SockJS(url, undefined, {protocols_whitelist: transports}) : new WebSocket(url);

            ws.onopen = function () {
                setConnected(true);
                log('Info: connection opened.');
            };
            ws.onmessage = function (event) {
                log('Received: ' + event.data);
            };
            ws.onclose = function (event) {
                setConnected(false);
                log('Info: connection closed.');
                log(event);
            };
        }

        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('connection not established, please connect.');
            }
        }

        function updateUrl(urlPath) {
            if (urlPath.indexOf('socketjs') != -1) {
                url = urlPath;
                document.getElementById('sockJsTransportSelect').style.visibility = 'visible';
            }
            else {
              if (window.location.protocol == 'http:') {
                  url = 'ws://' + window.location.host + urlPath;
              } else {
                  url = 'wss://' + window.location.host + urlPath;
              }
              document.getElementById('sockJsTransportSelect').style.visibility = 'hidden';
            }
        }

        function updateTransport(transport) {
          transports = (transport == 'all') ?  [] : [transport];
        }
        
        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;
        }
        function clear() {
            $('#message').html('');
        }
    </script>
</head>
<body>
<div>
    <div id="connect-container">
        <input id="radio1" type="radio" name="group1" onclick="updateUrl('/tutorial01/echo');">
            <label for="radio1">W3C WebSocket</label>
        <br>
        <input id="radio2" type="radio" name="group1" onclick="updateUrl('/tutorial01/socketjs/echo');">
            <label for="radio2">SockJS</label>
        <div id="sockJsTransportSelect" style="visibility:hidden;">
            <span>SockJS transport:</span>
            <select onchange="updateTransport(this.value)">
              <option value="all">all</option>
              <option value="websocket">websocket</option>
              <option value="xhr-polling">xhr-polling</option>
              <option value="jsonp-polling">jsonp-polling</option>
              <option value="xhr-streaming">xhr-streaming</option>
              <option value="iframe-eventsource">iframe-eventsource</option>
              <option value="iframe-htmlfile">iframe-htmlfile</option>
            </select>
        </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>


이제 구성된 Page의 테스트를 위해 maven jetty를 build 항목에 추가합니다. 


    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>2.5</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-eclipse-plugin</artifactId>
                <version>2.8</version>
                <configuration>
                    <downloadSources>true</downloadSources>
                    <downloadJavadocs>true</downloadJavadocs>
                    <wtpversion>2.0</wtpversion>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.eclipse.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>9.0.4.v20130625</version>
                <configuration>
                    <webApp>
                        <contextPath>/${project.artifactId}</contextPath>
                    </webApp>
                </configuration>
            </plugin>
        </plugins>
    </build>


구성후, mvn jetty:run 명령어를 통해서 실행이 가능합니다. 


저작자 표시 비영리 변경 금지


출처 - http://netframework.tistory.com/382