Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014
Transcript of Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014
© 2014 SpringOne 2GX. All rights reserved. Do not distribute without permission.
Deep-dive into Spring WebSocketsSergi Almar @sergialmar
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
Every web developer should know about real-time web
The Lifecycle
4
browser serverHTTPcan we upgrade to WebSocket? I want you to talk to me as well!
HTTP 101Yes! Talk WebSocket to me!
WebSockethere’s a frame
WebSockethere’s a frame
WebSockethere’s a frame
close connection
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
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
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^@
8
Understanding the Workflow
The Workflow (simplified)
9
clientInboudChannel
clientOutboundChannel
WebSocket frame
WebSocket Endpoint
sends message
Message Handlers
processed by
sends to
WebSocket frame
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
clientInboudChannelWebSocket
frameWebSocket
Endpointsends
message
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) { ... }}
Endpoint Configuration
@Configurationpublic class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport { public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(“/ws") } ...}
12
.withSockJS();
Use SockJS for fallback options
DEMO - Tracking user presence with events https://github.com/salmar/spring-websocket-chat
13
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
clientInboudChannel
clientOutboundChannel
Message Handlers
Destination Configuration
@Configurationpublic class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport { ... @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/queue/", "/topic/"); registry.setApplicationDestinationPrefixes("/app"); }}
15
Broker destinations
Application destinations
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
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
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"}
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
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
clientInboudChannelSimpAnnotation MethodMessage
Handler
/app/chat.message
brokerChannel
/topic/chat.message
Return value wrapped into a message
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;}
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();}
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@Controllerpublic interface WebSocketGateway {! @MessageMapping(“/chat.message") @SendToUser("/queue/answer") @Gateway(requestChannel = "messageChannel") String filterMessage(String payload);!}
clientInboudChannel WebSocket Gateway
messageChannel
DEMO Application Destinations https://github.com/salmar/spring-websocket-chat
24
Broker Destination Messages
25
• Two options: • Use the built-in broker • Use a full-blown message broker
BrokerMessage Handler
clientInboudChannelSimpAnnotation MethodMessage
HandlerbrokerChannel
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"); }
Full Blown STOMP Broker
• Brokers with STOMP support: RabbitMQ, Apache ActiveMQ, HornetQ, OpenMQ…
27
@Autowiredprivate Environment env; @Overridepublic 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"));}
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
StompBrokerRelayMessageHandler
clientInboudChannel
brokerChannel
clientOutboundChannel
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
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)
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 !
DEMO Broker Destinations https://github.com/salmar/spring-websocket-chat
32
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
DEMO User Destinations https://github.com/salmar/spring-websocket-chat
34
What’s new in Spring 4.1
35
36
WebSocket Scope
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(); ...}
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() { ... }}
DEMO WebSocket Scoped Beans https://github.com/salmar/spring-websocket-chat
39
Performance & Monitoring
40
The Channels
!• Both are ExecutorSubscribableChannels,
but who is backing them?
41
clientInboudChannel
clientOutboundChannel
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
@Configurationpublic 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); } ...}
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);}
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; }
SockJS Java Client
45
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“);
WebSocket Security (new in Spring Security 4)
47
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
Security Message Flow
49
clientInboudChannelnew message
SecurityContextChannelInterceptor ChannelSecurityInterceptor
MessageHandler
AccessDecision Manager
delegates
* same applies to outbound messages sent to the clientOutboundChannel
SecurityContext
sets
pollsMessageExpressionVoter
WebSocket Security Configuration
@Configurationpublic 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
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
Spring Session https://github.com/spring-projects/spring-session
52
The Challenge
53
browser serverstart 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!
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
Testing WebSockets
55
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
57
Thank you! @sergialmar