A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

通过本文你将学习如何使用Spring Boot和WebSocket API开发一个简单的群聊天应用。
WebSocket是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务器主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
很多网站为实现推送技术,所用技术都是Ajax轮询。轮询指的是在特定的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后服务器返回最新的数据给浏览器。这种传统的模式有很明显的缺点,即浏览器需要不断的向服务器发出请求,而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,这样就会浪费很多资源。HTML5定义的WebSocket协议能更好的节省服务器带宽等资源,并能够实时地进行你通讯。
详情请看HTML5 WebSocket
新建项目
打开IDEA,选择Spring Initializer
填写好相关信息:
依赖选择Spring Web和WebSocket
之后选择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@EnableWebSocketMessageBrokerpublic 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;@Controllerpublic 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;@Componentpublic 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库——sockjs和stomp。
SockJS是一个WebSocket客户端,它尝试使用WebSocket,并为不支持WebSocket的旧版浏览器提供备用选项。 stomp是javascript的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方法用SockJS和stomp客户端连接到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,具体步骤可以参考下文:
然后配置STOMP插件:
rabbitmq-plugins enable rabbitmq_stomp
在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");}
最后,正常启动应用即可。


1 个回复

倒序浏览
有问题欢迎联系小优:DKA-2018
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马