在上一篇文章中,我们创建了一个 Spring Boot + WebSocket Hello World 示例。 在这篇文章中,我们将创建一个实时多用途聊天应用程序。
在上一篇文章中,我们还看到了如何将 Spring Boot + RabbitMQ 应用程序部署到 Pivotal Cloud Foundry。我已经将我们正在创建的实时聊天应用程序托管到 Pivotal Cloud Foundry 并使用可以在 JavaInUse 聊天应用程序上查看演示。
JavaInUse 聊天应用程序演示 在本教程中,我们将使用 STOMP 协议。STOMP 是一个简单的面向文本的消息传递协议,我们的 UI 客户端(浏览器)使用它连接到企业消息代理。
客户端可以使用 SEND 或 SUBSCRIBE 命令发送或订阅消息以及描述消息内容和接收人的“destination”标头。
它定义了客户端和服务器与消息传递语义进行通信的协议。它没有定义任何实现细节,而是解决了一个易于实现的用于消息传递集成的有线协议。该协议与 HTTP 大体相似,并使用以下命令在 TCP 上运行:
您还可以将 Spring 配置为使用专用的 STOMP 代理(例如 RabbitMQ、ActiveMQ 等)来实际广播消息。在这种情况下,Spring 维护与代理的 TCP 连接,将消息中继给它,并将消息从它向下传递到连接的 WebSocket 客户端。
定义 pom.xml 如下 - 添加spring-boot-starter-websocket和spring-boot-starter-amqp依赖项。
在上一篇文章中,我们还看到了如何将 Spring Boot + RabbitMQ 应用程序部署到 Pivotal Cloud Foundry。我已经将我们正在创建的实时聊天应用程序托管到 Pivotal Cloud Foundry 并使用可以在 JavaInUse 聊天应用程序上查看演示。
JavaInUse 聊天应用程序演示 在本教程中,我们将使用 STOMP 协议。STOMP 是一个简单的面向文本的消息传递协议,我们的 UI 客户端(浏览器)使用它连接到企业消息代理。
客户端可以使用 SEND 或 SUBSCRIBE 命令发送或订阅消息以及描述消息内容和接收人的“destination”标头。
它定义了客户端和服务器与消息传递语义进行通信的协议。它没有定义任何实现细节,而是解决了一个易于实现的用于消息传递集成的有线协议。该协议与 HTTP 大体相似,并使用以下命令在 TCP 上运行:
- CONNECT
- SEND
- SUBSCRIBE
- UNSUBSCRIBE
- BEGIN
- COMMIT
- ABORT
- ACK
- NACK
- DISCONNECT
您还可以将 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"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>spring-boot-websocket-chat</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-boot-websocket-chat</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-reactor-netty</artifactId>
</dependency>
</dependencies>
</project>
定义域类 WebSocketChatMessage 如下-
@Configuration告诉它是一个 Spring 配置类。 @EnableWebSocketMessageBroker启用由消息代理支持的 WebSocket 消息处理。在这里,我们使用 STOMP 作为消息代理。configureMessageBroker() 方法使rabbitmq 消息代理能够将消息传送回客户端,目的地为前缀为“/topic”和“/queue”。
同样在这里,我们配置了所有带有“/app”前缀的消息将被路由到控制器类中的@MessageMapping-annotated 方法。
例如,“/app/chat.sendMessage”是 WebSocketController.sendMessage() 方法映射到处理的端点。
例如,目标为 /app/chat.newUser 的消息将被路由到 newUser() 方法,目标为 /app/chat.sendMessage 的消息将被路由到 sendMessage() 方法。
定义 index.html。在这里,我们已经为我们的聊天应用程序定义了 UI。它还利用了 sockjs 和 stomp 库。HTML 文件包含用于显示聊天消息的用户界面。它包括 sockjs 和 stomp javascript 库。SockJS 是一个提供类 WebSocket 对象的浏览器 JavaScript 库。SockJS 为您提供了一个连贯的、跨浏览器的 Javascript API,它在浏览器和 Web 服务器之间创建了一个低延迟、全双工、跨域的通信通道。
STOMP JS 是 javascript 的 stomp 客户端。
我们需要对 RabbitMQ 执行一个额外的步骤 - 为 RabbitMQ 安装 STOMP 插件,以便它可以与 STOMP 消息一起使用 接下来通过将 Spring Boot Chat 应用程序作为 Java 应用程序运行来启动它。按如下方式点击 url - http://localhost:8080
输入用户名
然后我们会看到聊天窗口。
如果我们得到 rabbitMQconsole,我们可以看到它已经创建了一个队列。
Spring Boot + WebSocket + RabbitMQ 聊天示例
package 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
@EnableWebSocketMessageBroker
public class WebSocketChatConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocketApp").withSockJS();
}
@Override
public 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;
@Component
public 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;
@Controller
public 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;
@SpringBootApplication
public 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 客户端。
<!DOCTYPE html>
<html>
<head>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<title>JavaInUse Chat Application | JavaInUse</title>
<link rel="stylesheet" href="/css/style.css" />
<link
href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"
rel="stylesheet" id="bootstrap-css">
<script
src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
</head>
<body>
<div id="welcome-page">
<div class="welcome-page-container">
<h1 class="title">Welcome - To join the chat group enter your
name</h1>
<form id="welcomeForm" name="welcomeForm">
<div class="form-group">
<input type="text" id="name" placeholder="name"
class="form-control" />
</div>
<div class="form-group">
<button type="submit" onclass="accent username-submit">Lets
Begin</button>
</div>
</form>
</div>
</div>
<div id="dialogue-page" class="hidden">
<div class="dialogue-container">
<div class="dialogue-header">
<h2>JavaInUse Chat Application</h2>
</div>
<ul id="messageList">
</ul>
<form id="dialogueForm" name="dialogueForm" nameForm="dialogueForm">
<div class="form-group">
<div class="input-group clearfix">
<input type="text" id="chatMessage"
placeholder="Enter a message...." autocomplete="off"
class="form-control" />
<button type="submit" class="glyphicon glyphicon-share-alt">Send</button>
</div>
</div>
</form>
</div>
</div>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js"></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="/js/script.js"></script>
</body>
</html>
定义 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 聊天示例