Slide 1

Slide 1 text

© 2014 SpringOne 2GX. All rights reserved. Do not distribute without permission. Deep-dive into Spring WebSockets Sergi Almar @sergialmar

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

3 Every web developer should know about real-time web

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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^@

Slide 8

Slide 8 text

8 Understanding the Workflow

Slide 9

Slide 9 text

The Workflow (simplified) 9 clientInboudChannel clientOutboundChannel WebSocket frame WebSocket 
 Endpoint sends message Message
 Handlers processed by sends to WebSocket frame

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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 { ! public void onApplicationEvent(SessionConnectedEvent event) { ... } }

Slide 12

Slide 12 text

Endpoint Configuration @Configuration public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport { public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(“/ws") } ... } 12 .withSockJS(); Use SockJS for fallback options

Slide 13

Slide 13 text

DEMO - Tracking user presence with events https://github.com/salmar/spring-websocket-chat 13

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Subscription handling 17 @SubscribeMapping("/chat.participants") public Collection retrieveParticipants() { return participantRepository.getActiveSessions().values(); } stompClient.subscribe(“/app/chat.participants", callback); SUBSCRIBE id:sub-1 destination:/app/chat.participants

Slide 18

Slide 18 text

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"}

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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; }

Slide 22

Slide 22 text

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(); }

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

DEMO Application Destinations https://github.com/salmar/spring-websocket-chat 24

Slide 25

Slide 25 text

Broker Destination Messages 25 • Two options: • Use the built-in broker • Use a full-blown message broker BrokerMessage Handler clientInboudChannel SimpAnnotation MethodMessage Handler brokerChannel

Slide 26

Slide 26 text

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"); }

Slide 27

Slide 27 text

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")); }

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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)

Slide 31

Slide 31 text

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 !

Slide 32

Slide 32 text

DEMO Broker Destinations https://github.com/salmar/spring-websocket-chat 32

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

DEMO User Destinations https://github.com/salmar/spring-websocket-chat 34

Slide 35

Slide 35 text

What’s new in Spring 4.1 35

Slide 36

Slide 36 text

36 WebSocket Scope

Slide 37

Slide 37 text

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 attrs = headerAccessor.getSessionAttributes(); ... }

Slide 38

Slide 38 text

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() { ... } }

Slide 39

Slide 39 text

DEMO WebSocket Scoped Beans https://github.com/salmar/spring-websocket-chat 39

Slide 40

Slide 40 text

Performance & Monitoring 40

Slide 41

Slide 41 text

The Channels ! • Both are ExecutorSubscribableChannels,
 
 
 but who is backing them? 41 clientInboudChannel clientOutboundChannel

Slide 42

Slide 42 text

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); } ... }

Slide 43

Slide 43 text

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); }

Slide 44

Slide 44 text

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; }

Slide 45

Slide 45 text

SockJS Java Client 45

Slide 46

Slide 46 text

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 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“);

Slide 47

Slide 47 text

WebSocket Security (new in Spring Security 4) 47

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

Handler Method Security @PreAuthorize("hasRole('ROLE_ADMIN')") @SubscribeMapping(“/admin.notifications") public List getNotifications() { ... } 51 Protects Subscription @PreAuthorize("hasRole('ROLE_ADMIN')") @MessageMapping(“/admin.cancel") public void cancelNotifications() { ... } Protects Messages

Slide 52

Slide 52 text

Spring Session https://github.com/spring-projects/spring-session 52

Slide 53

Slide 53 text

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!

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

Testing WebSockets 55

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

57 Thank you! @sergialmar