Deep dive into_spring_web_sockets_ sergi_almar_springone2gx_2014

Post on 13-Jul-2015

1.721 views 1 download

Tags:

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