deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Upload: spring-io

Post on 13-Jul-2015

1.721 views

Category:

Documents


1 download

TRANSCRIPT

Page 1: 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

Page 2: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 3: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

3

Every web developer should know about real-time web

Page 4: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 5: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 6: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 7: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 8: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

8

Understanding the Workflow

Page 9: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

The Workflow (simplified)

9

clientInboudChannel

clientOutboundChannel

WebSocket frame

WebSocket Endpoint

sends message

Message Handlers

processed by

sends to

WebSocket frame

Page 10: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 11: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 12: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

Endpoint Configuration

@Configurationpublic class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport { public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(“/ws") } ...}

12

.withSockJS();

Use SockJS for fallback options

Page 13: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

13

Page 14: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 15: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 16: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 17: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 18: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 19: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 20: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 21: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 22: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 23: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 24: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

24

Page 25: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

Broker Destination Messages

25

• Two options: • Use the built-in broker • Use a full-blown message broker

BrokerMessage Handler

clientInboudChannelSimpAnnotation MethodMessage

HandlerbrokerChannel

Page 26: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 27: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 28: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 29: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 30: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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)

Page 31: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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 !

Page 32: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

32

Page 33: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 34: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

34

Page 35: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

What’s new in Spring 4.1

35

Page 36: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

36

WebSocket Scope

Page 37: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 38: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 39: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

39

Page 40: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

Performance & Monitoring

40

Page 41: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

The Channels

!• Both are ExecutorSubscribableChannels,

but who is backing them?

41

clientInboudChannel

clientOutboundChannel

Page 42: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 43: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 44: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 45: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

SockJS Java Client

45

Page 46: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 47: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

WebSocket Security (new in Spring Security 4)

47

Page 48: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 49: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

Security Message Flow

49

clientInboudChannelnew message

SecurityContextChannelInterceptor ChannelSecurityInterceptor

MessageHandler

AccessDecision Manager

delegates

* same applies to outbound messages sent to the clientOutboundChannel

SecurityContext

sets

pollsMessageExpressionVoter

Page 50: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 51: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 52: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

52

Page 53: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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!

Page 54: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 55: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

Testing WebSockets

55

Page 56: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

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

Page 57: Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

57

Thank you! @sergialmar