Spring Boot教程32——WebSocket

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Boot教程32——WebSocket相关的知识,希望对你有一定的参考价值。

WebSocket为浏览器和服务端提供了双工异步通信功能,即浏览器可以向服务端发送消息,服务端也可以向浏览器发送消息。WebSocket需要IE10+、Chrome13+、Firefox6+。
WebSocket是通过一个socket来实现双工异步通信能力的。但直接使用WebSocket协议开发程序比较繁琐,我们会使用它的子协议STOMP,它是一个更高级别的协议,使用一个基于帧(frame)的格式来定义消息,与Http的request和response类似(具有类似于@RequestMapping的@MessageMapping)。

Spring Boot对内嵌的Tomcat(7或者8)、Jetty9和Undertow使用WebSocket提供了支持。配置源码存于org.springframework.boot.autoconfigure.websocket下。
技术分享?

实战

1.新建Spring Boot项目

选择Thymeleaf和Websocket依赖
技术分享?

2.广播式

广播式即服务端有消息时,会将消息发送给所有连接了当前endpoint的浏览器。

1>.配置WebSocket

需要在配置类上使用@EnableWebSocketMessageBroker开启WebSocket支持,并通过继承AbsractWebSocketMessageBrokerConfigurer类,重写其方法来配置WebSocket。

package net.quickcodes.websocket;

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

@Configuration
@EnableWebSocketMessageBroker //1.通过@EnableWebSocketMessageBroker注解开启使用STOMP协议来传输基于代理(message broker)的消息,这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样。
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {//2.注册STOMP协议的节点(endpoint),并映射为指定的URL
        registry.addEndpoint("/endpointQuickcodes").withSockJS();//3.注册一个STOMP的endpoint,并指定使用SockJS协议。
    }


    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {//4.配置消息代理(Message Broker)
        registry.enableSimpleBroker("/topic"); //5.广播式应配置一个/topic消息代理。
    }

}

 

2>.浏览器向服务端发送的消息用此类接受

package net.quickcodes.websocket.domain;

public class QuickCodesMessage {
    private String name;

    public String getName(){
        return name;
    }
}

 

3>.服务端向浏览器发送的消息用此类接受

package net.quickcodes.websocket.domain;

public class QuickCodesResponse {
    private String responseMessage;
    public QuickCodesResponse(String responseMessage){
        this.responseMessage = responseMessage;
    }
    public String getResponseMessage(){
        return responseMessage;
    }
}

 

4>.演示控制器

package net.quickcodes.websocket.web;

import java.security.Principal;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

import net.quickcodes.websocket.domain.QuickCodesMessage;
import net.quickcodes.websocket.domain.QuickCodesResponse;

@Controller
public class QcController {
    @MessageMapping("/welcome")//1.当浏览器向服务端发送请求时,通过@MessageMapping映射/welcome这个地址,类似于@RequestMapping
    @SendTo("/topic/getResponse")//2.当服务端有消息时,会对订阅了@SendTo中的路径的浏览器发送消息
    public QuickCodesResponse say(QuickCodesMessage message) throws Exception{
        Thread.sleep(3000);
        return new QuickCodesResponse("Welcome, "+message.getName() + "!");
    }
}

 

5>.添加脚本

将stomp.min.js(STOMP协议的客户端脚本)、sockjs.min.js(SockJS的客户端脚本)以及jQuery放置在src/main/resources/static下。

6>.演示页面

在src/main/resources/templates下新建qc.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>Spring Boot+WebSocket+广播式</title>

</head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">貌似你的浏览器不支持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 th:src="@{sockjs.min.js}"></script>
<script th:src="@{stomp.min.js}"></script>
<script th:src="@{jquery.js}"></script>
<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(/endpointQuickcodes); //1.连接SockJS的endpoint名称为/endpointQuickcodes
        stompClient = Stomp.over(socket);//2.使用WebSocket子协议的STOMP客户端
        stompClient.connect({}, function(frame) {//3.连接WebSocket服务端
            setConnected(true);
            console.log(Connected:  + frame);
            stompClient.subscribe(/topic/getResponse, function(respnose){ //4.通过stompClient.subscribe订阅/topic/getResponse目标(destination)发送的消息,这个是在控制器的@SendTo中定义的。
                showResponse(JSON.parse(respnose.body).responseMessage);
            });
        });
    }


    function disconnect() {
        if (stompClient != null) {
            stompClient.disconnect();
        }
        setConnected(false);
        console.log("Disconnected");
    }

    function sendName() {
        var name = $(#name).val();
        //5.通过stompClient.send向/welcome目标发送消息,这个是在控制器的@MessageMapping中定义的。
        stompClient.send("/welcome", {}, JSON.stringify({ name: name }));
    }

    function showResponse(message) {
        var response = $("#response");
        response.html(message);
    }
</script>
</body>
</html>

 

7>.配置viewController

为qc.html提供便捷的路径映射

package net.quickcodes.websocket;


import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;

@Configuration
public class WebMvcConfig extends WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter {
    @Override
    public void addViewControllers(ViewControllerRegistry registry){
        registry.addViewController("/qc").setViewName("/qc");
    }
}

 

8>.运行

开启三个浏览器,并都访问http://localhost:8080/qc,分别连接服务器。然后在一个浏览器中发送一条消息,其他浏览器接收消息。

3.点对点式

广播式有自己的应用场景,但不能解决我们的一个常见场景,即消息由谁发送,就由谁接收的场景。
本例演示一个简单聊天室程序。例子中只有两个用户,互相发消息给彼此,因需要用户相关的内容,所以在这里引入最简单的Spring Security相关内容。

1>.在pom.xml添加Spring Security的starter pom:

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

 

2>.Spring Security的简单配置

package net.quickcodes.websocket;

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;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/","/login").permitAll()//1根路径和/login路径不拦截
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login") //2登陆页面
                .defaultSuccessUrl("/chat") //3登陆成功转向该页面
                .permitAll()
                .and()
                .logout()
                .permitAll();
    }

    //4
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .withUser("manon").password("manon").roles("USER")
                .and()
                .withUser("qc").password("qc").roles("USER");
    }
    //5忽略静态资源的拦截
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/resources/static/**");
    }
}

 

3>.配置WebSocket

package net.quickcodes.websocket;

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

@Configuration
@EnableWebSocketMessageBrokerpublic class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/endpointQuickcodes").withSockJS();
        registry.addEndpoint("/endpointChat");//注册一个名为/endpointChat的endpoint
    }


    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/queue","/topic"); //增加一个/queue消息代理
    }

}

 

4>.控制器

package net.quickcodes.websocket.web;

import java.security.Principal;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;

import net.quickcodes.websocket.domain.QuickCodesMessage;
import net.quickcodes.websocket.domain.QuickCodesResponse;

@Controller
public class QcController {
    @MessageMapping("/welcome")
    @SendTo("/topic/getResponse")
    public QuickCodesResponse say(QuickCodesMessage message) throws Exception{
        Thread.sleep(3000);
        return new QuickCodesResponse("Welcome, "+message.getName() + "!");
    }


    @Autowired
    private SimpMessagingTemplate messagingTemplate;//1.通过SimpMessagingTemplate向浏览器发送消息

    @MessageMapping("/chat")
    public void handleChat(Principal principal, String msg) { //2.在Spring MVC中,可以直接在参数中获得principal,principal中包含当前用户的信息
        if (principal.getName().equals("qc")) {//3.这是一段硬编码,如果发送人是qc,则发送给manon;如果发送人是manon,则发送给qc,可根据项目实际需要改写此处代码
            messagingTemplate.convertAndSendToUser("manon",
                    "/queue/notifications", principal.getName() + "-send:"
                            + msg);//4.通过messagingTemplate.convertAndSendToUser向用户发送消息,第一个参数是接收消息的用户,第二个是浏览器订阅的地址见,第三个是消息本身。
        } else {
            messagingTemplate.convertAndSendToUser("qc",
                    "/queue/notifications", principal.getName() + "-send:"
                            + msg);
        }
    }
}

 

5>.登陆页面

src/main/resources/templates/login.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<meta charset="UTF-8" />
<head>
    <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>

 

6>.聊天页面

src/main/resources/templates/chat.html

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8" />
<head>
    <title>Home</title>
    <script th:src="@{sockjs.min.js}"></script>
    <script th:src="@{stomp.min.js}"></script>
    <script th:src="@{jquery.js}"></script>
</head>
<body>
<p>
    聊天室
</p>

<form id="wiselyForm">
    <textarea rows="4" cols="60" name="text"></textarea>
    <input type="submit"/>
</form>

<script th:inline="javascript">
    $(#wiselyForm).submit(function(e){
        e.preventDefault();
        var text = $(#wiselyForm).find(textarea[name="text"]).val();
        sendSpittle(text);
    });

    var sock = new SockJS("/endpointChat"); //1.连接名称为/endpointChat的endpoint
    var stomp = Stomp.over(sock);
    stomp.connect(guest, guest, function(frame) {
        stomp.subscribe("/user/queue/notifications", handleNotification);//2.订阅/user/queue/notifications发送的消息,这里与控制器的messagingTemplate.convertAndSendToUser中定义的订阅地址保持一致。这里多了一个/user,并且这个/user是必须的,使用了/user才会发送消息到指定的用户
    });



    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>

 

7>.增加页面的viewController

package net.quickcodes.websocket;


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

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter{
    @Override
    public void addViewControllers(ViewControllerRegistry registry){
        registry.addViewController("/qc").setViewName("/qc");
        registry.addViewController("/login").setViewName("/login");
        registry.addViewController("/chat").setViewName("/chat");
    }
}

 

8>.运行

分别在两个用户的浏览器下访问http://localhost:8080/login并分别用不同用户名登陆,然后互发消息

以上是关于Spring Boot教程32——WebSocket的主要内容,如果未能解决你的问题,请参考以下文章

一款Java开源的Spring Boot即时通讯IM聊天系统

Angular 基本身份验证 Spring Boot

Spring Boot简明教程--Spring Boot版本号介绍

Spring Boot入门教程大纲

spring boot教程 网盘下载

websocke 在线测试地址