WebSocket
Spring 4.0에서부터 지원하는 기능중 가장 눈여겨봐야지 될 내용은 WebSocket입니다.
WebSocket은 RFC6455에서 정의한 기능으로 다음과 같은 특징을 가지고 있습니다.
- Real-time full duplex communication over TCP
- Uses port 80 / 443
- URL scheme : ws, wss (SSL)
- Small overhead for text message (frame base) - 0x00 ~ 0xFF
- Ping/Pong frames for staying alive
WebSocket을 이용해서 접근하게 되면 다음과 같은 Request와 Server Response가 나타납니다.
Request
GET /echo HTTP/1.1
Host: localhost:8080
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Sec-WebSocket-Version: 13
Origin: http://localhost:9000
Sec-WebSocket-Key: HfYHmSeeKJWzbQv/K5yBVQ==
Cookie: _ga=GA1.1.18456469.1406047636
Connection: keep-alive, Upgrade
Upgrade: websocket
Response
HTTP/1.1 101 Switching Protocols
Server: Apache-Coyote/1.1
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: i2iJBvVoYvXsVlrnYki5BNeKjew=
Date: Wed, 23 Jul 2014 17:56:03 GMT
WebSocket의 javascript client code는 다음과 같이 구성될 수 있습니다.
$scope.ws = new WebSocket('ws://localhost:8080/echo');
$scope.ws.onopen = function() {
console.log('websocket opened');
};
$scope.ws.onmessage = function(message) {
console.log(message);
console.log('receive message : ' + message.data);
};
$scope.ws.onclose = function(event) {
console.log(event);
console.log('websocket closed');
};
이제 Spring 4.0에서 지원하는 WebSocket을 구성해보도록 하겠습니다. WebSocket을 지원하는 서버를 개발하기 위해서는 다음과 같은 절차로서 구성되어야지 됩니다.
1) MessageHandler의 구성
2) Spring Configuratio에 ConnectionEndPoint 설정
이제 한 단계식 구성을 따라가보도록 하겠습니다.
MessageHandler의 구성
Client에서 전송되는 message를 처리하는 MessageHandler입니다. MessageHandler는 크게 2가지로 나눌 수 있습니다. TextWebSocketHandler와 BinaryWebSocketHandler가 그것입니다. 이 두가지는 이름으로 알 수 있듯이, Text Message/Binary Message를 각각 처리하는데 사용됩니다.
먼저, Input Text Message에 ECHO:를 붙여주는 매우 단순한 TextMessageHandler를 구성해보도록 하겠습니다. TextMessgeHandler의 경우 매우 중요합니다. json data의 경우에도 역시 TextMessageHandler를 통해서 처리가 되는 것이 일반적이기 때문에 큰 Binary를 처리하거나, 아니면 암호화가 된 특정 이진데이터를 처리하는 것이 아니라면 대부분의 WebSocket 서버에서 사용될 내용은 TextMessageHandler를 기반으로 구성이 가능합니다.
단순한 EchoTextMessageHandler는 다음과 같이 구성될 수 있습니다.
@Component
public class EchoHandler extends TextWebSocketHandler {
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
TextMessage echoMessage = new TextMessage("ECHO :" + message.getPayload());
session.sendMessage(echoMessage);
}
}
ConnectionEndPoint의 구성
WebSocket을 이용한 client code를 보시면 쉽게 아실 수 있는 것이, connection의 endpoint가 존재합니다. 이 endpoint를 통해서 데이터를 보내고, 받는 작업을 할 수 있습니다. 이런 endpoint를 지정하는 작업은 다음과 같이 진행할 수 있습니다.
ConnectionEndPoint를 구성하기 위해서 Spring은 @EnableWebSocket annotation과 WebSocketConfigurer interface를 제공하고 있습니다. 여기서 중요한 것은 WebSocketConfigurer interface입니다. interface는 다음과 같이 구성됩니다.
public interface WebSocketConfigurer {
void registerWebSocketHandlers(WebSocketHandlerRegistry registry);
}
registerWebSocketHandlers
라는 method 하나만이 존재하고, 이 method안에 connection endpoint를 다음과 같이 구성할 수 있습니다.
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoHandler, "/echo");
}
/echo를 connection endpoint로 만들어주고, 연결된 connection에 대한 Handler를 지정해줍니다. DI를 위해 방금 만든 EchoHandler를 Bean으로 등록하고, 등록된 Bean을 이용하는 전체 @Configuration code는 다음과 같습니다.
@Configuration
@EnableWebSocket
@EnableWebMvc
@ComponentScan(basePackages = {
"me.xyzlast.controllers",
"me.xyzlast.handlers"
})
public class ControllerConfiguration extends WebMvcConfigurerAdapter implements WebSocketConfigurer {
@Autowired
private EchoHandler echoHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoHandler, "/echo");
}
}
위 설정은 WebSocket 서버와 WebMVC를 모두 다 지원하기 위해서 구성한 결과입니다.
이제 WebSocket을 이용한 Echo Server 구성이 모두 마쳐졌습니다. tomcat 7.x 이상이 필요하며, angularJS를 이용한 client page는 다음과 같이 구성할 수 있습니다.
angular.module('websocketClientApp').controller('WsCtrl', function ($scope, $timeout) {
$scope.message = '';
$scope.ws;
$scope.echoMessages = [];
$scope.init = function() {
$scope.ws = new WebSocket('ws://localhost:8080/echo');
$scope.ws.onopen = function() {
console.log('websocket opened');
};
$scope.ws.onmessage = function(message) {
console.log(message);
console.log('receive message : ' + message.data);
$scope.echoMessages.unshift(message.data);
$timeout(function() {
$scope.$apply('echoMessages');
})
};
$scope.ws.onclose = function(event) {
console.log(event);
console.log('websocket closed');
};
};
$scope.send = function() {
$scope.ws.send($scope.message);
};
$scope.init();
});
<h3>WebSocket Test Page</h3>
<input type="text" ng-model="message"/>
<button ng-click="send()">SEND</button>
<ul ng-repeat="echo in echoMessages">
<li>{{echo}}</li>
</ul>
결과는 다음과 같습니다.
SocketJS
WebSocket은 지금까지 사용되고 있던 Ajax에 비하여 매우 훌륭한 방법입니다. 그렇지만, WebSocket이 Spring 4.0에서나 지원이 된 이유는 약간 발전이 너무나도 느렸습니다. WebSocket을 지원하는 Browser의 종류는 다음과 같습니다.
WebSocket의 경우, IE 10이상에서 지원하고 있기 때문에, 현실상 지원하기 힘듭니다. 이 문제를 해결하기 위해서, socket.io와 같은 websocket을 지원하지 않는 browser에서 websocket과 같이 접근하기 위한 방법들이 계속해서 나왔는데, SockJS도 그 방법들중 하나입니다.
SockJS는 기본적으로 WebSocket을 이용하고 있고, interface가 동일합니다. 다만 WebSockeet과는 다음과 같은 차이점을 가지고 있습니다.
- schema가 ws가 아닌 http
- WebSocket과 같이 Browser에서 제공되는 library가 아닌, 외부 library 사용
- IE 6이상부터 지원합니다.
- Server 측에서도 websocket이 아닌 SockJS server side library를 사용
websocket과 sockjs의 client code는 다음과 같습니다. 비교해보시면 schema만을 제외하면 완전히 동일한 것을 알 수 있습니다.
WebSocket
var ws = new WebSocket('ws://domain/endpoint');
ws.onopen = function() {
console.log('open socket');
}
ws.onmessage = function(message) {
console.log('message', message.data);
}
ws.onclose = function(e) {
console.log('close');
}
SockJS
var sock = new SockJS('http://domain/endpoint');
sock.onopen = function() {
console.log('open socket');
}
sock.onmessage = function(message) {
console.log('message', message.data);
}
sock.onclose = function(e) {
console.log('close');
}
Spring 4.0은 SockJS 서버를 지원하고 있으며, 지원 방법은 WebSocket 서버와 거의 유사합니다. 아니 동일한 수준입니다.
SockJS 서버의 구축 방법은 WebSocket 서버의 구축 방법과 동일합니다.
1) MessageHandler의 구성
2) Spring Configuratio에 ConnectionEndPoint 설정
MessageHandler의 구성
MessageHandler는 WebSocket과 완전히 동일합니다.
Connection EndPoint의 구성
Connection EndPoint의 경우에도 거의 유사합니다. 다음은 WebSocket의 endpoint와 SockJS의 endpoint 비교입니다.
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoHandler, "/echo");
registry.addHandler(echoHandler, "/echojs").withSockJS();
}
주의할점은 SockJS와 WebSocket의 endpoint를 동일하게 넣어서는 안된다는 점입니다. 같은 port를 이용하고, schema만이 다르기 때문에 접근되는 endpoint가 동일한 경우, 오동작을 발생시키면서 connection이 종료됩니다.
SockJS client의 구성
SockJS의 경우, bower에서 지원하고 있습니다. 다음 command를 통해서 bower로 install을 할 수 있습니다.
bower install sockjs --save
angularjs + yeoman을 통해서 구성한 sockjs client code는 다음과 같습니다.
angular.module('websocketClientApp').controller('SockCtrl', function ($scope) {
$scope.message = '';
$scope.sock;
$scope.init = function() {
$scope.sock = new SockJS('http://localhost:8080/echojs');
$scope.sock.onopen = function() {
console.log('websocket opened');
};
$scope.sock.onmessage = function(message) {
console.log(message);
console.log('receive message : ' + message.data);
};
$scope.sock.onclose = function(event) {
console.log(event);
console.log('websocket closed');
};
};
$scope.send = function() {
$scope.sock.send($scope.message);
};
$scope.init();
});
<script src="bower_components/sockjs/sockjs.js"></script>
<h3>SockJS Test Page</h3>
<input type="text" ng-model="message"/>
<button ng-click="send()">SEND</button>
STOMP
STOMP는 text 지향의 message protocol입니다. websocket을 이용한 message handling을 보다 더 쉽게 만들어줍니다. WebSocket이나 SockJS의 경우 onmessage function에서 받는 메세지를 모두 handling해야지 되는 단점을 가지고 있습니다. 이러한 단점을 구독(subscription)과 사용자 구독(user)를 통해서 처리할 수 있다는 장점을 가지고 있습니다.
STOMP를 이해하기 위해서는 Client code의 흐름을 보는 것이 좀 더 이해가 편합니다.
$scope.client;
$scope.name = '';
$scope.message = '';
$scope.init = function() {
var socket = new SockJS('http://localhost:8080/endpoint');
$scope.client = Stomp.over(socket);
$scope.client.connect({}, function(frame) {
console.log('connected stomp over sockjs');
$scope.client.subscribe('/subscribe/echo', function(message) {
console.log('receive subscribe');
console.log(message);
});
});
};
$scope.send = function() {
var data = {
name: $scope.name,
message: $scope.message
};
$scope.client.send('/app/echo', {}, JSON.stringify(data));
};
STOMP는 기본적으로 subscribe를 통해서 데이터를 전달받습니다. 따라서, 이 message가 어떠한 method에 의해서 전송을 받았는지를 좀 더 명확히 할 수 있습니다. 기존의 WebSocket이나 SockJS를 이용하게 되는 경우에는 전달받는 message의 범위가 매우 광범위하기 때문에, 전달되는 메세지에 message key값을 넣어서 send/receive에 대한 처리를 따로 해줘야지 되는 것이 일반적입니다. 그렇지만, STOMP의 경우에는 접근하는 방식이 send point와 subscribe point가 각기 다르기 때문에 처리를 좀 더 자유롭게 해줄 수 있습니다.
STOMP에서는 subscribe와 user/** 두개로 subscribe의 target을 나눌수 있는데, 각각의 차이는 전자는 전역이고, 후자는 호출된 사용자에게만 던져진다는 차이를 가지고 있습니다.
이제 Spring을 이용한 STOMP 서버 구축에 들어가보도록 하겠습니다.
Dependency 설정
STOMP를 지원하기 위해서 Spring은 spring-message에 대한 dependency를 필요로 합니다.
compile "org.springframework:spring-messaging:${rootProject.ext.springVersion}"
@Configuration 설정
STOMP를 지원하기 위해서는 @EnableWebSocketMessageBroker과 @Configuration에서는 AbstractWebSocketMessageBrokerConfigurer를 상속받거나, WebSocketMessageBrokerConfigurer 인터페이스를 구현해야지 됩니다.
@Configuration에서 할 일은 2가지입니다.
1) EndPoint의 설정
2) (option) STOMP URL에 대한 전역 prefix 설정
3) (option) send에 대한 subscribe url의 prefix 설정
4) @EnableWebSocketMessageBroker 설정
EndPoint 설정
EndPoint의 경우, SockJS를 사용할지, WebSocket을 사용할지에 따라 다른 설정을 해주게 됩니다. STOMP를 이용하는 것은 대부분 SockJS를 이용해서 Browser의 적합성을 높이는 경우가 많기 때문에, SockJS를 이용하는 것이 일반적입니다. WebSocketMessageBrokerConfigurer 인터페이스의 registerStompEndpoints method를 다음과 같이 정의합니다.
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/endpoint").withSockJS();
}
prefix 설정
Send Url에 대한 전역 prefix와 subscribe에 대한 전역 prefix를 설정할 수 있습니다. 이는 WebSocketMessageBrokerConfigurer 인터페이스의 configureMessageBroker method 에서 수정하면 됩니다. (아래는 모든 send prefix를 app으로 지정하고, subscribe의 기본 prefix를 subscribe로 지정했습니다.)
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/subscribe");
registry.setApplicationDestinationPrefixes("/app");
}
@EnableWebSocketMessageBroker 설정
모든 설정이 완료된 @Configuration code는 다음과 같습니다.
@Configuration
@EnableWebSocketMessageBroker
@ComponentScan(basePackages = {
"me.xyzlast.controllers"
})
public class StompConfigurer extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/endpoint").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/subscribe");
registry.setApplicationDestinationPrefixes("/app");
}
}
@Controller 구성
STOMP의 @Controller는 Spring MVC의 @Controller와 같은 @annotation을 사용하고, STOMP를 제공하기 위한 다음 @annotation들을 제공합니다.
@annotation | description |
---|
@MessageMapping | STOMP client의 send에 대한 target url입니다. |
@SendTo | STOMP client의 subscribe에 대한 target url을 지정합니다. (지정되지 않은 경우, @MessageMapping에 지정된 URL + @Configuration에서 설정된 prefix URL을 이용합니다. ) |
@SubscribeEvent | client에서 subscribe 할 수 있는 url을 지정합니다. 특정 message가 발생하거나 event가 발생했을 때, Client에 값을 전송하는데 사용합니다. |
annotation이 적용된 @Controller code는 다음과 같이 구성될 수 있습니다.
@Controller
public class EchoController {
@MessageMapping("/echo")
@SendTo("/subscribe/echo")
public Hello sendEcho(Hello hello) throws Exception {
System.out.println("receive message : " + hello.toString());
Hello echoHello = new Hello();
echoHello.setMessage(hello.getName());
echoHello.setName(hello.getMessage());
return echoHello;
}
}
위 @Controller의 sendEcho methos는 /echo
url을 통해 데이터를 전달받고, /subscribe/echo
를 통해 데이터를 전달하게 됩니다.
기존의 SockJS와는 다르게 MessageHandler를 따로 구현하지 않습니다. 기본적으로 @Controller를 이용해서 처리하기 때문에 기존의 WebMvc와 거의 유사한 형태의 개발을 진행할 수 있습니다.
User Destination
STOMP에서는 특정 사용자들에게 메세지를 보낼 수 있습니다. 기본값으로 설정되어 있는 /user
로 시작되는 subscribe의 경우에는 각 사용자들이 받을 수 있는 url을 제공합니다. 이는 사용자들의 특화된 message를 보내거나 error handling시에 주로 사용됩니다.
client.subscribe('/user/queue/private-message') = function(messaage) {
}
이러한 User subscribe는 Spring에서 @SendToUser annotation으로 기능이 따로 제공되고 있습니다.
@MessageMapping("/message")
@SendToUser
public String sendMessage(String message) {
return message.toUpperCase();
}
@MessageExceptionHandler
@SendToUser("/queue/errors")
public String handlingException(IllegalStateException ex) {
return ex.getMessage();
}