摘要:本章介绍了带有Spring Boot的WebSockets,并描述了该技术如何帮助您跨应用实现消息传递,甚至跨应用程序的多个实例。在讨论web应用程序时,我们可以说REST是另一种进行消息传递的方式。在这一章中,我们将重点讨论一种有状态的通信方式,这就是WebSockets带来的内容。

一:WebSockets

WebSockets是一种允许双向通信的协议,通常在web浏览器中使用。这个协议首先使用一个握手(通常是一个HTTP请求),然后通过TCP发送一个基本的消息帧(一个协议开关)。WebSockets的想法是避免多个HTTP连接,如AJAX(XMLHttpRequest)或iframe和长轮询。见图7 - 1。

Spring Boot Messaging Chapter 7 Web Messaging_WebSockets

二:Using WebSockets with Spring

在我们讨论如何使用Spring引导的WebSockets之前,重要的是要知道并不是所有的浏览器都支持这种技术。访问http://caniuse.com/websockets,了解哪些浏览器是支持WebSockets。

Spring Boot Messaging Chapter 7 Web Messaging_WebSockets_02

其中绿色代表支持WebSockets技术

      Spring框架版本4包含一个支持WebSockets的新Spring websocket模块;它还兼容Java规范JSR-356。该模块还具有在必要时模拟WebSockets API的后备选项(请记住,并不是所有的浏览器都支持WebSockets)。它使用SockJS协议来完成这项任务。您可以在https://github.com/sockjs/sockjs-protocol中获得关于它的更多信息。

      同样值得一提的是,在最初的握手(HTTP,我们使用SockJS)之后,通信切换到TCP连接(这意味着您只发送了一个字节流,要么是文本,要么是二进制)。因此,您可以使用任何类型的消息传递架构,比如异步或事件驱动的消息传递。在这个级别上,您可以使用诸如STOMP(简单/流文本定向消息协议)这样的子协议,这允许您拥有更好的消息传递格式,客户端和服务器可以理解。见图7 - 2。

Spring Boot Messaging Chapter 7 Web Messaging_WebSockets_03

图7-2展示了如何使用Spring和客户端所需的组件来实现WebSockets。

2.1.Low-Level WebSockets

在我们进入回退选项(SockJS)和子协议(STOMP)之前,让我们看看如何使用Spring Boot实现一个低级别的WebSockets应用。

在本书的源代码中我们将使用websocket-demo项目。让我们从分析配置开始。为了使用低级的WebSockets通信,我们需要实现org.springframework.web.socket.config.annotation.WebSocketConfigurer接口,打开com.micai.spring.messaging.config.LlWebSocketConfig类,如清单7-1所示。

Listing 7-1. com.micai.spring.messaging.config.LlWebSocketConfig.java

@Configuration
@EnableWebSocket
public class LlWebSocketConfig implements WebSocketConfigurer {

@Autowired
private LlWebSocketHandler llWebSocketHandler;

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
webSocketHandlerRegistry.addHandler(llWebSocketHandler, "/llws");
}
}

清单7-1展示了在Spring中启用WebSockets所需的配置。请记住,我们配置这个类的方式是使用一个低级的WebSockets通信。让我们回顾一下使用的组件:

• @EnableWebSocket:  这对于启用和配置WebSockets请求是必要的。

• WebSocketConfigurer: 这是一个定义回调方法来配置WebSockets请求处理的接口。通常,您需要实现registerWebSocketHandlers方法。

• registerWebSocketHandlers:  这个方法需要通过添加将用于WebSockets处理请求的处理程序来实现。在这个方法中,我们注册一个处理程序(LlWebSocketHandler)实例,并传递用于握手和通信的终端地址。

• WebSocketHandlerRegistry:  这是一个用于注册WebSocketHandler实现的接口。我们将使用TextWebSocketHandler实现,并在下一节中查看它的代码。

 

正如您所看到的,使用Spring配置一个低级WebSockets是非常简单的。接下来,让我们打开com.micai.spring.messag.web.socket.LlWebSocketHandler类。参见清单7 - 2。

Listing 7-2. com.micai.spring.messaging.web.socket.LlWebSocketHandler.java

@Component
public class LlWebSocketHandler extends TextWebSocketHandler {

private static final Logger LOGGER = LoggerFactory.getLogger(LlWebSocketHandler.class);

@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
super.afterConnectionEstablished(session);
}

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
LOGGER.info(">>>> ", message);
}
}

清单7-2展示了我们将要用来接收来自客户端的消息的WebSockets handler。让我们来检查这个类:

• TextWebSocketHandler: 这是一个具体的类,通过AbstractWebSocketHandler类实现WebSocketHandler接口。此实现仅用于处理文本消息。我们将重写两种方法:afterConnectionEstablished和handleTextMessage。

• afterConnectionEstablished: 当客户端成功地使用WebSockets协议连接时,就调用此方法。在这个方法中,您可以使用WebSocketSession实例来发送或接收消息。现在,我们将使用它的默认行为,但是我们将在日志中看到更多(通过AOP 的WebSocketsAudit类)。

• handledTextMessage: 这个方法接收一个WebSocketSession(稍后我们将使用它)和一个TextMessage实例。TextMessage实例管理字节流并将它们转换成字符串。

 

到目前为止,我们已经实现了服务器端,但是客户端呢?通常,您将有一个web页面来完成客户端的工作。打开 src/main/resources/static/llws.html文件,如清单7-3所示。

Listing 7-3. Snippet of llws.html

<script>
$(function(){
var connection = new WebSocket('ws://localhost:8080/llws');
connection.onopen = function () {
console.log('Connected...');
};
connection.onmessage = function(event){
console.log('>>>>> ' + event.data);
var json = JSON.parse(event.data);
$("#output").append("<span><strong>" + json.user + "</strong>: <em>" + json.message +"</em></span><br/>");
};
connection.onclose = function(event){
$("#output").append("<p class=\"text-uppercase\"><strong>CONNECTION: CLOSED</strong></p>");
};
$("#send").click(function(){
var message = {};
message["user"] = $("#user").val();
message["message"] = $("#message").val();
connection.send(JSON.stringify(message));
});
});
</script>

清单7-3只显示了重要代码的JavaScript片段,我将在下面解释:

• WebSocket: 这是JavaScript引擎的一部分,它将连接到指定的URI。请注意,模式是ws,我们正在使用/llws地址,我们在configuration类中指定了(请参见清单7-1)。

• onopen: 这是一个回调函数,它是在与服务器建立连接时执行的。请注意,我们只是将字符串记录到控制台。

• onmessage: 这是一个回调函数,当从服务器收到一条消息时执行。在这种情况下,我们正在解析一个事件。将数据转换为JSON对象。

• onclose: 这是一个回调函数,当连接被关闭或与服务器丢失时执行。

• $.click/send: 这是一个附在Send按钮上的回调函数,当按钮被单击时,它会被调用。这里我们使用send方法,它将发送消息对象的JSON字符串。

 

接下来,让我们运行websocket-demo项目;启动之后,打开浏览器并转到http://localhost:8080/llws.html。您应该会看到类似于图7-3的内容。

Spring Boot Messaging Chapter 7 Web Messaging_Message_04

Figure 7-3. ​​http://localhost:8080/llws.html​​

在你通过浏览器打开llws.html页面之后,查看一下应用程序日志。您应该会看到类似于图7-4的内容。

Spring Boot Messaging Chapter 7 Web Messaging_Message_05

Figure 7-4. The websocket-demo project logs

图7-4显示了日志,由此建立的后续连接是从LlWebSocketHandler类调用的。这意味着客户端成功地连接到服务器。您还可以查看一下浏览器的开发者控制台,以查看显示连接的字符串的日志。见图7 - 5。

Spring Boot Messaging Chapter 7 Web Messaging_spring_06

 Figure 7-5. Browser’s console

接下来,您可以通过修改来自llws.html的用户和消息输入来发送一条消息。并单击Send按钮。单击Send按钮之后,您应该会看到类似于图7-6的内容。

Spring Boot Messaging Chapter 7 Web Messaging_spring_07

Figure 7-6. websocket-demo project logs after clicking Send

图7-6显示了发送消息后的日志以及从handleTextMessage方法打印出来的日志。

这个例子向您展示了如何从客户端发送一条消息到服务器。现在,让我们从服务器上做一个响应,一个来自服务端的回复。您需要在handleTextMessage中添加一些东西来处理响应。

修改handleTextMessage以如下所列:

@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
System.out.println(">>>> " + message);
session.sendMessage(message);
}

正如您所看到的,我们使用session实例来调用sendMessage,这意味着我们将使用连通客户端的相同session来应答。

重新启动websocket-demo项目并刷新llws.html页面。然后通过填写用户和消息输入来发送一条消息。您应该在Server面板的回应消息中看到响应,如图7-7所示。注意,如果您正在使用STS,您只需要等待项目重新启动;这是有可能的,这要归功于spring-boot-devtools插件。

Spring Boot Messaging Chapter 7 Web Messaging_Message_08

 Figure 7-7. Echo server response

如果您停止了应用程序,您将看到来自服务器面板的回应消息中的图7-8。

Spring Boot Messaging Chapter 7 Web Messaging_spring_09

Figure 7-8. Closed connection

 

正如您所看到的,创建低级WebSockets应用程序非常简单。如果您需要从Spring应用程序发送消息,会发生什么情况?换句话说,你的应用需要成为客户端。您可以向应用程序添加以下代码(参见清单7-4)。

Listing 7-4. WebSockets Client—Snippet of the WebSocketDemoApplication.java

StandardWebSocketClient client = new StandardWebSocketClient();
ListenableFuture<WebSocketSession> future =
client.doHandshake(handler,
new WebSocketHttpHeaders(),
new URI("ws://localhost:8080/llws"));
WebSocketSession session = future.get();
WebSocketMessage<String> message =
new TextMessage("Hello there...");
session.sendMessage(message);

 清单7-4显示了如果您想要创建WebSockets客户端(而不是HTML web页面),您需要添加哪些内容。在这里,我们使用StandardWebSocketClient(一个低级WebSockets协议),并手动执行握手。它传递给处理程序一些头文件和你将要连接的URI。(注意,您可以使用以前的处理程序或创建自己的处理程序,然后您可以实现afterConnectionEstablished检查您是否成功连接。)然后你会得到一个WebSocketSession实例,并可以发送消息。您可以看到,使用带有Spring类的WebSockets客户端是一个简单的实现。接下来,让我们开始使用SockJS和STOMP子协议的备用选项。

 

2.2.Using SockJS and STOMP

为什么我们需要使用SockJS和STOMP?请记住,并非所有浏览器都支持WebSockets,通常客户端和服务器必须就如何处理消息达成一致。

当然,这不是正确的消息传递方式,因为我们想要有一个解耦场景,客户端没有绑定到服务器。

       SockJS帮助模拟WebSockets并执行最初的握手。然后,通过使用STOMP,我们可以用一种可互操作的有线格式来回复,这样我们就可以使用支持该协议的多个代理。

2.2.1.Chat Room Application(聊天室程序)

我们将继续使用websocket-demo项目,但是我们将使用不同的文件和类。这个例子创建了一个聊天室,这是WebSockets技术的一个非常常见的用法。

首先,让我们看看如何配置这个项目将使用SockJS和STOMP。打开com.micai.spring.messaging.config.WebSocketConfig类。参见清单7 - 5。

Listing 7-5. com.micai.spring.messaging.config.WebSocketConfig.java

@Configuration
@EnableWebSocketMessageBroker
@EnableConfigurationProperties(SimpleWebSocketsProperties.class)
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

@Autowired
private SimpleWebSocketsProperties simpleWebSocketsProperties;

@Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
stompEndpointRegistry.addEndpoint(simpleWebSocketsProperties.getEndpoint()).withSockJS();
}

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker(simpleWebSocketsProperties.getTopic());
registry.setApplicationDestinationPrefixes(simpleWebSocketsProperties.getAppDestinationPrefix());
}
}

清单7-5显示了使用SocksJS和STOMP使用WebSockets所需的配置。让我们回顾一下它:

• @EnableWebSocketMessageBroker: 这个注解需要使用更高级别的消息传递子协议(SockJS/STOMP)来支持基于WebSockets的代理后端消息传递。

• AbstractWebSocketMessageBrokerConfigurer: 这个类实现了WebSocketMessageBrokerConfigurer来配置消息处理,使用简单的消息传递协议,比如WebSockets客户端的STOMP。

• registerStompEndpoints: 这种方法被调用来注册STOMP端点,将每个端点映射到一个特定的URL并配置SockJS后备选项。

• StompEndpointRegistry: 这是一个在WebSockets端点上注册STOMP的契约。它提供了一个流畅的API来构建注册表。

• withSockJS: 这个方法将使握手所需的SockJS后备选项成为可能。

• configureMessageBroker:这个方法被调用来配置message broker options。在这种情况下,我们使用MessageBrokerRegistry。

• MessageBrokerRegistry: 这个实例将帮助配置所有代理选项。我们使用enableSimpleBroker,它接受一个或多个前缀来过滤针对代理的目的地。这将与带有@SendTo注释的带注释的方法一起使用。我们还使setApplicationDestinationPrefixes前缀来配置一个或多个前缀,以过滤目标瞄准应用程序注释的方法。换句话说,它将寻找用@MessageMapping映射注释的方法。

 

您可以看到,我们需要添加一个端点、一个代理和前缀路径,以使用SockJS和STOMP来配置WebSockets。看一看图7-9。

Spring Boot Messaging Chapter 7 Web Messaging_spring_10

图7-9向您展示了客户端和服务器之间通信的一般情况。接下来,打开com.micai.spring.messaging.controller.SimpleController类。参见清单7-6。

Listing 7-6. com.micai.spring.messaging.controller.SimpleController.java

@Controller
public class SimpleController {

@MessageMapping("${micai.ws.mapping}")
@SendTo("/topic/chat-room")
public ChatMessage chatRoom(ChatMessage message) {
return message;
}
}

清单7-6通过使用新的注解向您展示了接收者和发送者。请记住,项目现在将运行一个聊天室,因此不同的客户端可以连接和接收来自其他用户的消息:

• @MessageMapping: 这个注释与应用程序前缀有关,这意味着客户端需要向前缀+映射发送一条消息。在我们的例子中,他是/my-app/chat-room。这个注释由@Controller类的方法支持,并且这个值可以被当作ant-style、散列分隔和路径模式来对待。有了这个注释,您就有了可以用作方法参数的其他注释,包括@Payload, @Header, @Headers, @DestinationVariable, and java.security.Principal.

• @SendTo: 你已经知道这个注解;它和我们在JMS和RabbitMQ章节中使用的是相同的。在这种情况下,这个注解用于向任何其他目的地发送一条消息。

 

尽管我们没有使用@SubscribeMapping,但是您可以在@Controller注释类中使用它,这是您想要用来处理传入消息的方法。当您需要获得@SendTo的响应副本时,这通常是有用的。

另一个我们没有使用的实用程序类是SimpleMessagingTemplate。它可以用来发送消息。例如:

@Controller
public class AnotherController {

@Autowired
private SimpMessagingTemplate template;

@RequestMapping(path="/rate/new", method = RequestMethod.POST)
public void newRates(Rate rate) {
this.template.convertAndSend("/topic/new-rate", rate);
}
}

 正如您所看到的,使用WebSockets技术实现subscriber/publisher模型非常简单。AnotherController通过发送一条消息(ChatMessage)来充当客户端。

接下来,让我们看看另一个客户端。打开src/main/resources/static/sockjs-stomp.html页面。参见清单7-7。

Listing 7-7. Snippet of sockjs-stomp.html

<script>
$(function(){
var socket = new SockJS('http://localhost:8080/stomp-endpoint');
var stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/chat-room', function (data) {
console.log('>>>>> ' + data);
var json = JSON.parse(data.body);
$("#output").append("<span><strong>" + json.user + "</strong>: <em>" + json.message +"</em></span><br/>");
});
});
$("#send").click(function(){
var chatMessage = {}
chatMessage["user"] = $("#user").val();
chatMessage["message"] = $("#message").val();
stompClient.send("/my-app/chat-room",{},JSON.stringify(chatMessage));
});
});
</script>

清单7-7显示了JavaScript客户端。让年代分析它:

• SockJS: 这是一个模拟WebSockets协议的JavaScript库。这个对象连接到服务器端指定的/stomp端点。有关此库的更多信息,请访问http://sockjs.org/。

• STOMP: 这是一个使用WebSockets协议的JavaScript库,通常需要SockJS对象。要了解更多信息,请访问http://jmesnil.net/stomp-websocket/doc/。

• connect: 这是一个回调,当连接与服务器建立时将被调用。

• subscribe: 这是一个回调,当在订阅的目的地有一条消息时,它将被调用。

• send: 该方法将向所提供的目的地发送一条消息。

 

这是在JavaScript客户端中使用SockJS和STOMP的非常简单的方法。现在您已经准备好运行websockets-demo项目了。

注意:在本节中运行websocket-demo项目之前,请确保LlWebSocketConfig类是禁用的(通过注释掉@Configuration和@Enablewebsocket注释)。

 

运行这个项目并打开一个浏览器。去http://localhost:8080/sockjs-stomp.html页面。您将看到类似于前面的示例。如果您感兴趣的话,您可以到浏览器的开发者控制台查看控制台日志。见图7 - 10。

 

Spring Boot Messaging Chapter 7 Web Messaging_Spring Boot_11

Figure 7-10. Browser’s console log

 

图7-10显示了连接日志。现在打开第二个浏览器窗口并访问相同的URL。这个想法是为了模拟两个客户的通信。接下来,发送一些消息并查看结果。参见图7-11和7-12。 

Spring Boot Messaging Chapter 7 Web Messaging_Web Messaging_12

 Figure 7-11. Two browser clients

Spring Boot Messaging Chapter 7 Web Messaging_Web Messaging_13

Figure 7-12. Application logs

 

图7-11显示了两个客户端使用SockJS和STOMP通过WebSockets发送消息。

图7-12显示了SimpleController日志和ChatMessage正在由聊天室方法处理(这是因为@MessageMapping和@SendTo注解)。

这就是你如何用Spring Boot和spring-websocket模块轻松创建聊天室的方式。

问:如何使用Spring创建SockJS客户端?假设您需要使用STOMP将消息发送到远程WebSockets连接。看一下WebSocketsDemoApplication类中的代码,它被注释掉了。您将看到如何使用SockJSClient和WebSocketsStompClient类。

 

三:Using RabbitMQ as a STOMP Broker Relay

您是否想知道如果您的应用程序需要更多的支持,更可伸缩,会发生什么?一种解决方案是添加几个Spring Boot应用程序,这些应用程序启用了WebSockets代理,然后在它们前面添加一个负载均衡器。这样就能获得高可用性,因为您需要为应用程序添加逻辑和行为,这样,如果其中一个应用挂掉了,其他的应用还能继续对客户端作出响应。

好消息是spring-websocket模块有一种方法可以使用外部消息中间件:RabbitMQ。RabbitMQ将STOMP协议作为一个插件。它还包括真正的高可用性和建立集群的简单方法。

按照以下步骤将RabbitMQ作为STOMP中继添加到您的应用程序中:

1. 确保启用RabbitMQ STOMP插件:

$ rabbitmq-plugins enable rabbitmq_stomp

$ rabbitmq-plugins enable rabbitmq_web_stomp

2. 将以下依赖项添加到pom.xml文件.

<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-net</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.8.Final</version>
</dependency>

 3. 配置类似于以下代码的WebSocketsConfig类:

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes(props.getAppDestinationPrefix());
config.enableStompBrokerRelay("/topic", "/queue").setRelayPort(61613);
}

与前一个版本不同的是,现在configureMessageBroker方法中,您正在配置enableStompBrokerRelay(使用/topic和/queue),并使用setRelayPort方法添加STOMP端口(值61613,RabbitMQ的STOMP端口)。

这样就。您现在可以使用RabbitMQ作为消息代理。在运行项目之前,请确保RabbitMQ服务已经启动并运行。然后您可以运行这个项目并使用相同的sockjs-stomp.html页面。这里的重要部分是密切关注RabbitMQ控制台,以查看连接和队列。

 

四.Currency Project

看一下rest-api-websockets项目。您将会找到RateWebSocketsConfig类,它与另一个项目非常相似。其思想是,货币项目有一个简单的WebSockets代理,它将通过WebSockets协议接受任何客户端连接。

每当有一个新的利率发布时,它都会向客户端发送一条消息到/rat/new端点。看一看这个:

• RateWebSocketsConfig:该类具有WebSockets消息传递所需的配置。

• CurrencyController: 添加新利率方法中的该类有以下语句

webSocket.convertAndSend("/rate/new", currencyExchange);

• src/main/resources/public/index-ws.html: 这个网页有定义连接到服务器的客户端的代码。看一看;它非常简单。

 

要测试它,只需在命令行中添加一个简单的post

$ curl -i -X POST -H "Content-Type: application/json" -H "Accept: application/json"

-d '{"base":"USD","date":"2017-02-15","rates":[{"code":"EUR","rate":0.82857,

"date":"2017-02-15"},{"code":"JPY","rate":105.17,"date":"2017-02-15"},

{"code":"MXN","rate":22.232,"date":"2017-02-15"},

{"code":"GBP","rate":0.75705,"date":"2017-02-16"}]}' localhost:8080/currency/new

 

您还可以使用任何其他REST客户端,比如POSTMAN:https://www.getpostman.com/。在发布新利率之后,您将看到面板中的利率,如图7-13所示。

Spring Boot Messaging Chapter 7 Web Messaging_Message_14

Figure 7-13. Currency exchange through WebSockets

 

五:总结

本章讨论了使用Spring websocket模块和Spring Boot的WebSockets消息。

您了解了WebSockets是如何使用HTTP握手的,然后切换到TCP连接。您看到了如何创建低级别服务器/客户端的示例。

您看到了如何使用SockJS和STOMP来促进异步或事件驱动消息传递的通信。您还学习了如何配置RabbitMQ,并将其用作STOMP转发消息。您在货币交换项目中看到了一些代码,它为连接到该服务的任何客户端发送新利率。

下一章将向您展示如何使用Spring Integration module将您的代码与多种技术集成在一起。