在上一篇文章中,我们还看到了如何将 Spring Boot + RabbitMQ 应用程序部署到 Pivotal Cloud Foundry。我已经将我们正在创建的实时聊天应用程序托管到 Pivotal Cloud Foundry 并使用可以在 JavaInUse 聊天应用程序上查看演示。
JavaInUse 聊天应用程序演示 在本教程中,我们将使用 STOMP 协议。STOMP 是一个简单的面向文本的消息传递协议,我们的 UI 客户端(浏览器)使用它连接到企业消息代理。
客户端可以使用 SEND 或 SUBSCRIBE 命令发送或订阅消息以及描述消息内容和接收人的“destination”标头。
它定义了客户端和服务器与消息传递语义进行通信的协议。它没有定义任何实现细节,而是解决了一个易于实现的用于消息传递集成的有线协议。该协议与 HTTP 大体相似,并使用以下命令在 TCP 上运行:
CONNECTSENDSUBSCRIBEUNSUBSCRIBEBEGINCOMMITABORTACKNACKDISCONNECT
当使用 Spring 的 STOMP 支持时,Spring WebSocket 应用程序充当客户端的 STOMP 代理。消息被路由到@Controller 消息处理方法或一个简单的内存代理,该代理跟踪订阅并将消息广播给订阅用户。您还可以将 Spring 配置为使用专用的 STOMP 代理(例如 RabbitMQ、ActiveMQ 等)来实际广播消息。在这种情况下,Spring 维护与代理的 TCP 连接,将消息中继给它,并将消息从它向下传递到连接的 WebSocket 客户端。
视频
本教程在下面的 Youtube 视频中进行了解释。让我们开始-
创建 Spring Boot WebSocket 应用程序- 该项目将如下 -
定义 pom.xml 如下 - 添加spring-boot-starter-websocket和spring-boot-starter-amqp依赖项。
<?xml version="1.0" encoding="UTF-8"?>定义域类 WebSocketChatMessage 如下- 4.0.0 com.example spring-boot-websocket-chat0.0.1-SNAPSHOT jar spring-boot-websocket-chat org.springframework.boot spring-boot-starter-parent2.0.0.RELEASE UTF-8 UTF-8 1.8 org.springframework.boot spring-boot-starter-websocketorg.springframework.boot spring-boot-starter-amqporg.springframework.boot spring-boot-starter-reactor-nettypackage com.javainuse.domain;public class WebSocketChatMessage {private String type;private String content;private String sender;public String getType() {return type;}public void setType(String type) {this.type = type;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}public String getSender() {return sender;}public void setSender(String sender) {this.sender = sender;}} 定义 WebSocket 配置类。
@Configuration告诉它是一个 Spring 配置类。 @EnableWebSocketMessageBroker启用由消息代理支持的 WebSocket 消息处理。在这里,我们使用 STOMP 作为消息代理。configureMessageBroker() 方法使rabbitmq 消息代理能够将消息传送回客户端,目的地为前缀为“/topic”和“/queue”。
同样在这里,我们配置了所有带有“/app”前缀的消息将被路由到控制器类中的@MessageMapping-annotated 方法。
例如,“/app/chat.sendMessage”是 WebSocketController.sendMessage() 方法映射到处理的端点。package com.javainuse.config;import org.springframework.context.annotation.Configuration;import org.springframework.messaging.simp.config.MessageBrokerRegistry;import org.springframework.web.socket.config.annotation.*;@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketChatConfig implements WebSocketMessageBrokerConfigurer {@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/websocketApp").withSockJS();}@Overridepublic void configureMessageBroker(MessageBrokerRegistry registry) {registry.setApplicationDestinationPrefixes("/app");registry.enableStompBrokerRelay("/topic").setRelayHost("localhost").setRelayPort(61613).setClientLogin("guest").setClientPasscode("guest");}} 定义 WebSocker 监听器类。此类监听诸如新用户加入聊天或用户离开聊天等事件。package com.javainuse.config;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.event.EventListener;import org.springframework.messaging.simp.SimpMessageSendingOperations;import org.springframework.messaging.simp.stomp.StompHeaderAccessor;import org.springframework.stereotype.Component;import org.springframework.web.socket.messaging.SessionConnectedEvent;import org.springframework.web.socket.messaging.SessionDisconnectEvent;import com.javainuse.domain.WebSocketChatMessage;@Componentpublic class WebSocketChatEventListener { @Autowired private SimpMessageSendingOperations messagingTemplate; @EventListener public void handleWebSocketConnectListener(SessionConnectedEvent event) { System.out.println("Received a new web socket connection"); } @EventListener public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) { StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage()); String username = (String) headerAccessor.getSessionAttributes().get("username"); if(username != null) { WebSocketChatMessage chatMessage = new WebSocketChatMessage(); chatMessage.setType("Leave"); chatMessage.setSender(username); messagingTemplate.convertAndSend("/topic/public", chatMessage); } }} 定义控制器类。之前我们已经配置了 websocket,所有来自客户端的带有前缀“/app”的消息都将被路由到带有@MessageMapping 注释的适当消息处理方法。
例如,目标为 /app/chat.newUser 的消息将被路由到 newUser() 方法,目标为 /app/chat.sendMessage 的消息将被路由到 sendMessage() 方法。package com.javainuse.controller;import org.springframework.messaging.handler.annotation.MessageMapping;import org.springframework.messaging.handler.annotation.Payload;import org.springframework.messaging.handler.annotation.SendTo;import org.springframework.messaging.simp.SimpMessageHeaderAccessor;import org.springframework.stereotype.Controller;import com.javainuse.domain.WebSocketChatMessage;@Controllerpublic class WebSocketChatController {@MessageMapping("/chat.sendMessage")@SendTo("/topic/javainuse")public WebSocketChatMessage sendMessage(@Payload WebSocketChatMessage webSocketChatMessage) {return webSocketChatMessage;}@MessageMapping("/chat.newUser")@SendTo("/topic/javainuse")public WebSocketChatMessage newUser(@Payload WebSocketChatMessage webSocketChatMessage,SimpMessageHeaderAccessor headerAccessor) {headerAccessor.getSessionAttributes().put("username", webSocketChatMessage.getSender());return webSocketChatMessage;}} 最后用@SpringBootApplication注解定义Spring Boot类package com.javainuse;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class SpringBootChatApplication {public static void main(String[] args) {SpringApplication.run( SpringBootChatApplication.class , args);}}
定义 index.html。在这里,我们已经为我们的聊天应用程序定义了 UI。它还利用了 sockjs 和 stomp 库。HTML 文件包含用于显示聊天消息的用户界面。它包括 sockjs 和 stomp javascript 库。SockJS 是一个提供类 WebSocket 对象的浏览器 Javascript 库。SockJS 为您提供了一个连贯的、跨浏览器的 Javascript API,它在浏览器和 Web 服务器之间创建了一个低延迟、全双工、跨域的通信通道。
STOMP JS 是 javascript 的 stomp 客户端。JavaInUse Chat Application | JavaInUse Welcome - To join the chat group enter yournameJavaInUse Chat Application
定义 javascript 文件。stompClient.subscribe()函数采用一个回调方法,只要消息到达订阅的主题,就会调用该方法。connect()函数利用 SockJS 和 stomp 客户端建立到我们在 Spring Boot 应用程序中配置的 /websocketApp 端点的连接。客户端订阅 /topic/javainuse 目的地。'use strict';document.querySelector('#welcomeForm').addEventListener('submit', connect, true)document.querySelector('#dialogueForm').addEventListener('submit', sendMessage, true)var stompClient = null;var name = null;function connect(event) {name = document.querySelector('#name').value.trim();if (name) {document.querySelector('#welcome-page').classList.add('hidden');document.querySelector('#dialogue-page').classList.remove('hidden');var socket = new SockJS('/websocketApp');stompClient = Stomp.over(socket);stompClient.connect({}, connectionSuccess);}event.preventDefault();}function connectionSuccess() {stompClient.subscribe('/topic/javainuse', onMessageReceived);stompClient.send("/app/chat.newUser", {}, JSON.stringify({sender : name,type : 'newUser'}))}function sendMessage(event) {var messageContent = document.querySelector('#chatMessage').value.trim();if (messageContent && stompClient) {var chatMessage = {sender : name,content : document.querySelector('#chatMessage').value,type : 'CHAT'};stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(chatMessage));document.querySelector('#chatMessage').value = '';}event.preventDefault();}function onMessageReceived(payload) {var message = JSON.parse(payload.body);var messageElement = document.createElement('li');if (message.type === 'newUser') {messageElement.classList.add('event-data');message.content = message.sender + 'has joined the chat';} else if (message.type === 'Leave') {messageElement.classList.add('event-data');message.content = message.sender + 'has left the chat';} else {messageElement.classList.add('message-data');var element = document.createElement('i');var text = document.createTextNode(message.sender[0]);element.appendChild(text);messageElement.appendChild(element);var usernameElement = document.createElement('span');var usernameText = document.createTextNode(message.sender);usernameElement.appendChild(usernameText);messageElement.appendChild(usernameElement);}var textElement = document.createElement('p');var messageText = document.createTextNode(message.content);textElement.appendChild(messageText);messageElement.appendChild(textElement);document.querySelector('#messageList').appendChild(messageElement);document.querySelector('#messageList').scrollTop = document.querySelector('#messageList').scrollHeight;} 定义 CSS-{-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;}html, body {height: 100%;overflow: hidden;}body {margin: 0;padding: 0;font-weight: 400;font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;font-size: 1rem;line-height: 1.58;color: #333;background-color: #f4f4f4;height: 100%;}.clearfix:after {display: block;content: "";clear: both;}.hidden {display: none;}input {padding-left: 10px;outline: none;}h1, h2, h3, h4, h5, h6 {margin-top: 20px;margin-bottom: 20px;}h1 {font-size: 1.7em;}a {color: #128ff2;}button {box-shadow: none;border: 1px solid transparent;font-size: 14px;outline: none;line-height: 100%;white-space: nowrap;vertical-align: middle;padding: 0.6rem 1rem;border-radius: 2px;transition: all 0.2s ease-in-out;cursor: pointer;min-height: 38px;}button.default {background-color: #e8e8e8;color: #333;box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);}button.primary {background-color: #128ff2;box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);color: #fff;}}button.accent {background-color: #ff4743;box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);color: #fff;}#welcome-page {text-align: center;}.welcome-page-container {background-color: grey;width: 100%;max-width: 500px;display: inline-block;margin-top: 42px;vertical-align: middle;position: relative;padding: 35px 55px 35px;min-height: 250px;position: absolute;top: 50%;left: 0;right: 0;margin: 0 auto;margin-top: -160px;}#dialogue-page {position: relative;height: 100%;}.dialogue-container {background-color: green;margin: 10px 0;max-width: 700px;margin-left: auto;margin-right: auto;box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27);margin-top: 30px;height: calc(100% - 60px);max-height: 600px;position: relative;}#dialogue-page ul {list-style-type: none;background-color: #FFF;margin: 0;overflow: auto;overflow-y: scroll;padding: 0 20px 0px 20px;height: calc(100% - 150px);}#dialogue-page #dialogueForm {padding: 20px;}#dialogue-page ul li {line-height: 1.5rem;padding: 10px 20px;margin: 0;border-bottom: 1px solid #f4f4f4;}#dialogue-page ul li p {margin: 0;}#dialogue-page .event-data {width: 100%;text-align: center;clear: both;}#dialogue-page .event-data p {color: #777;font-size: 14px;word-wrap: break-word;}#dialogue-page .message-data {padding-left: 68px;position: relative;}#dialogue-page .message-data i {position: absolute;width: 42px;height: 42px;overflow: hidden;left: 10px;display: inline-block;vertical-align: middle;font-size: 18px;line-height: 42px;color: #fff;text-align: center;border-radius: 50%;font-style: normal;text-transform: uppercase;}#dialogue-page .message-data span {color: #333;font-weight: 600;}#dialogue-page .message-data p {color: #43464b;}#dialogueForm .input-group input {border: 0;padding: 10px;background: whitesmoke;float: left;width: calc(100% - 85px);}#dialogueForm .input-group button {float: left;width: 80px;height: 38px;margin-left: 5px;}.dialogue-header {text-align: center;padding: 15px;border-bottom: 1px solid #ececec;}.dialogue-header h2 {margin: 0;font-weight: 500;}@media screen and (max-width: 730px) {.dialogue-container {margin-left: 10px;margin-right: 10px;margin-top: 10px;}} 我们完成了所需的 Java 代码。现在让我们启动 RabbitMQ。正如我们在 RabbitMQ 入门中详细解释的那样,执行启动 RabbitMQ 的步骤。
我们需要对 RabbitMQ 执行一个额外的步骤 - 为 RabbitMQ 安装 STOMP 插件,以便它可以与 STOMP 消息一起使用 接下来通过将 Spring Boot Chat 应用程序作为 Java 应用程序运行来启动它。按如下方式点击 url - http://localhost:8080
输入用户名
然后我们会看到聊天窗口。
如果我们得到 rabbitMQconsole,我们可以看到它已经创建了一个队列。下载源代码 下载 -
Spring Boot + WebSocket + RabbitMQ 聊天示例