websockets with spring 4
DESCRIPTION
Talk delivered at Java2Days 2013 on Websocket support in Spring 4 with SockJs and StompTRANSCRIPT
WebSockets with Spring 4Sergi Almar @sergialmar
• CTO @ Voz.io
• Spring Certified Trainer
• javaHispano Core Member
• Spring I/O conference organiser
Who I am
Social FeedsMultiplayer Games
Collaborative Apps
Clickstream DataFinancial Tickets Sports Updates
Multimedia Chat
Location-based AppsOnline Education
• Polling
• Long Polling / Comet
• Flash
Real-Time Data on the Web
Problem Applications need two-way communication Too many connections and overhead with ajax / comet
WebSocketstwo-way communication done right
• Real-time full duplex communication over TCP
• Uses port 80 / 443 (URL scheme: ws:// and wss://)
• Small overhead for text messages (frames)
• 0x00 for frame start, 0xFF for frame end (vs HTTP 1Kb)
• Ping / pong frames for staying alive
WebSocket Protocol / RFC 6455
WebSocket HandshakeGET /mychat HTTP/1.1!Host: server.example.com!Upgrade: websocket!Connection: Upgrade!Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==!Sec-WebSocket-Protocol: chat!Sec-WebSocket-Version: 13!Origin: http://example.com!
client sends a WebSocket handshake request
HTTP/1.1 101 Switching Protocols!Upgrade: websocket!Connection: Upgrade!Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=!Sec-WebSocket-Protocol: chat!
server response
JS WebSocket API var ws = new WebSocket("ws://www.java2days.com/ws");!! // When the connection is open, send some data to the server! ws.onopen = function () {! ws.send('Here I am!');! };!! // Log messages from the server! ws.onmessage = function (event) {! console.log('message: ' + event.data);! };!! ws.onclose = function (event) {! console.log('closed:' + event.code);! };!
• Multiple implementations before the standard
• JSR-356 (May 2013)
• Reference implementation Tyrus (bundled with Glassfish 4)
• Rewrite across containers (tomcat 8.0.0-RC5, Jetty 9.1…)
Java WebSocket Implementations
WebSockets.springify()
• WebSockets are now supported in Spring 4
• Fallback options with SockJS
• STOMP over WebSocket
• Foundation for messaging architecture
Spring WebSockets
WebSocket Handlers
public class EchoHandler extends TextWebSocketHandlerAdapter {!! @Override! public void handleTextMessage(WebSocketSession session, !! ! ! ! ! ! ! ! ! ! TextMessage message) throws Exception {! session.sendMessage(message);! }!! }
WebSocket Config
@Configuration! @EnableWebSocket! public class WsConfig implements WebSocketConfigurer {!! @Override! public void registerWebSocketHandlers(!! ! ! ! ! ! ! ! ! ! ! WebSocketHandlerRegistry registry) {!! registry.addHandler(new EchoHandler(), "/echo");! }! }!
• Previous example showed how to configure a global handler
• but you may want to have a stateful per-session handler
Per-Session Handler
@Configuration!@EnableWebSocket!public class WsConfig implements WebSocketConfigurer {!! @Bean! public WebSocketHandler echoHandler() {! return new PerConnectionWebSocketHandler(EchoHandler.class);! }!! @Override! public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {! registry.addHandler(echoHandler(), "/echo");! }!}
DI
Can anyone join the party?
http://caniuse.com/#feat=websockets / DEC 2013
SockJSdude, where are my socks?
• Coherent, cross-browser, Javascript API for full duplex communication.
• Close to HTML5 WebSockets API
• Client and server side implementation (ruby, node…and also in spring-websockets)
SockJS
SocksJS API var sock = new SockJS('http://www.java2days.com/ws');!! sock.onopen = function() {! sock.send('Here I am'); ! };!! sock.onmessage = function(event) {! console.log('message', e.data);! };!! sock.onclose = function() {! console.log('close');! };!
• Base URL: /base_url
• Info test: /base_url/info
• Session URL: /base_url/server/session
SockJS URLs
Enabling SocksJS
@Configuration! @EnableWebSocket! public class WsConfig implements WebSocketConfigurer {!! @Override! public void registerWebSocketHandlers(!! ! ! ! ! ! ! ! ! ! ! WebSocketHandlerRegistry registry) {!! registry.addHandler(new EchoHandler(), “/echo”).withSockJS();! }! }!
MessageHandler doesn’t change (SocketJsService delivers the message to the handler regardless of the protocol)
• WebSockets are too low level and different from HTTP / REST
• We need asynchronous, event-driven, reactive programming style
• Mmmm, that sounds familiar: JMS, AMQP… but we still want to stick to the Spring MVC programming model
Problem
Here’s were we are…
…but we want something like this
• Stomp over WebSocket
• Some Spring Integration types have been promoted to the core
• Message, MessageChannel, MessageHandler…
• @MessageMapping, @SubscribeEvent…
• New spring-messaging module
Solution
STOMP
• Simple interoperable protocol for asynchronous messaging
• Supported by Apache ActiveMQ, RabbitMQ, HornetQ…
• Frames modelled on HTTP
STOMP
COMMAND!header1:value1!header2:value2!!Body^@!
var socket = new SockJS('/myapp/echo');!var client = Stomp.over(socket);
• SEND a message
• SUBSCRIBE / UNSUBSCRIBE from destination
• ACK / NACK the reception of a message (optional by default)
Client Frames
• Convey a MESSAGE from subscription to the client
• Send RECEIPT when server has successfully processed a client frame
• Send an ERROR if something goes wrong
Server Frames
Configuration!@Configuration!@EnableWebSocketMessageBroker!public class Config implements WebSocketMessageBrokerConfigurer {!! @Override! public void registerStompEndpoints(StompEndpointRegistry r){! r.addEndpoint("/stomp");! }!! @Override! public void configureMessageBroker(MessageBrokerConfigurer c){! c.enableSimpleBroker("/topic/");! c.setApplicationDestinationPrefixes("/app");! }!!}!
Subscriptions processed by spring
simple broker
Sending MessagesstompClient.send("/app/trade", {}, JSON.stringify(trade));
SEND!destination:/app/trade!content-type:application/json!content-length:47!!{"action":"Sell","ticker":"DELL","shares":"10"}!
@MessageMapping("/trade")!public void executeTrade(Trade trade, Principal principal) {!! trade.setUsername(principal.getName());!! this.tradeService.executeTrade(trade);!}
stomp.js
supports ant-style patterns
prefix
• Flexible handler method signatures
• @PathVariable, @Header/@Headers, @Payload, Message, Principal
• Message converters
Handler Methods
@Controller!public class ChatController {!! @MessageMapping("/message")! public void sendMessage(String message, Principal principal) {! // ...! }!}!
• Can also return a value
!
!
!
!
• Or define the destination with @SendTo
Handler Methods
@MessageMapping("/message")! public String sendMessage(String message) {! return message.toUpperCase();! }
Return wrapped in a Message and sent to /topic/message
@MessageMapping("/message")! @SendTo("/topic/spring-room")! public String sendMessage(String message) {! return message.toUpperCase();! }!
Intercepting SubscriptionsstompClient.subscribe("/app/positions", function(message) {! ...!});!
SUBSCRIBE!id:sub-0!destination:/app/positions!
@Controller!public class PortfolioController {!! @SubscribeEvent("/positions")! public List<Position> getPositions(Principal p) {! Portfolio portfolio = ...! return portfolio.getPositions();! }!} sent directly to the client, not going
though the message broker
• User specific queues can be used
• /user/** kind of paths
• queues with unique id will be created
• Useful to send user related information or errors
User destinations
client.subscribe("/user/queue/private-messages", function(msg) {! // ...!});!!client.subscribe("/user/queue/errors", function(msg) {! // ...!});!
Sending to user@Controller!public class ChatController {!! @MessageMapping("/message")! @SendToUser! public String sendMessage(String message) {! return message.toUpperCase();! }!! @MessageExceptionHandler! @SendToUser("/queue/errors")! public String handleException(IllegalStateException ex) {! return ex.getMessage();! }!!}!
Will be sent to /user/{username}/queue/message
• Sending messages without handler methods
SimpMessagingTemplate
template.convertAndSend("/topic/chat", message;!template.convertAndSendToUser(user, dest, message;
• Previous configuration used Spring’s simple broker
• Not suitable for clustering
• Subset of STOMP
• A message broker can be plugged-in instead
Using a Message Broker
Message Broker Config
@Configuration!@EnableWebSocketMessageBroker!public class Config implements WebSocketMessageBrokerConfigurer{!! @Override! public void configureMessageBroker(MessageBrokerConfigurer c){! c.enableStompBrokerRelay("/queue/", "/topic/");! c.setApplicationDestinationPrefixes("/app");! }!}
Thanks! @sergialmar