SpringBoot+WebSocket实现消息推送及简单的聊天功能

版权声明: https://blog.csdn.net/typ1805/article/details/83150055

一、WebSocket简介

      WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。

       WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

      原理:在实现websocket连线过程中,需要通过浏览器发出websocket连线请求,然后服务器发出回应,这个过程通常称为“握手” 。在 WebSocket API,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。在此WebSocket 协议中,为我们实现即时服务带来了两大好处:

  • 1. Header:互相沟通的Header是很小的-大概只有 2 Bytes
  • 2. Server Push:服务器的推送,服务器不再被动的接收到浏览器的请求之后才返回数据,而是在有新数据时就主动推送给浏览器。

二、实现消息推送

1、添加websocke的t依赖

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-websocket</artifactId>
		</dependency>

2、webSocket配置,添加@EnableWebSocketMessageBroker注解开启TOMP协议

package com.example.demo.websocket.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;

/**
 * 路径:com.example.demo.websocket.config
 * 类名:
 * 功能:webSocket配置
 * 备注:@EnableWebSocketMessageBroker注解表示开启使用STOMP协议来传输基于代理的消息,Broker就是代理的意思;
 * 创建人:typ
 * 创建时间:2018/10/18 10:15
 * 修改人:
 * 修改备注:
 * 修改时间:
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    /**
     * 方法名:
     * 功能:《用一句话描述一下》
     * 描述:注册STOMP协议的节点,并指定映射的URL
     * 创建人:typ
     * 创建时间:2018/10/18 10:21
     * 修改人:
     * 修改描述:
     * 修改时间:
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //注册STOMP协议节点,同时指定使用SockJS协议
        registry.addEndpoint("/endpointSang").withSockJS();
    }

    /**
     * 方法名:
     * 功能:《用一句话描述一下》
     * 描述:配置消息代理,由于我们是实现推送功能,这里的消息代理是/topic
     * 创建人:typ
     * 创建时间:2018/10/18 10:22
     * 修改人:
     * 修改描述:
     * 修改时间:
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic");
    }
}

3、接受消息的实体

package com.example.demo.websocket.entity;

/**
 * 路径:com.example.demo.websocket.entity
 * 类名:
 * 功能:接受消息的实体
 * 备注:
 * 创建人:typ
 * 创建时间:2018/10/18 11:03
 * 修改人:
 * 修改备注:
 * 修改时间:
 */
public class RequestMessage {

    private String name;

    public String getName() {
        return name;
    }
}

4、响应消息的实体

package com.example.demo.websocket.entity;

/**
 * 路径:com.example.demo.websocket.entity
 * 类名:
 * 功能:响应消息的实体
 * 备注:
 * 创建人:typ
 * 创建时间:2018/10/18 11:03
 * 修改人:
 * 修改备注:
 * 修改时间:
 */
public class ResponseMessage {
    private String responseMessage;

    public ResponseMessage(String responseMessage) {
        this.responseMessage = responseMessage;
    }

    public String getResponseMessage() {
        return responseMessage;
    }
}

5、配置viewController

package com.example.demo.websocket.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

/**
 * 路径:com.example.demo.websocket.config
 * 类名:
 * 功能:《用一句描述一下》
 * 备注:为ws.html提供路径映射
 * 创建人:typ
 * 创建时间:2018/10/18 10:34
 * 修改人:
 * 修改备注:
 * 修改时间:
 */
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/ws").setViewName("/ws");
    }

}

6、创建Controller,添加@SendTo注解表示当服务器有消息需要推送的时候,会对订阅了SendTo中路径的浏览器发送消息。

package com.example.demo.websocket.controller;

import com.example.demo.websocket.entity.RequestMessage;
import com.example.demo.websocket.entity.ResponseMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;

/**
 * 路径:com.example.demo.websocket.controller
 * 类名:
 * 功能:《用一句描述一下》
 * 备注:
 * 创建人:typ
 * 创建时间:2018/10/18 10:26
 * 修改人:
 * 修改备注:
 * 修改时间:
 */
@Slf4j
@Controller
public class WsController {

    /**
     * 方法名:
     * 功能:《用一句话描述一下》
     * 描述:@MessageMapping注解和我们之前使用的@RequestMapping类似;
     *      @SendTo注解表示当服务器有消息需要推送的时候,会对订阅了@SendTo中路径的浏览器发送消息。
     * 创建人:typ
     * 创建时间:2018/10/18 10:28
     * 修改人:
     * 修改描述:
     * 修改时间:
     */
    @MessageMapping("/welcome")
    @SendTo("/topic/getResponse")
    public ResponseMessage say(@RequestBody RequestMessage message){
        log.info("消息:{}",message.getName());
        String mag = "welcom," + message.getName() + "!";
        return new ResponseMessage(mag);
    }
}

7、测试页面

引入js脚本文件,分别是STOMP协议的客户端脚本stomp.js、SockJS的客户端脚本sock.js以及jQuery,放到src/main/resources/static/js目录下。

在src/main/resources/templates目录下新建一个ws.html页面,如下:

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>WebSocket</title>
    <script th:src="@{js/sockjs.min.js}"></script>
    <script th:src="@{js/stomp.js}"></script>
    <script th:src="@{js/jquery-3.1.1.js}"></script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #e80b0a;">浏览器不支持WebSocket</h2></noscript>
<div>
    <div>
        <button id="connect" onclick="connect();">连接</button>
        <button id="disconnect" disabled="disabled" onclick="disconnect();">断开连接</button>
    </div>

    <div id="conversationDiv">
        <label>输入你的名字</label><input type="text" id="name"/>
        <button id="sendName" onclick="sendName();">发送</button>
        <p id="response"></p>
    </div>
</div>
<script type="text/javascript">
    var stompClient = null;
    function setConnected(connected) {
        document.getElementById("connect").disabled = connected;
        document.getElementById("disconnect").disabled = !connected;
        document.getElementById("conversationDiv").style.visibility = connected ? 'visible' : 'hidden';
        $("#response").html();
    }
    function connect() {
        var socket = new SockJS('/endpointSang');
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function (frame) {
            setConnected(true);
            console.log('Connected:' + frame);
            stompClient.subscribe('/topic/getResponse', function (response) {
                showResponse(JSON.parse(response.body).responseMessage);
            })
        });
    }
    function disconnect() {
        if (stompClient != null) {
            stompClient.disconnect();
        }
        setConnected(false);
        console.log('Disconnected');
    }
    function sendName() {
        var name = $('#name').val();
        console.log('name:' + name);
        stompClient.send("/welcome", {}, JSON.stringify({'name': name}));
    }
    function showResponse(message) {
        $("#response").html(message);
    }
</script>
</body>
</html>

启动工程测试,打开两个浏览器访问:http://localhost:8081/ws

 

三、实现聊天功能

1、使用SpringSecurity管理权限,添加依赖

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

2、WebSocket配置文件

package com.example.demo.websocket.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;

/**
 * 路径:com.example.demo.websocket.config
 * 类名:
 * 功能:webSocket配置
 * 备注:@EnableWebSocketMessageBroker注解表示开启使用STOMP协议来传输基于代理的消息,Broker就是代理的意思;
 * 创建人:typ
 * 创建时间:2018/10/18 11:48
 * 修改人:
 * 修改备注:
 * 修改时间:
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer{

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //注册STOMP协议节点,同时指定使用SockJS协议
        registry.addEndpoint("/endpointChat").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/queue");
    }
}


3、页面映射配置文件

package com.example.demo.websocket.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

/**
 * 路径:com.example.demo.websocket.config
 * 类名:
 * 功能:《用一句描述一下》
 * 备注:
 * 创建人:typ
 * 创建时间:2018/10/18 11:52
 * 修改人:
 * 修改备注:
 * 修改时间:
 */
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter{

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("/login");
        registry.addViewController("/chat").setViewName("/chat");
    }
}

4、SpringSecurity配置文件,添加@EnableWebSecurity注解,设置登录用户及密码

​package com.example.demo.websocket.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * 路径:com.example.demo.websocket.config
 * 类名:
 * 功能:《用一句描述一下》
 * 备注:
 * 创建人:typ
 * 创建时间:2018/10/18 11:53
 * 修改人:
 * 修改备注:
 * 修改时间:
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    /**
     * 方法名:
     * 功能:先设置拦截规则,设置默认登录页面以及登录成功后的跳转页面
     * 描述:
     * 创建人:typ
     * 创建时间:2018/10/18 21:07 
     * 修改人:
     * 修改描述:
     * 修改时间:
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //设置拦截器
                .antMatchers("/")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                //开启默认登录页面
                .formLogin()
                //默认登录页面
                .loginPage("/login")
                //默认登录成功跳转页面
                .defaultSuccessUrl("/chat")
                .permitAll()
                .and()
                //设置注销
                .logout()
                .permitAll();
    }

    /**
     * 方法名:
     * 功能:定义两个用户,设置用户名、用户密码、用户角色等信息。
     * 描述:
     * 创建人:typ
     * 创建时间:2018/10/18 21:07
     * 修改人:
     * 修改描述:
     * 修改时间:
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .passwordEncoder(new PasswordEncoderConfig())
                .withUser("admin").password("admin").roles("USER")
                .and()
                .withUser("root").password("root").roles("USER");
    }

    /**
     * 方法名:
     * 功能:设置静态资源不被拦截。
     * 描述:
     * 创建人:typ
     * 创建时间:2018/10/18 21:08 
     * 修改人:
     * 修改描述:
     * 修改时间:
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        //设置不拦截规则
        web.ignoring().antMatchers("/resources/static/**");
    }
}

5、提供一个PasswordEncorderConfig的实例,实现PasswordEncorder接口

在springboot2.0.3以上版本中会报错:

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

package com.example.demo.websocket.config;

import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 路径:com.example.demo.websocket.config
 * 类名:
 * 功能:提供一个PasswordEncorder的实例,否则后台会报错误
 * 备注:
 * 创建人:typ
 * 创建时间:2018/10/18 11:48
 * 修改人:
 * 修改备注:
 * 修改时间:
 */
public class PasswordEncoderConfig implements PasswordEncoder {

    @Override
    public String encode(CharSequence charSequence) {
        return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return s.equals(charSequence.toString());
    }
}

6、控制器controller配置文件

package com.example.demo.websocket.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;

import java.security.Principal;

/**
 * 路径:com.example.demo.websocket.controller
 * 类名:
 * 功能:webSocket实现聊天室
 * 备注:
 * 创建人:typ
 * 创建时间:2018/10/18 11:48
 * 修改人:
 * 修改备注:
 * 修改时间:
 */
@Slf4j
@Controller
public class WsController {
    
    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    /**
     * 方法名:
     * 功能:《用一句话描述一下》
     * 描述:
     * 创建人:typ
     * 创建时间:2018/10/18 12:04 
     * 修改人:
     * 修改描述:
     * 修改时间:
     */
    @MessageMapping("/chat")
    public void handleChat(Principal principal, String msg) {
        log.info("name:{} ,msg:{},",principal.getName(),msg);
        if (principal.getName().equals("admin")) {
            messagingTemplate.convertAndSendToUser("admin", "/queue/notifications", principal.getName() + "给您发来了消息:" + msg);
        }else{
            messagingTemplate.convertAndSendToUser("root", "/queue/notifications", principal.getName() + "给您发来了消息:" + msg);
        }
    }
}

7、创建登录和聊天页面,在src/main/resources/templates目录下新建login.html和chat.html文件

     stomp中的connect方法用来连接服务端,连接成功之后注册监听,在注册监听的时候,注册的地址/user/queue/notifications比WebSocket配置文件中的多了一个/user,这个/user是必不可少的,使用了它消息才会点对点传送。 收到消息后在handleNotification方法中处理,实际上就是把收到的内容添加到id为output的div中。
依赖的静态js文件消息推送中的一致,具体HTML代码如下:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <meta charset="UTF-8" />
    <title>登录</title>
</head>
<body>
<div th:if="${param.error}">
    无效的账号或密码
</div>
<div th:if="${param.logout}">
    你已注销
</div>
<form th:action="@{/login}" method="post">
    <div><label>账号:<input type="text" name="username" /></label></div>
    <div><label>密码:<input type="password" name="password" /></label></div>
    <div><input type="submit" value="登录" /></div>
</form>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>聊天室</title>
    <script th:src="@{js/sockjs.min.js}"></script>
    <script th:src="@{js/stomp.js}"></script>
    <script th:src="@{js/jquery-3.1.1.js}"></script>
</head>
<body>
<p>聊天室</p>
<form id="sangForm">
    <textarea rows="4" cols="60" name="text"></textarea>
    <input type="submit" value="发送"/>
</form>
<script th:inline="javascript">
    $("#sangForm").submit(function (e) {
        e.preventDefault();
        var textArea = $("#sangForm").find('textarea[name="text"]');
        var text = textArea.val();
        sendSpittle(text);
        textArea.val('');
    });
    var sock = new SockJS("/endpointChat");
    var stomp = Stomp.over(sock);
    stomp.connect('guest','guest',function (frame) {
        stomp.subscribe("/user/queue/notifications", handleNotification);
    });
    function handleNotification(message) {
        $("#output").append("<b>Received: "+message.body+"</b><br/>")
    }
    function sendSpittle(text) {
        stomp.send("/chat", {}, text);
    }
    $("#stop").click(function () {
        sock.close();
    });
</script>
<div id="output"></div>
</body>
</html>

启动工程测试,在不同的两个浏览器中用不同的用户登录,效果入下:

下载源码:https://download.csdn.net/download/typ1805/10730574

猜你喜欢

转载自blog.csdn.net/typ1805/article/details/83150055