使用SpringBoot开发群聊应用

通过本文你将学习如何使用Spring Boot和WebSocket API开发一个简单的群聊天应用。

WebSocket是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务器主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

很多网站为实现推送技术,所用技术都是Ajax轮询。轮询指的是在特定的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后服务器返回最新的数据给浏览器。这种传统的模式有很明显的缺点,即浏览器需要不断的向服务器发出请求,而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,这样就会浪费很多资源。HTML5定义的WebSocket协议能更好的节省服务器带宽等资源,并能够实时地进行你通讯。

详情请看HTML5 WebSocket

新建项目

打开IDEA,选择Spring Initializer

填写好相关信息:

依赖选择Spring WebWebSocket

之后选择Finish即可。创建完毕后,项目目录结构如下:

WebSocket配置

首先我们配置一下WebSocket端点和消息代理。在com.andy.chat包下创建一个名为config的包,然后在config包新增一个带有以下内容的类WebSocketConfig

package com.andy.chat.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@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端点,客户端将使用该端点连接到我们的WebSocket服务器。端点配置中使用withSockJS()方法,用于为不支持WebSocket的浏览器启用备用选项。

你可能注意到方法名中带有STOMP。STOMP(Simple Text Orientated Messaging Protocol,简单文本定向消息协议)允许STOMP客户端与任意STOMP消息代理进行交互。

为什么需要STOMP?因为WebSocket只是一种通信协议。 它没有定义如何仅向订阅了特定主题的用户发送消息,或者如何向特定用户发送消息,所以需要STOMP来实现这些功能。举个例子,在课堂上举手就好比WebSocket代表的通信协议,因为它定义了在讲堂上如何和老师建立通信(举手)。而具体你想问老师的内容:“李老师,为什么这个函数不是连续的呢?”就相当于STOMP协议,因为它定义如何向特定用户发送消息(李老师)。

在第二个方法中,我们配置了一个消息代理,用于将消息从一个客户端路由到另一个客户端。第一行定义了以/app开头为目标的消息应应路由到消息处理方法。第二行定义了以/topic开头为目标的消息应路由到消息代理。消息代理广播消息到所有订阅了特定主题的所有连接的客户端。

上例中,我们使用了一个简单的内存消息代理。 也可以使用任何其他功能齐全的消息代理,例如RabbitMQActiveMQ

Message模型

Message模型用来表示客户端与服务器之间的消息。在com.andy.chat新建一个包model,在model包中新建Message类:

package com.andy.chat.model;

public class Message {
    private MessageType messageType;
    private String content;

    public MessageType getMessageType() {
        return messageType;
    }

    public void setMessageType(MessageType messageType) {
        this.messageType = messageType;
    }

    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;
    }

    private String sender;

    public enum MessageType {
        JOIN,CHAT, LEAVE
    }

}

收发消息的控制器

我们将在控制器中定义消息处理方法。在com.andy.chat包下新建一个包controller并在其中创建类ChatController

package com.andy.chat.controller;

import com.andy.chat.model.Message;
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("/sendMessage")
    @SendTo("/topic/public")
    public Message sendMessage(@Payload Message message) {
        return message;
    }

    @MessageMapping("addUser")
    @SendTo("/topic/public")
    public Message addUser(@Payload Message message, SimpMessageHeaderAccessor headerAccessor) {
        headerAccessor.getSessionAttributes().put("username", message.getSender());
        return message;
    }
}

正如在WebSocket配置一节所配置的,从客户端发送的所有以/app开头的消息将被路由到这些消息处理方法,这些方法使用@MessageMapping注解。

例如,以/app/sendMessage为目标的消息被路由到sendMessage()方法,以/app/addUser为目标的消息被路由到addUser方法。

添加WebSocket事件监听器

我们将使用事件监听器来监听Socket连接和断开事件,以便记录这些事件,并在用户加入或离开群聊时广播它们。

package com.andy.chat.controller;

import com.andy.chat.model.Message;
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 messageSendingOperations;

    @EventListener
    public void handleWebSocketConnectListener(SessionConnectedEvent event) {
        logger.info("新连接");
    }

    @EventListener
    public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
        String username = (String) headerAccessor.getSessionAttributes().get("username");
        if (username != null) {
            logger.info("用户{}取消连接", username);
            Message message = new Message();
            message.setMessageType(Message.MessageType.LEAVE);
            message.setSender(username);
            messageSendingOperations.convertAndSend("/topic/public", message);
        }
    }
}

SessionDisconnect事件中,从WebSocket会话中提取用户名,并将用户离开事件广播到所有连接的客户端。

前端开发

在项目的static下创建如下目录结构:

HTML文件用来显示用户群聊信息。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>群聊</title>
    <link rel="stylesheet" href="css/main.css">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
</head>
<body>

<div id="username-page">
    <div class="username-page-container">
        <h1 class="title">请输入您的用户名</h1>
        <form id="username-form">
            <div class="form-group">
                <input type="text" class="form-control" id="name" placeholder="用户名" autocomplete="off">
            </div>
            <div class="form-group">
                <button type="submit" class="username-submit accent">开始聊天</button>
            </div>
        </form>
    </div>
</div>

<div id="chat-page" class="hidden">
    <div class="chat-container">
        <div class="chat-header">
            <h2>我的群聊</h2>
        </div>
        <div class="connecting">
            连接中.....
        </div>
        <ul id="message-area">
        </ul>
        <form  id="message-form">
            <div class="form-group">
                <div class="input-group clearfix">
                    <input type="text" class="form-control" id="message" placeholder="输入消息" autocomplete="off">
                    <button type="submit" class="primary">发送</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/main.js"></script>
</body>
</html>

我们在其中引入了两个javascript库——sockjsstomp

SockJS是一个WebSocket客户端,它尝试使用WebSocket,并为不支持WebSocket的旧版浏览器提供备用选项。 stompjavascript的STOMP客户端。

现在,我们来编写用于连接WebSocket端点和收发信息的javascript代码。将下列代码添加到main.js文件中:

'use strict';

let usernamePage = document.querySelector("#username-page");
let chatPage = document.querySelector("#chat-page");
let usernameForm = document.querySelector("#username-form");
let messageForm = document.querySelector("#message-form");
let messageInput = document.querySelector("#message");
let messageArea = document.querySelector("#message-area");
let connectingElement = document.querySelector(".connecting");

let username = null;
let stompClient = null;
let 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");
        let socket = new SockJS('/ws');
        stompClient = Stomp.over(socket);
        stompClient.connect({}, onConnected, onError);
    }
    event.preventDefault();
}

function onConnected() {
    // 订阅Public主题
    stompClient.subscribe("/topic/public", onMessageReceived);
    stompClient.send("/app/addUser", {}, JSON.stringify({
        sender: username,
        messageType: 'JOIN'
    }));
    connectingElement.classList.add("hidden");
}

function onError(error) {
    connectingElement.textContent = "连接不上WebSocket服务器,请刷新页面!";
    connectingElement.style.color = "red";
}

function sendMessage(event) {
    let messageContent = messageInput.value.trim();
    if (messageContent && stompClient) {
        let message = {
            sender: username,
            content: messageContent,
            messageType: "CHAT"
        };
        stompClient.send("/app/sendMessage", {}, JSON.stringify(message));
        messageInput.value = "";
    }
    event.preventDefault();
}

function onMessageReceived(payload) {
    var message = JSON.parse(payload.body);

    var messageElement = document.createElement('li');

    if(message.messageType === 'JOIN') {
        messageElement.classList.add('event-message');
        message.content = message.sender + ' 加入!';
    } else if (message.messageType === 'LEAVE') {
        messageElement.classList.add('event-message');
        message.content = message.sender + ' 离开!';
    } 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方法用SockJSstomp客户端连接到Spring Boot配置的/ws端点。

添加如下代码到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 #message-form {
    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;
}

#message-form .input-group input {
    float: left;
    width: calc(100% - 85px);
}

#message-form .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);
    }

    #message-form .input-group button {
        width: 65px;
    }

    #message-form .input-group input {
        width: calc(100% - 70px);
    }

    .chat-header {
        padding: 10px;
    }

    .connecting {
        top: 60px;
    }

    .chat-header h2 {
        font-size: 1.1em;
    }
}

启动应用

若想启动该应用,在命令行执行以下命令(或者在IDEA中启动):

mvn spring-boot:run

应用将会在默认的8080端口启动,在浏览器访问http://localhost:8080即可。

使用Rabbit MQ

如果要使用RabbitMQ之类的功能全面的消息代理而不是简单的内存消息代理,我们需要先安装RabbitMQ,具体步骤可以参考下文:

RabbitMQ安装

然后配置STOMP插件:

rabbitmq-plugins enable rabbitmq_stomp

STOMP Plugin

在pom.xml文件中添加以下依赖项:

<!-- RabbitMQ Starter Dependency -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

<!-- Following additional dependency is required for Full Featured STOMP Broker Relay -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-reactor-netty</artifactId>
</dependency>

添加好以上依赖后,可以在WebSocketConfig.java文件中启用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");
}

最后,正常启动应用即可。

参考

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

原文地址:https://www.cnblogs.com/viljw/p/12397221.html

时间: 2024-07-29 08:18:48

使用SpringBoot开发群聊应用的相关文章

基于itchat的微信群聊小助手基础开发(一)

前段时间由于要管理微信群,基于itchat开发了一个简单的微信机器人 主要功能有: 图灵机器人功能 群聊昵称格式修改提示 消息防撤回功能 斗图功能 要开发一个基于itchat的最基本的聊天机器人,在github上作者已经给出基本的操作,本博文只对附加的功能进行说明总结. 确保已经了解itchat的基本操作,先对功能1和功能2进行说明: 1.图灵机器人功能 该功能也是最基础的功能之一,itchat的基础案例也有相关例子. 图灵机器人简单而言就是以一定的规则给图灵的服务器发送数据包,图灵的服务器会以

android 开发,视频群聊引发短信异常

我正在开发一个收到信息后发送一条短信进行回复的程序(自动回复),当我的手机启动群聊程序,我的程序并没有成功的发送消息,于是我采用了如下的代码解决这个问题. <intent-filter android:priority="500"> <action android:name="android.provider.Telephony.SMS_RECEIVED"/> </intent-filter> 在信息发送后,我专门记录了SMS日志

ASP.NET SignalR 与LayIM配合,轻松实现网站客服聊天室(四) 添加表情、群聊功能

休息了两天,还是决定把这个尾巴给收了.本篇是最后一篇,也算是草草收尾吧.今天要加上表情功能和群聊.基本上就差不多了,其他功能,读者可以自行扩展或者优化.至于我写的代码方面,自己也没去重构.好的,我们开始在原有的基础上添加功能. 1.表情功能 表情我用了一个jquery插件,qqFace.由于GitHub中源码已经包含,我这里不在粘贴地址.先看一下添加表情后的效果吧. 表情这里不要忘了加页面样式,表情选中样式.还有一个就是小bug就是,表情框被聊天框限制住了.另外,初始化表情插件的地方应该在有元素

基于Kurento的WebRTC移动视频群聊技术方案

说在前面的话:视频实时群聊天有三种架构: Mesh架构:终端之间互相连接,没有中心服务器,产生的问题,每个终端都要连接n-1个终端,每个终端的编码和网络压力都很大.群聊人数N不可能太大. Router架构:终端之间引入中心服务器,学名MCU(Multi Point Control Unit),每个终端的视频流都发布到MCU服务器上,然后服务器负责编码发布多视频流的工作,减轻客户端的压力. Mix架构:在Router架构基础上,多个视频流在服务器端被合为一个视频流,减轻网络压力. 下面讲我们的选择

实现在同一台服务器上登录的ssh用户的群聊(聊天室)功能

直接上代码了,注释还算清晰,有问题欢迎提问指证. 为方便下载编译,代码都放到一个文件里了. 服务器是CentOS,客户端用的secureCRT. /* 功能: 在同一台服务器上ssh登录的用户可以群聊(聊天室) 原理:<span style="white-space:pre"> </span>1.通过roomNo.来区分不同的房间或群组: 2.以roomNo.作为key来创建一块共享内存,来保存进入到该room的用户列表: 3.用户以ssh(或其它方式)登录到服

IM群聊消息的已读回执功能该怎么实现?

本文引用了架构师之路公众号作者沈剑的文章,内容有改动,感谢原作者. 1.前言 我们平时在使用即时通讯应用时候,每当发出一条聊天消息,都希望对方尽快看到,并尽快回复,但对方到底有没有真的看到?我却并不知道. 一个残酷的现实是,很多时候对方其实是早就已经看到了这条消息,但出出种种原因(大家都懂的),通常都是默默返回--假装没看见. 像微信这样的熟人社交工具,在产品的设计理念上,为了保持使用者的隐私性,在线状态.已读回执等涉及隐私的功能,都没有提供.但很多时候,尤其商务.办公场合下,特别需要一种强反馈

spring websocket 和socketjs实现单聊群聊,广播的消息推送详解

spring websocket 和socketjs实现单聊群聊,广播的消息推送详解 WebSocket简单介绍 随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了.近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据. 我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而

Java Socket通信实现私聊、群聊

前言 闲言少叙,上代码! 代码编写 server服务端 /** * 服务端 */ public class Server { private static ServerSocket server = null; private static Socket ss = null; /** * 客户端集合 */ private static Map<String, ServerThread> serverThreadMap = new HashMap<String, ServerThread&

websocket实现群聊和单聊(转)

昨日内容回顾 1.Flask路由 1.endpoint="user" # 反向url地址 2.url_address = url_for("user") 3.methods = ["GET","POST"] # 允许请求进入视图函数的方式 4.redirect_to # 在进入视图函数之前重定向 5./index/<nid> # 动态参数路由 <int:nid> def index(nid) 6.str