Spring 4.0 provides support for WebSocket communications, including:
- Low-level API for sending and receiving messages;
- Advanced API for sending and receiving messages;
- The template used to send the message;
- Support SOCKJS to solve the problem of browser side, server and agent not supporting WebSocket.
1 using spring's low-level websocket API
In its simplest form, websocket is just a channel for communication between two applications. An app at one end of the websocket sends a message, and the other end processes the message. Because it is full-duplex, messages can be sent and processed at each end. As shown in 18.1.
WebSocket communication can be applied to any type of application, but the most common scenario for WebSocket is to implement communication between server and browser-based applications.
To use the lower-level API to process messages in spring, we must write a class that implements Websockethandler. Websockethandler requires us to implement five methods. The simpler way to implement Websockethandler directly is to extend the Abstractwebsockethandler, which is an abstract implementation of Websockethandler.
public class MarcoHandler extends AbstractWebSocketHandler { private static final Logger logger = LoggerFactory.getLogger(MarcoHandler.class); @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { logger.info("Received message: " + message.getPayload()); Thread.sleep(2000); session.sendMessage(new TextMessage("Polo!")); }}
In addition to the five methods defined in the overloaded Websockethandler, we can also overload the three methods defined in Abstractwebsockethandler:
- Handlebinarymessage ()
- Handlepongmessage ()
- Handletextmessage ()
These three methods are only embodiments of the Handlemessage () method, and each method corresponds to a particular type of message.
Another option, we can extend Textwebsockethandler or binarywebsockethandler. Textwebsockethandler is a subclass of Abstractwebsockethandler, and it refuses to process binary messages. It overloads the Handlebinarymessage () method and closes the WebSocket connection if a binary message is received. Similarly, Binarywebsockethandler is also a subclass of Abstractweb-sockethandler, which overloads the Handletextmessage () method and closes the connection if a text message is received.
Now that we have the message handler class, we have to configure it so that spring can forward the message to it. In spring's Java configuration, this requires using @enablewebsocket on a configuration class and implementing the Websocketconfigurer interface, as shown in the following program manifest.
Listing 18.2 In the Java configuration, enable WebSocket and map the message processor
@Configuration@EnableWebSocketpublic class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {// registry.addHandler(marcoHandler(), "/marco").withSockJS(); registry.addHandler(marcoHandler(), "/marco"); } @Bean public MarcoHandler marcoHandler() { return new MarcoHandler(); }}
or XML configuration:
Program listing 18.3 is configured in XML with the WebSocket namespace WebSocket
This is the desired configuration, regardless of whether you use Java or XML.
Now, we can turn our attention to the client, which will send "marco!" Text messages to the server, and listen for text messages from the server. The JavaScript code shown in the following list of programs opens a raw websocket and uses it to send messages to the server.
Listing 18.4 JavaScript client connected to "Marco" WebSocket
By sending "marco!", this endless Marco Polo game begins, because the server-side Marcohandler as a response will "polo!" Sent back, when the client receives a message from the server, the OnMessage event sends another "marco!" to the server. This process will continue until the connection is closed.
2 dealing with scenarios that do not support websocket
WebSocket is a relatively new specification. Although it was normalized as early as the end of 2011, there was still no consistent support on Web browsers and application servers. Firefox and Chrome have already fully supported WebSocket, but some other browsers are just beginning to support WebSocket. Here are a few popular browsers that support the minimum version of the WebSocket feature:
- Internet explorer:10.0
- firefox:4.0 (partial support), 6.0 (full support).
- chrome:4.0 (partial support), 13.0 (full support).
- safari:5.0 (partial support), 6.0 (full support).
- opera:11.0 (partial support), 12.10 (full support).
- IOS safari:4.2 (partially supported), 6.0 (full support).
- Android browser:4.4.
Server-side support for WebSocket is also not a better place to go. GlassFish started supporting a certain form of websocket a few years ago, but many other application servers have just begun to support WebSocket in recent releases. For example, when I was testing the example above, I was using the release candidate build of Tomcat 8.
Even if the browser and application server versions are compliant and both sides support websocket, there may be a problem between the two. The firewall proxy typically restricts all traffic except HTTP. They may not be supported or (also) not configured to allow WebSocket communication.
Fortunately, the mention of WebSocket's alternatives is exactly what SOCKJS is good at. SOCKJS allows us to use a unified programming model, as if the WebSocket is fully supported at all levels, and SOCKJS provides alternatives at the bottom.
For example, in order to enable SOCKJS communication on the server side, we can simply ask to add this feature in the spring configuration. Revisit the Registerwebsockethandlers () method in Listing 18.2 and add a little bit of content to enable SOCKJS:
@Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(marcoHandler(), "/marco").withSockJS(); }
- The XML accomplishes the same configuration effect:
To use SOCKJS on the client side, you need to ensure that the SOCKJS client library is loaded. The specific approach relies heavily on using a JavaScript module loader (such as require.js or curl.js) or simply <script>
loading the JavaScript library with tags. The simplest way to load the SOCKJS client library is to <script>
load it from the SOCKJS CDN using a tag, as shown below:
<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
In addition to loading the SOCKJS client library, in program listing 18.4, you only need to modify two lines of code to use SOCKJS:
var url = ‘marco‘;var sock = new SocktJS(url);
The first modification you make is a URL. The URL that sockjs handles is "http:/" or "https://" mode, not "ws://" and "wss://". Even so, we can use relative URLs to avoid writing full, fully qualified URLs. In this case, if the page containing JavaScript is located under the "Http://localhost:8080/websocket" path, the given "Marco" path will be formed to "http://localhost:8080/ Websocket/marco "Connection.
3 Using STOMP messages
Using WebSocket (or SOCKJS) directly is similar to using TCP sockets to write Web applications. Because there is no high-level line protocol (wire protocol), we need to define the semantics of messages sent between applications, and we need to ensure that both sides of the connection can follow these semantics.
However, the good news is that we do not have to use the native WebSocket connection. Just as HTTP adds a request-response model layer above a TCP socket, STOMP provides a frame-based line format (frame-based wire format) layer above WebSocket to define the semantics of the message.
At first glance, the STOMP message format is very similar to the structure of the HTTP request. Similar to HTTP requests and responses, stomp frames consist of commands, one or more header information, and the load. For example, here is a stomp frame that sends the data:
SENDdestination:/app/marcocontent-length:20{\"message\":\"Marco!\"}
3.1 Enabling the Stomp message feature
Add the @messagemapping annotation to the controller method in spring MVC to handle the stomp message, which is very similar to how the HTTP request is handled by a method with @requestmapping annotations. But unlike @requestmapping,
- The functionality of the @MessageMapping cannot be enabled through @enablewebmvc, but is @enablewebsocketmessagebroker.
- Spring's web messaging functionality is built on the message broker, so there is something else that needs to be configured in addition to telling spring that we want to process the message.
@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketStompConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/marcopolo").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) {// registry.enableStompBrokerRelay("/queue", "/topic"); registry.enableSimpleBroker("/queue", "/topic"); registry.setApplicationDestinationPrefixes("/app"); }}
The above configuration, which overloads the Registerstompendpoints () method, registers "/marcopolo" as the stomp endpoint. This path differs from the destination path where the message was previously sent and received. This is an endpoint that the client connects to before subscribing to or publishing the message to the destination path.
Websocketstompconfig also configures a simple message agent by overloading the Configuremessagebroker () method. The message agent will handle messages prefixed with "/topic" and "/queue". In addition, messages destined for the application will be prefixed with a "/app". Figure 18.2 shows the message flow in this configuration.
Enable Stomp Agent relay
For applications in a production environment, you might want to support websocket messages, such as RABBITMQ or ACTIVEMQ, with proxies that really support stomp. Such proxies provide better extensibility and robustness for message functionality, but they also support the Stomp command fully. We need to build an agent for stomp according to the relevant documentation. Once you are ready, you can use the Stomp proxy to replace the memory agent, simply by overloading the Configuremessagebroker () method as follows:
@Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableStompBrokerRelay("/queue", "/topic"); registry.setApplicationDestinationPrefixes("/app"); }
The first line of the Configuremessagebroker () method above enables the Stomp Agent relay (Broker relay) feature and sets its destination prefix to "/topic" and "/queue". In this case, spring will know that all messages destined for "/topic" or "/queue" will be sent to the Stomp proxy.
Set the applied prefix to "/app" in the Configuremessagebroker () method in the second row. All messages that begin with "/app" in all destinations will be routed to the method with the @messagemapping annotation, not to the agent queue or topic.
By default, the Stomp Agent Relay assumes that the agent listens on port 61613 for localhost, and that both the client's username and password are "guest". If your Stomp agent is located on another server, or if it is configured as a different client credential, then we may need to configure these details when enabling Stomp Agent relay:
@Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableStompBrokerRelay("/queue", "/topic") .setRelayHost("rabbit.someotherserver") .setRelayPort(62623) .setClientLogin("marcopolo") .setClientPasscode("letmein01") registry.setApplicationDestinationPrefixes("/app"); }
3.2 Processing of STOMP messages from clients
Spring 4.0 introduces the @messagemapping annotation, which is used for the processing of stomp messages, similar to the @requestmapping annotations of spring MVC. When a message arrives at a particular destination, the method with @messagemapping annotations can process the messages.
@Controllerpublic class MarcoController { private static final Logger logger = LoggerFactory .getLogger(MarcoController.class); @MessageMapping("/marco") public Shout handleShout(Shout incoming) { logger.info("Received message: " + incoming.getMessage()); try { Thread.sleep(2000); } catch (InterruptedException e) {} Shout outgoing = new Shout(); outgoing.setMessage("Polo!"); return outgoing; }}
The Handleshout () method can handle messages arriving at a specified destination. In this case, the destination is "/app/marco" (the "/app" prefix is implied, as we configure it as the destination prefix for the app).
- The Shout class is a simple javabean.
public class Shout { private String message; public String getMessage() { return message; } public void setMessage(String message) { this.message = message; }}
Because we are not dealing with HTTP now, we cannot use spring's Httpmessageconverter implementation to convert the load to a shout object. Spring 4.0 provides several message converters as part of their messaging API. Table 18.1 Describes these message converters, which may be used when processing stomp messages.
Table 18.1 Spring can use a message converter to convert a message payload to a Java type
Processing Subscriptions
The main application scenario for @SubscribeMapping is to implement the request-response pattern. In Request-response mode, the client subscribes to a destination and then expects to get a one-time response at that destination.
For example, consider the following method for @subscribemapping annotation annotations:
@SubscribeMapping({"/marco"}) public Shout handleSubscription(){ Shout outgoing = new Shout(); outgoing.setMessage("Polo!"); return outgoing; }
As you can see, the Handlesubscription () method uses the @subscribemapping annotation, which is used to process subscriptions to "/app/marco" destinations (similar to @messagemapping, "/app" is implied). When this subscription is processed, the Handlesubscription () method produces an output shout object and returns it. The Shout object is then converted to a message and is sent back to the client at the same destination as the client subscription.
If you think this request-response mode is not very different from the request-response mode of HTTP GET, then you are basically right. However, the key difference here is that the HttpGet request is synchronous, and the request-response pattern of the subscription is asynchronous so that the client can process the response when it is available, without having to wait.
writing a JavaScript client
Program listing 18.7 using the Stomp Library to send messages via JavaScript
In this case, the URL refers to the stomp endpoint (not including the app's context path "/stomp") that is configured in program listing 18.5.
However, the difference here is that we no longer use SOCKJS directly, but instead create a STOMP client instance by calling Stomp.over (sock). This actually encapsulates the sockjs so that the stomp message can be sent on the WebSocket connection.
3.3 Sending a message to the client
WebSocket is generally considered as a way for the server to send data to the browser, and the data sent in this way does not have to be in response to the HTTP request. With spring and websocket/stomp, how do you communicate with a browser-based client?
Spring provides two ways to send data to the client:
- As an accompanying result of processing a message or processing a subscription;
- Use the message template.
After the message is processed, the message is sent
@MessageMapping("/marco") public Shout handleShout(Shout incoming) { logger.info("Received message: " + incoming.getMessage()); Shout outgoing = new Shout(); outgoing.setMessage("Polo!"); return outgoing; }
When the @messagemapping annotated method has a return value, the returned object is converted (via a message converter) and placed into the payload of the stomp frame and sent to the message agent.
By default, the destination that the frame is destined to is the same as the destination that triggered the processor method, except that the "/topic" prefix is added. In this case, this means that the shout object returned by the Handleshout () method is written to the payload of the Stomp frame and published to the "/topic/marco" destination. However, we can overload the destination by adding @sendto annotations to the method:
@MessageMapping("/marco")@SendTo("/topic/shout") public Shout handleShout(Shout incoming) { logger.info("Received message: " + incoming.getMessage()); Shout outgoing = new Shout(); outgoing.setMessage("Polo!"); return outgoing; }
Following this @sendto annotation, the message will be published to "/topic/shout". All applications that subscribe to this topic, such as clients, will receive this message.
In a similar way, @SubscribeMapping annotation annotations can send a message as a response to the subscription.
@SubscribeMapping("/marco") public Shout handleSubscription(){ Shout outgoing = new Shout(); outgoing.setMessage("Polo!"); return outgoing; }
The difference between the @SubscribeMapping is that the shout message will be sent directly to the client without having to go through the message agent. If you add a @sendto annotation to the method, the message will be sent to the specified destination, which will go through the proxy.
send a message anywhere in the app
@MessageMapping and @subscribemapping provide a very simple way to send a message, which is the accompanying result of receiving a message or processing a subscription. However, Spring's simpmessagingtemplate is able to send messages anywhere in the app, even without having to first receive a message as a precondition.
Instead of requiring the user to refresh the page, let the homepage subscribe to a stomp theme that will receive a live feed of spittle updates when spittle is created. In the home page, we need to add the following JavaScript code block:
The handlebars library renders the spittle data as HTML and inserts it into the list. The handlebars template is defined in a separate <script>
label, as follows:
On the server side, we can use Simpmessagingtemplate to publish all newly created spittle as messages to the "/topic/spittlefeed" topic. The spittlefeedserviceimpl shown in the following list of programs is a simple service that implements this function:
Program manifest 18.8 Simpmessagingtemplate can post messages anywhere in the app
@Servicepublic class SpittleFeedServiceImpl implements SpittleFeedService { private SimpMessageSendingOperations messaging; @Autowired public SpittleFeedServiceImpl(SimpMessageSendingOperations messaging) { this.messaging = messaging; } public void broadcastSpittle(Spittle spittle) { messaging.convertAndSend("/topic/spittlefeed", spittle); }}
In this scenario, we want all the clients to be able to see the real-time spittle feed in a timely manner, which is a good practice. But sometimes, we want to send a message to the specified user, not all clients.
4 sending messages to target users
However, if you know who the user is, you can handle the message associated with a user, not just with all clients. The good news is that we've learned how to identify users. By using the same authentication mechanism as in chapter 9th, we can use spring security to authenticate the user and process the message for the target user.
When using the spring and stomp messaging features, we have three ways to take advantage of authenticated users:
- @MessageMapping and @subscribemapping labeling methods can use principal to obtain authenticated users;
- The values returned by the @MessageMapping, @SubscribeMapping, and @messageexception methods can be sent as messages to authenticated users;
- Simpmessagingtemplate can send messages to specific users.
4.1 Handling users ' messages in the controller
In the @messagemapping or @subscribemapping method of a controller, there are two ways to understand user information when processing messages. In the processor method, by simply adding a principal parameter, this method can know who the user is and use that information to focus on the user-related data. In addition, the processor method can also use the @sendtouser annotation to indicate that its return value is to be sent as a message to a client of an authenticated user (only to that client).
@MessageMapping("/spittle") @SendToUser("/queue/notifications") public Notification handleSpittle(Principal principal, SpittleForm form) { Spittle spittle = new Spittle(principal.getName(), form.getText(), new Date()); spittleRepo.save(spittle); feedService.broadcastSpittle(spittle); return new Notification("Saved Spittle for user: " + principal.getName()); }
JavaScript Client code:
stomp.subscribe("/user/queue/notifications", handleNotification);
Internally, destinations prefixed with "/user" will be processed in a special way. This message is not handled by Annotationmethodmessagehandler (as with the application message). Nor will it be handled by Simplebrokermessagehandler or Stompbrokerrelaymessagehandler (as in the case of proxy messages) to "/user" The message that is prefixed will be processed by Userdestinationmessagehandler, as shown in 18.4.
!!!
4.2 Sending a message to a specified user
In addition to Convertandsend (), Simpmessagingtemplate also provides a Convertandsendtouser () method. By name you can tell that the Convertandsendtouser () method allows us to send a message to a specific user.
To illustrate this feature, we want to add a feature to the Spittr app that will alert the user when another user submits a spittle that mentions a user. For example, if the spittle text contains "@jbauer", then we should send a message to the client that is logged on with the "Jbauer" username. The Broadcastspittle () method in the following list of programs uses Convertandsendtouser () to alert the user to the conversation.
@Servicepublic class SpittleFeedServiceImpl implements SpittleFeedService { private SimpMessagingTemplate messaging; private Pattern pattern = Pattern.compile("\\@(\\S+)"); @Autowired public SpittleFeedServiceImpl(SimpMessagingTemplate messaging) { this.messaging = messaging; } public void broadcastSpittle(Spittle spittle) { messaging.convertAndSend("/topic/spittlefeed", spittle); Matcher matcher = pattern.matcher(spittle.getMessage()); if (matcher.find()) { String username = matcher.group(1); messaging.convertAndSendToUser(username, "/queue/notifications", new Notification("You just got mentioned!")); } }}
In Broadcastspittle (), if the message for a given Spittle object contains content similar to the user name (that is, text that begins with "@"), a new notification is sent to the name "/queue/ Notifications "on the destination. Therefore, if the spittle contains "@jbauer", the notification will be sent to the "/user/jbauer/queue/notifications" destination.
5 Handling Message Exception source code
Https://github.com/myitroad/spring-in-action-4/tree/master/Chapter_18
List of attachments
- Code18-3.jpg
- Code18-4.jpg
- Code18-7.jpg
- Img18-1.jpg
- Img18-2.jpg
- Img18-4.jpg
- Jssubcrbtopic.jpg
- Spittletmplt.jpg
- Table18-1.jpg
- Xmlconfsockjs.jpg
18th-using websocket and stomp to implement message functions