手把手教你使用WebSocket搭建多人在线聊天室

手把手教你使用WebSocket搭建多人在线聊天室

前言

本文中搭建了一个简易的多人聊天室,使用了WebSocket的基础特性。

源代码来自老外的一篇好文:

https://www.callicoder.com/spring-boot-websocket-chat-example/

本文内容摘要:

手把手教你使用WebSocket搭建多人在线聊天室

此外,在下一篇文章中,我们将做到:

正文

WebSocket多人在线聊天室

本文工程源代码:

https://github.com/qqxx6661/springboot-websocket-demo

新建工程

我们新建一个SpringBoot2的项目工程,在默认依赖中,添加websocket依赖:


 org.springframework.boot
 spring-boot-starter-websocket

WebSocket 配置

我们先来设置websocket的配置,新建config文件夹,在里面新建类WebSocketConfig

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
 @Override
 public void registerStompEndpoints(StompEndpointRegistry registry) {
 registry.addEndpoint("/ws").withSockJS();
 }
 @Override
 public void configureMessageBroker(MessageBrokerRegistry registry) {
 registry.setApplicationDestinationPrefixes("/app");
 registry.enableSimpleBroker("/topic");
 }
}

代码解释:

@EnableWebSocketMessageBroker用于启用我们的WebSocket服务器。

我们实现了WebSocketMessageBrokerConfigurer接口,并实现了其中的方法。

在第一种方法中,我们注册一个websocket端点,客户端将使用它连接到我们的websocket服务器。

withSockJS()是用来为不支持websocket的浏览器启用后备选项,使用了SockJS。

方法名中的STOMP是来自Spring框架STOMP实现。 STOMP代表简单文本导向的消息传递协议。它是一种消息传递协议,用于定义数据交换的格式和规则。为啥我们需要这个东西?因为WebSocket只是一种通信协议。它没有定义诸如以下内容:如何仅向订阅特定主题的用户发送消息,或者如何向特定用户发送消息。我们需要STOMP来实现这些功能。

在configureMessageBroker方法中,我们配置一个消息代理,用于将消息从一个客户端路由到另一个客户端。

第一行定义了以“/app”开头的消息应该路由到消息处理方法(之后会定义这个方法)。

第二行定义了以“/topic”开头的消息应该路由到消息代理。消息代理向订阅特定主题的所有连接客户端广播消息。

在上面的示例中,我们使用的是内存中的消息代理。

之后也可以使用RabbitMQ或ActiveMQ等其他消息代理。

创建 ChatMessage 实体

ChatMessage用来在客户端和服务端中交互

我们新建model文件夹,创建实体类ChatMessage。

public class ChatMessage {
 private MessageType type;
 private String content;
 private String sender;
 public enum MessageType {
 CHAT,
 JOIN,
 LEAVE
 }
 public MessageType getType() {
 return type;
 }
 public void setType(MessageType 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;
 }
}

实体中,有三个字段:

类型有三种:

创建Controller来接收和发送消息

创建controller文件夹,在controller文件夹添加类ChatController

import com.example.websocketdemo.model.ChatMessage;
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;
@Controller
public class ChatController {
 @MessageMapping("/chat.sendMessage")
 @SendTo("/topic/public")
 public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
 return chatMessage;
 }
 @MessageMapping("/chat.addUser")
 @SendTo("/topic/public")
 public ChatMessage addUser(@Payload ChatMessage chatMessage, 
 SimpMessageHeaderAccessor headerAccessor) {
 // Add username in web socket session
 headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
 return chatMessage;
 }
}

代码解释:

我们在websocket配置中,从目的地以/app开头的客户端发送的所有消息都将路由到这些使用@MessageMapping注释的消息处理方法。

例如,具有目标/app/chat.sendMessage的消息将路由到sendMessage()方法,并且具有目标/app/chat.addUser的消息将路由到addUser()方法

添加WebSocket事件监听

完成了上述代码后,我们还需要对socket的连接和断连事件进行监听,这样我们才能广播用户进来和出去等操作。

创建listener文件夹,新建WebSocketEventListener类

import com.example.websocketdemo.model.ChatMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
@Component
public class WebSocketEventListener {
 private static final Logger logger = LoggerFactory.getLogger(WebSocketEventListener.class);
 @Autowired
 private SimpMessageSendingOperations messagingTemplate;
 @EventListener
 public void handleWebSocketConnectListener(SessionConnectedEvent event) {
 logger.info("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) {
 logger.info("User Disconnected : " + username);
 ChatMessage chatMessage = new ChatMessage();
 chatMessage.setType(ChatMessage.MessageType.LEAVE);
 chatMessage.setSender(username);
 messagingTemplate.convertAndSend("/topic/public", chatMessage);
 }
 }
}

代码解释:

我们已经在ChatController中定义的addUser()方法中广播了用户加入事件。因此,我们不需要在SessionConnected事件中执行任何操作。

在SessionDisconnect事件中,编写代码用来从websocket会话中提取用户名,并向所有连接的客户端广播用户离开事件。

创建前端聊天室页面

我们在src/main/resources文件下创建前端文件,结构类似这样:

static
 └── css
 └── main.css
 └── js
 └── main.js
 └── index.html 

1. HTML文件 index.html

HTML文件包含用于显示聊天消息的用户界面。 它包括sockjs和stomp 两个js库。

SockJS是一个WebSocket客户端,它尝试使用本机WebSockets,并为不支持WebSocket的旧浏览器提供支持。 STOMP JS是javascript的stomp客户端。

笔者在文件里使用了国内的CDN源



 
 
 Spring Boot WebSocket Chat Application
 
 
 
 
 
 
 

Type your username

Spring WebSocket Chat Demo

Connecting...

2. JavaScript main.js

添加连接到websocket端点以及发送和接收消息所需的javascript。

'use strict';
var usernamePage = document.querySelector('#username-page');
var chatPage = document.querySelector('#chat-page');
var usernameForm = document.querySelector('#usernameForm');
var messageForm = document.querySelector('#messageForm');
var messageInput = document.querySelector('#message');
var messageArea = document.querySelector('#messageArea');
var connectingElement = document.querySelector('.connecting');
var stompClient = null;
var username = null;
var colors = [
 '#2196F3', '#32c787', '#00BCD4', '#ff5652',
 '#ffc107', '#ff85af', '#FF9800', '#39bbb0'
];
function connect(event) {
 username = document.querySelector('#name').value.trim();
 if(username) {
 usernamePage.classList.add('hidden');
 chatPage.classList.remove('hidden');
 var socket = new SockJS('/ws');
 stompClient = Stomp.over(socket);
 stompClient.connect({}, onConnected, onError);
 }
 event.preventDefault();
}
function onConnected() {
 // Subscribe to the Public Topic
 stompClient.subscribe('/topic/public', onMessageReceived);
 // Tell your username to the server
 stompClient.send("/app/chat.addUser",
 {},
 JSON.stringify({sender: username, type: 'JOIN'})
 )
 connectingElement.classList.add('hidden');
}
function onError(error) {
 connectingElement.textContent = 'Could not connect to WebSocket server. Please refresh this page to try again!';
 connectingElement.style.color = 'red';
}
function sendMessage(event) {
 var messageContent = messageInput.value.trim();
 if(messageContent && stompClient) {
 var chatMessage = {
 sender: username,
 content: messageInput.value,
 type: 'CHAT'
 };
 stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(chatMessage));
 messageInput.value = '';
 }
 event.preventDefault();
}
function onMessageReceived(payload) {
 var message = JSON.parse(payload.body);
 var messageElement = document.createElement('li');
 if(message.type === 'JOIN') {
 messageElement.classList.add('event-message');
 message.content = message.sender + ' joined!';
 } else if (message.type === 'LEAVE') {
 messageElement.classList.add('event-message');
 message.content = message.sender + ' left!';
 } else {
 messageElement.classList.add('chat-message');
 var avatarElement = document.createElement('i');
 var avatarText = document.createTextNode(message.sender[0]);
 avatarElement.appendChild(avatarText);
 avatarElement.style['background-color'] = getAvatarColor(message.sender);
 messageElement.appendChild(avatarElement);
 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);
 messageArea.appendChild(messageElement);
 messageArea.scrollTop = messageArea.scrollHeight;
}
function getAvatarColor(messageSender) {
 var hash = 0;
 for (var i = 0; i < messageSender.length; i++) {
 hash = 31 * hash + messageSender.charCodeAt(i);
 }
 var index = Math.abs(hash % colors.length);
 return colors[index];
}
usernameForm.addEventListener('submit', connect, true)
messageForm.addEventListener('submit', sendMessage, true)

代码解释:

connect()函数使用SockJS和stomp客户端连接到我们在Spring Boot中配置的/ws端点。

成功连接后,客户端订阅/topic/public,并通过向/app/chat.addUser目的地发送消息将该用户的名称告知服务器。

stompClient.subscribe()函数采用一种回调方法,只要消息到达订阅主题,就会调用该方法。

其它的代码用于在屏幕上显示和格式化消息。

3. CSS main.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%;
}
body:before {
 height: 50%;
 width: 100%;
 position: absolute;
 top: 0;
 left: 0;
 background: #128ff2;
 content: "";
 z-index: 0;
}
.clearfix:after {
 display: block;
 content: "";
 clear: both;
}
.hidden {
 display: none;
}
.form-control {
 width: 100%;
 min-height: 38px;
 font-size: 15px;
 border: 1px solid #c8c8c8;
}
.form-group {
 margin-bottom: 15px;
}
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;
}
#username-page {
 text-align: center;
}
.username-page-container {
 background: #fff;
 box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27);
 border-radius: 2px;
 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;
}
.username-page-container .username-submit {
 margin-top: 10px;
}
#chat-page {
 position: relative;
 height: 100%;
}
.chat-container {
 max-width: 700px;
 margin-left: auto;
 margin-right: auto;
 background-color: #fff;
 box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27);
 margin-top: 30px;
 height: calc(100% - 60px);
 max-height: 600px;
 position: relative;
}
#chat-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);
}
#chat-page #messageForm {
 padding: 20px;
}
#chat-page ul li {
 line-height: 1.5rem;
 padding: 10px 20px;
 margin: 0;
 border-bottom: 1px solid #f4f4f4;
}
#chat-page ul li p {
 margin: 0;
}
#chat-page .event-message {
 width: 100%;
 text-align: center;
 clear: both;
}
#chat-page .event-message p {
 color: #777;
 font-size: 14px;
 word-wrap: break-word;
}
#chat-page .chat-message {
 padding-left: 68px;
 position: relative;
}
#chat-page .chat-message 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;
}
#chat-page .chat-message span {
 color: #333;
 font-weight: 600;
}
#chat-page .chat-message p {
 color: #43464b;
}
#messageForm .input-group input {
 float: left;
 width: calc(100% - 85px);
}
#messageForm .input-group button {
 float: left;
 width: 80px;
 height: 38px;
 margin-left: 5px;
}
.chat-header {
 text-align: center;
 padding: 15px;
 border-bottom: 1px solid #ececec;
}
.chat-header h2 {
 margin: 0;
 font-weight: 500;
}
.connecting {
 padding-top: 5px;
 text-align: center;
 color: #777;
 position: absolute;
 top: 65px;
 width: 100%;
}
@media screen and (max-width: 730px) {
 .chat-container {
 margin-left: 10px;
 margin-right: 10px;
 margin-top: 10px;
 }
}
@media screen and (max-width: 480px) {
 .chat-container {
 height: calc(100% - 30px);
 }
 .username-page-container {
 width: auto;
 margin-left: 15px;
 margin-right: 15px;
 padding: 25px;
 }
 #chat-page ul {
 height: calc(100% - 120px);
 }
 #messageForm .input-group button {
 width: 65px;
 }
 #messageForm .input-group input {
 width: calc(100% - 70px);
 }
 .chat-header {
 padding: 10px;
 }
 .connecting {
 top: 60px;
 }
 .chat-header h2 {
 font-size: 1.1em;
 }
}

整个项目结构如下:

手把手教你使用WebSocket搭建多人在线聊天室

启动

启动SpringBoot项目

效果入下:

手把手教你使用WebSocket搭建多人在线聊天室

手把手教你使用WebSocket搭建多人在线聊天室

补充:使用RabbitMQ代替内存作为消息代理

添加依赖:



 org.springframework.boot
 spring-boot-starter-amqp



 org.springframework.boot
 spring-boot-starter-reactor-netty

然后将WebSocketConfig类中configureMessageBroker方法改为使用RabbitMq,完成!

public void configureMessageBroker(MessageBrokerRegistry registry) {
 registry.setApplicationDestinationPrefixes("/app");
 // Use this for enabling a Full featured broker like RabbitMQ
 registry.enableStompBrokerRelay("/topic")
 .setRelayHost("localhost")
 .setRelayPort(61613)
 .setClientLogin("guest")
 .setClientPasscode("guest");
}

如此一来,便可以通过RabbitMq进行消息的订阅。

总结

我们在本文中搭建了一个简易的多人聊天室,使用了WebSocket的特性。

本文工程源代码:

https://github.com/qqxx6661/springboot-websocket-demo

在下一篇文章中,我们将做到:

下一篇文章过两天奉上,也可以关注我的公众号:后端技术漫谈,就不会忘记了看,哈哈哈哈哈。

关注我

我目前是一名后端开发工程师。主要关注后端开发,数据安全,爬虫,边缘计算等方向。

微信:yangzd1102

Github:@qqxx6661

个人博客:

原创博客主要内容

个人公众号:后端技术漫谈

手把手教你使用WebSocket搭建多人在线聊天室

个人公众号:后端技术漫谈

如果文章对你有帮助,不妨收藏起来并转发给您的朋友们~

展开阅读全文

页面更新:2024-05-21

标签:在线   聊天室   菜鸟   集群   路由   源代码   文件夹   客户端   定义   消息   事件   代码   文件   方法   数码   用户

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top