Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Deep Dive Into Spring WebSockets

Sergi Almar
September 09, 2014

Deep Dive Into Spring WebSockets

WebSocket support has been part of Spring since 4.0. In this session we'll review the architecture behind it and we'll try to show how to build highly scalable websocket based applications. We will also cover the new features in Spring 4.1, the new Spring Security 4 support for WebSockets and the different testing approaches.

Sergi Almar

September 09, 2014
Tweet

Other Decks in Programming

Transcript

  1. © 2014 SpringOne 2GX. All rights reserved. Do not distribute

    without permission. Deep-dive into Spring WebSockets Sergi Almar @sergialmar
  2. Agenda • Intro to WebSockets • Understanding the workflow •

    What’s new in Spring 4.1 • WebSocket scope • SockJs Java Client • Performance & Monitoring • Spring Session • WebSocket Security • Testing WebSockets 2
  3. The Lifecycle 4 browser server HTTP can we upgrade to

    WebSocket? I want you to talk to me as well! HTTP 101 Yes! Talk WebSocket to me! WebSocket here’s a frame WebSocket here’s a frame WebSocket here’s a frame close connection
  4. The Handshake • Handshake upgrades the connection from HTTP to

    WebSocket • Why don’t we start directly with TCP? • uses same ports as HTTP/S 80 and 443 (ws: wss:) • goes through firewalls 5 GET /ws HTTP/1.1 
 Host: springone2gx.com 
 Upgrade: websocket 
 Connection: Upgrade 
 Sec-WebSocket-Key: VeYGnqa/NEA6AgTGKhtjaA== 
 Sec-WebSocket-Protocol: mqtt 
 Sec-WebSocket-Version: 13 
 Origin: http://springone2gx.com HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: upgrade Sec-WebSocket-Accept: Ht/cfI9zfvxplkh0qIsssi746yM= Sec-WebSocket-Protocol: mqtt
  5. Spring WebSockets • Why should we use Spring WebSockets instead

    of plain JSR-356? • Fallback options with SockJS • Support for STOMP subprotocol • No security • Integration with messaging components and Spring programming style ! • This presentation focuses on STOMP over WebSocket • See Rossen’s presentation from s2gx 2013 to know more about WebSocket protocol and API 6
  6. STOMP • Simple interoperable protocol for asynchronous messaging • coming

    from the HTTP school of design • Communication between client and server is through a ‘frame’ • Client frames: • SEND, SUBSCRIBE / UNSUBSCRIBE, ACK / NACK • Server frames: • MESSAGE, RECEIPT, ERROR 7 COMMAND header1:value1 header2:value2 ! Body^@
  7. The Workflow (simplified) 9 clientInboudChannel clientOutboundChannel WebSocket frame WebSocket 


    Endpoint sends message Message
 Handlers processed by sends to WebSocket frame
  8. The WebSocket Endpoint • Bridges JSR 356 (Java API for

    WebSocket) to Spring WebSocketHandlers • Finds the right sub protocol handler • if no subprotocol is defined, STOMP is used • Decodes the message • Sends the message to the clientInboundChannel • Publishes application events 10 clientInboudChannel WebSocket frame WebSocket 
 Endpoint sends message
  9. Application Events • SessionConnectEvent, SessionConnectedEvent, SessionDisconnectEvent • (warning: the last

    one may be published more than once per session) • SessionSubscribeEvent, SessionUnsubscribeEvent 11 public class NewConnectionListener implements ApplicationListener<SessionConnectedEvent> { ! public void onApplicationEvent(SessionConnectedEvent event) { ... } }
  10. Endpoint Configuration @Configuration public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport { public

    void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(“/ws") } ... } 12 .withSockJS(); Use SockJS for fallback options
  11. Message Handlers • SimpAnnotationMethodMessageHandler • processes messages to application destinations

    • SimpleBrokerMessageHandler • built-in STOMP broker processing broker destinations • StompBrokerRelayMessageHandler • forwards messages to a full blown STOMP broker • UserDestinationMessageHandler • handles messages sent to users (with the /user prefix) 14 clientInboudCh annel clientOutboundCh annel Message
 Handlers
  12. Destination Configuration @Configuration public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport { ...

    @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/queue/", "/topic/"); registry.setApplicationDestinationPrefixes("/app"); } } 15 Broker destinations Application destinations
  13. Application Destination Messages 16 • Messages sent to application destinations

    will be processed by SimpAnnotationMethodMessageHandler • Delegates to message handler methods defined in @Controller or @RestController classes • Flexible signatures like request handling methods • expected arguments: Message, @Payload, @Header, @Headers, MessageHeaders, HeaderAccessor, @DestinationVariable, Principal
  14. Subscription handling 17 @SubscribeMapping("/chat.participants") public Collection<LoginEvent> retrieveParticipants() { return participantRepository.getActiveSessions().values();

    } stompClient.subscribe(“/app/chat.participants", callback); SUBSCRIBE id:sub-1 destination:/app/chat.participants
  15. Message handling 18 @MessageMapping("/chat.message") public ChatMessage filterMessage(@Payload ChatMessage message, Principal

    principal) { checkProfanityAndSanitize(message); return message; } stompClient.send(‘/app/chat.message’, {}, JSON.stringify({message: ‘hi there’})); SEND destination:/app/chat.message content-length:22 ! {“message": "hi there"}
  16. Message Converters • Message converters are used for method arguments

    and return values. Built-in: • StringMessageConverter • ByteArrayMessageConverter • MappingJackson2MessageConverter • (requires Jackson lib in the classpath) 19 @MessageMapping(“/chat.message") public ChatMessage filterMessage(@Payload ChatMessage message, Principal principal) { ... } JSON payload to ChatMessage Will be converted to JSON
  17. Handler Method Response • SimpAnnotationMethodMessageHandler doesn’t know about STOMP semantics

    • Return values are sent to a STOMP message broker via the brokerChannel (doesn’t apply to @SubscriptionMapping) • can be the built-in simple broker • or a full blown message broker 20 clientInboudChannel SimpAnnotation MethodMessage Handler /app/chat.message brokerChannel /topic/chat.message Return value wrapped into a message
  18. Overriding Response Destinations • Use @SendTo to override destination to

    send the response to • @SubscribeMapping returns directly to the client, but will send response to the broker if annotation used @MessageMapping("/chat.message") @SendTo("/topic/chat.filtered") public ChatMessage filterMessage(@Payload ChatMessage message, Principal principal) { ... return message; }
  19. Handling Exceptions • Similar to @ExceptionHandler in Spring MVC •

    Method signature similar to message handling methods • response sent to /topic/destination by default • override with @SentTo or @SentoToUser • Use @ControllerAdvice to define global exception handlers 22 @MessageExceptionHandler @SendToUser("/queue/errors") public String handleProfanity(TooMuchProfanityException e) { return e.getMessage(); }
  20. Spring Integration 4.1 Support • Spring Integration 4.1 adds WebSocket

    inbound and outbound adapters • Let’s you use a gateway to forward the processing to SI 23 @MessagingGateway @Controller public interface WebSocketGateway { ! @MessageMapping(“/chat.message") @SendToUser("/queue/answer") @Gateway(requestChannel = "messageChannel") String filterMessage(String payload); ! } clientInboudChannel WebSocket Gateway messageChannel
  21. Broker Destination Messages 25 • Two options: • Use the

    built-in broker • Use a full-blown message broker BrokerMessage Handler clientInboudChannel SimpAnnotation MethodMessage Handler brokerChannel
  22. The Simple Broker • Built-in broker, only supports a subset

    of STOMP commands • Stores everything in memory • SimpleBrokerMessageHandler will be subscribed to inboundClientChannel and brokerChannel 26 @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/queue/", "/topic/"); registry.setApplicationDestinationPrefixes("/app"); }
  23. Full Blown STOMP Broker • Brokers with STOMP support: RabbitMQ,

    Apache ActiveMQ, HornetQ, OpenMQ… 27 @Autowired private Environment env; @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableStompBrokerRelay("/queue/", "/topic/") .setRelayHost(env.getRequiredProperty("rabbitmq.host")) .setRelayPort(env.getRequiredProperty("rabbitmq.stompport", Integer.class)) .setSystemLogin(env.getRequiredProperty("rabbitmq.user")) .setSystemPasscode(env.getRequiredProperty("rabbitmq.password")) .setClientLogin(env.getRequiredProperty("rabbitmq.user")) .setClientPasscode(env.getRequiredProperty("rabbitmq.password")) .setVirtualHost(env.getRequiredProperty("rabbitmq.vh")); }
  24. STOMP Broker Relay • StompBrokerRelayMessageHandler will be subscribed to inboundClientChannel

    and brokerChannel • uses reactor-net to connect to the broker • forwards all messages to the broker 28 StompBrokerRelay MessageHandler clientInboudChannel brokerChannel clientOutboundChannel
  25. A Word On Destinations • The STOMP protocol treats destinations

    as opaque string and their syntax is server implementation specific • other protocols like MQTT define the segment spec (a slash) • Web developers are used to destinations separated by a slash (like /topic/chat/messages), but . is traditionally used in messaging • Simple broker is happy with / in destinations • Other brokers like RabbitMQ will not like it (standard segment separator is a dot) • Prefer dot notation in for STOMP destinations • /topic/chat.messages 29
  26. STOMP Broker connection failure • Heartbeat messages are constantly sent

    to the broker 30 WebSocket tcp • When the broker goes down, a notification is published • BrokerAvailabilityEvent (available = false) • Reconnection happens transparently when service is available • BrokerAvailabilityEvent (available = true)
  27. Client Disconnection • Heartbeat messages are also sent to the

    WebSocket client 31 WebSocket tcp • If there’s a disconnection (app goes down, connection outage…), reconnection doesn’t happen transparently • SockJS doesn’t provide auto-reconnection (other libraries like Socket.io do) • Client receives an error, needs to handle reconnection on error !
  28. User Destinations • UserDestinationMessageHandler processes user destinations • starting with

    /user • Subscribing to destinations like /user/queue/chat.message will be converted to unique destinations in the user session • something like /queue/chat.message-user2244 • Send to destinations like /user/{username}/queue/chat.message to send only to a specific user (may have more than one session) @MessageMapping(“/chat.checker") @SendToUser(value= “/chat.message.filtered”, broadcast = false) public ChatMessage filterMessage(@Payload ChatMessage message) { return message; } Targets only the session who sent the message
  29. WebSocket Scope • SimpAnnotationMethodMessageHandler exposes WebSocket session attributes in a

    header of a thread-bound object • use header accessor to get them 37 @MessageMapping(“/chat.message") public void filterMessage(SimpMessageHeaderAccessor headerAccessor) { Map<String, Object> attrs = headerAccessor.getSessionAttributes(); ... }
  30. WebSocket Scoped Beans • Scopes a bean definition to a

    WebSocket session • initialisation and destruction callbacks also work • @PostConstruct after DI, @PreDestroy when WebSocket session ends • Define it as a scoped proxy 38 @Component @Scope(value="websocket", proxyMode = ScopedProxyMode.TARGET_CLASS) public class SessionProfanity { ! @PostConstruct public void init() { ... } ! @PreDestroy public void destroy() { ... } }
  31. The Channels ! • Both are ExecutorSubscribableChannels,
 
 
 but

    who is backing them? 41 clientInboudChannel clientOutboundChannel
  32. Thread Pools • inboundMessageChannel backed by clientInboundChannelExecutor • increase number

    of threads if I/O bound operations performed • outboundMessageChannel backed by clientOutboundChannelExecutor • increase number of threads in case of slow clients • Both configured at AVAILABLE_PROCESSORS * 2 42 @Configuration public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport { public void configureClientInboundChannel(ChannelRegistration registration) { registration.taskExecutor().corePoolSize(Runtime.getRuntime().availableProcessors() * 4); } public void configureClientOutboundChannel(ChannelRegistration registration) { registration.taskExecutor().corePoolSize(Runtime.getRuntime().availableProcessors() * 4); } ... }
  33. Dealing with slow deliveries • clientOutboundChannel load is more unpredictable

    that the one in clientInboundChannel • Clients can also be slow consumers, if we cannot keep with the peace, messages will be buffered • You can configure how long you want to buffer these messages: • sendTimeLimit: max amount of time allowed when sending (def 10sec) • sendBufferSizeLimit: amount of data to buffer (0 to disable buffering) 43 public void configureWebSocketTransport(WebSocketTransportRegistration registration) { registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024); }
  34. WebSocketMessageBrokerStats • Logs stats info every 30 minutes • Expose

    it as a HTTP endpoint or via JMX 44 @Autowired private WebSocketMessageBrokerStats stats; @RequestMapping("/stats") public @ResponseBody WebSocketMessageBrokerStats showStats() { return stats; }
  35. SockJS Java Client • JavaScript client is not the only

    way to communicate with SockJS endpoints • Java client may be useful for server-to-server communication • supports websocket, xhr-streaming and xhr-polling transports • Also useful to simulate high volume of connections 46 List<Transport> transports = new ArrayList<>(2); transports.add(new WebSocketTransport(StandardWebSocketClient())); transports.add(new RestTemplateXhrTransport()); ! SockJsClient sockJsClient = new SockJsClient(transports); sockJsClient.doHandshake(new MyWebSocketHandler(), “ws://springone2gx.com/ws“);
  36. WebSocket Security • New spring-security-messaging module in Spring Security 4

    • Security applied via ChannelInterceptor, configured with: • Java config extending AbstractSecurityWebSocketMessageBrokerConfigurer • Spring Security annotations • Can be applied to Subscriptions and Messages 48
  37. Security Message Flow 49 clientInboudChannel new message SecurityContextChannelInterceptor ChannelSecurityInterceptor MessageHandler

    AccessDecision Manager delegates * same applies to outbound messages sent to the clientOutboundChannel SecurityContext sets polls MessageExpressionVoter
  38. WebSocket Security Configuration @Configuration public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    ! @Override protected void configure(MessageSecurityMetadataSourceRegistry messages) { messages .destinationMatchers("/user/queue/errors").permitAll() .destinationMatchers("/topic/admin/*").hasRole("ADMIN") .anyMessage().hasRole("USER"); ! } } 50 Protects Subscriptions and Messages with ROLE_ADMIN * just the initial support, will be improved in future releases
  39. Handler Method Security @PreAuthorize("hasRole('ROLE_ADMIN')") @SubscribeMapping(“/admin.notifications") public List<String> getNotifications() { ...

    } 51 Protects Subscription @PreAuthorize("hasRole('ROLE_ADMIN')") @MessageMapping(“/admin.cancel") public void cancelNotifications() { ... } Protects Messages
  40. The Challenge 53 browser server start HTTP session start WebSocket

    session WebSocket WebSocket Session closed HTTP Session expires We need to ping the server to maintain the HTTP session alive so the WebSocket session is not closed!
  41. Spring Session • Provides a common infrastructure to manage sessions

    • available to any environment • Features • clustering in a vendor neutral way (using Redis) • pluggable strategy for determining the session id • keeps the HttpSession alive when a WebSocket is active (no need to ping the server) • Not GA, current version 1.0.0.M1 54
  42. Types of Tests • Controller tests • Unit tests for

    Controllers • Out of the container integration testing • Use TestContext framework to load the context and send messages to clientInboundChannel • Setup minimum infrastructure (like SimpAnnotationMethodMessageHandler) and pass messages directly • End to end testing / Load testing • Run an embedded WebSocket server • Use the SockJS Java Client (not an end-to-end test for JS) • Can also simulate high volume of clients 56