使用 Spring Boot + WebSocket + RabbitMQ 构建聊天应用程序

上一篇文章中,我们创建了一个 Spring Boot + WebSocket Hello World 示例。 在这篇文章中,我们将创建一个实时多用途聊天应用程序。
上一篇文章中,我们还看到了如何将 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 支持时,Spring WebSocket 应用程序充当客户端的 STOMP 代理。消息被路由到@Controller 消息处理方法或一个简单的内存代理,该代理跟踪订阅并将消息广播给订阅用户。
您还可以将 Spring 配置为使用专用的 STOMP 代理(例如 RabbitMQ、ActiveMQ 等)来实际广播消息。在这种情况下,Spring 维护与代理的 TCP 连接,将消息中继给它,并将消息从它向下传递到连接的 WebSocket 客户端。

 

视频

本教程在下面的 Youtube 视频中进行了解释。
 

让我们开始-

创建 Spring Boot WebSocket 应用程序-

该项目将如下 -



定义 pom.xml 如下 - 添加spring-boot-starter-websocketspring-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 如下-
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 聊天示例
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值