24-SpringBoot——核心-WebSocket

Spring Boot & Spring Cloud 同时被 2 个专栏收录
43 篇文章 0 订阅
32 篇文章 16 订阅

SpringBoot——核心-WebSocket


【博文目录>>>】


【项目源码>>>】


【WebSocket】


WebSocket 为浏览器和服务端提供了双工异步通信的功能,即浏览器可以向服务端发送消息,服务端也可以向浏览器发送消息。WebSocket 需浏览器的支持,如IE 10+、Chrome 13+,Firefox 6+,这对我们现在的浏览器来说都不是问题。

WebSocket 是通过一个socket 来实现双工异步通信能力的。但是直接使用WebSocket (或者SockJS: WebSocket 协议的模拟,增加了当浏览器不支持WebSocket 的时候的兼容支持)协议开发程序显得特别烦琐,我们会使用它的子协议STOMP ,它是一个更高级别的协议, STOMP协议使用一个基于帧(企ame )的格式来定义消息,与HTTP 的request 和response 类似(具有类似于@RequestMappir屯的@MessageMapping ),我们会在后面实战内容中观察STOMP 的帧。

Spring Boot 对内嵌的Tomcat (7 或者8 )、Jetty9 和Undertow 使用WebSocket 提供了支持。配置源码存于org.springframework.boot.autoconfigure.websocket。Spring Boot 为WebSockct 捉供的stater pom 是spring-boot-starter-websocket

广播式


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

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

当一个浏览器发送一个消息到服务端时,其他浏览器也能接收到从服务端发送来的这个消息。开启三个浏览器窗口,并访问http://localhost: 8080/ws ,分别连接服务器。然后在一个浏览器中发送一条消息,其他浏览器接收消息。连接服务端,可以看到进行效果。所有浏览器接收服务端发送的消息。

【代码实现】


ch0707-SpringBoot-Web-Websocket

package com.example.spring.boot.websocket;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Author: 王俊超
 * Date: 2017-07-17 07:44
 * All Rights Reserved !!!
 */
@SpringBootApplication
public class SampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(SampleApplication.class, args);
    }
}
package com.example.spring.boot.websocket;

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

/**
 * Author: 王俊超
 * Date: 2017-07-17 07:39
 * All Rights Reserved !!!
 */
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/ws").setViewName("/ws");
    }
}
package com.example.spring.boot.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;

/**
 * 通过@EnableWebSocketMessageBroker 注解开启使用STOMP 协议来传输基于代理message broker )的消息,
 * 这时控制器支持使用@MessageMapping ,就像使用@RequestMapping一样。
 * Author: 王俊超
 * Date: 2017-07-17 07:32
 * All Rights Reserved !!!
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
    /**
     * 注册STOMP 协议的节点( endpoint ),并映射的指定的URL
     * @param registry
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 泣册一个STOMP 的endpoint ,并指定使用SockJS 协议
        registry.addEndpoint("/endpointWisely").withSockJS();
    }

    /**
     * 配置 消息代理( Message Broker )。
     * @param registry
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // t·:r苗式应配捏一个/topic 消息代现。
        registry.enableSimpleBroker("/topic");
    }
}
package com.example.spring.boot.websocket;

/**
 * Author: 王俊超
 * Date: 2017-07-17 07:35
 * All Rights Reserved !!!
 */
public class WiselyMessage {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
package com.example.spring.boot.websocket;

/**
 * Author: 王俊超
 * Date: 2017-07-17 07:38
 * All Rights Reserved !!!
 */
public class WiselyResponse {

    private String responseMessage;

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

    public String getResponseMessage() {
        return responseMessage;
    }
}
package com.example.spring.boot.websocket;

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

/**
 * Author: 王俊超
 * Date: 2017-07-17 07:36
 * All Rights Reserved !!!
 */
@Controller
public class WsController {
    // ①当浏览器向服务端发送请求时,通过@MessageMapping 映射/welcome 这个地址,
    // 类似于@RequestMapping
    @MessageMapping("/welcome")
    // 当服务端有消息时,会对订阅了@SendTo 中的路径的浏览器发送消息。
    @SendTo("/topic/getResponse")
    public WiselyResponse say(WiselyMessage message) throws InterruptedException {
        Thread.sleep(3000);
        return new WiselyResponse("Welcome, " +  message.getName());
    }
}
<!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() {
        // 连接SockJS 的endpoint 名称为“/endpointWisely”
        var socket = new SockJS('/endpointWisely');
        // 使用STOMP 子协议的WebSocket 客户端
        stompClient = Stomp.over(socket);
        // 连接WebSocket 服务端。
        stompClient.connect({}, function (frame) {
            setConnected(true);
            console.log('Connected: ' + frame);
            stompClient.subscribe('/topic/getResponse', function (respnose) {
                // 通过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();
        // 通过stompClient.send 向/welcome 目标( destination )发送消息,
        // 这个是在控制器的@MessageMapping 中定义的。
        stompClient.send("/welcome", {}, JSON.stringify({'name': name}));
    }

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

点对点式


广播式有自己的应用场景,但是广播式不能解决我们一个常见的场景,即消息由谁发送、由谁接收的问题。

【代码实现】


ch0708-SpringBoot-Web-Websocket-点对点通讯

package com.example.spring.boot.websocket.p2p;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Author: 王俊超
 * Date: 2017-07-17 07:44
 * All Rights Reserved !!!
 */
@SpringBootApplication
public class SampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(SampleApplication.class, args);
    }
}
package com.example.spring.boot.websocket.p2p;

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

/**
 * Author: 王俊超
 * Date: 2017-07-17 08:01
 * All Rights Reserved !!!
 */
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("/login");
        registry.addViewController("/chat").setViewName("/chat");
    }
}
package com.example.spring.boot.websocket.p2p;

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;

/**
 * Author: 王俊超
 * Date: 2017-07-17 07:51
 * All Rights Reserved !!!
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/", "/login").permitAll() // 设置Spring Security 对/和/login路径不拦截。
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login") // 设置Spring Security 的登录页面访问的路径为/login
                .defaultSuccessUrl("/chat") // 登录成功后转向/chat 路径。
                .permitAll()
                .and()
                .logout()
                .permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 在内存中分别配置两个用户wjc和wisely,密码和用户名一致,角色是USER
        auth.inMemoryAuthentication()
                .withUser("wjc").password("wjc").roles("USER")
                .and()
                .withUser("wisely").password("wisely").roles("USER");
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        // /resources/static/目录下的静态资源, Spring Security 不拦截。
        web.ignoring().antMatchers("/resources/static/*");
    }
}
package com.example.spring.boot.websocket.p2p;

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

/**
 * Author: 王俊超
 * Date: 2017-07-17 07:56
 * All Rights Reserved !!!
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 注册一个名为/endpointChat的endpoint
        registry.addEndpoint("/endpointChat").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 点对点式应增加一个/queue 消息代理。
        registry.enableSimpleBroker("/queue", "/topic");
    }
}
package com.example.spring.boot.websocket.p2p;

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;

/**
 * Author: 王俊超
 * Date: 2017-07-17 07:57
 * All Rights Reserved !!!
 */
@Controller
public class WsController {
    @Autowired
    private SimpMessagingTemplate template;
    @MessageMapping("/chat")
    public void handleChat(Principal principal, String message) {
        if (principal.getName().equals("wjc")) {
            template.convertAndSendToUser("wisely", "/queue/notifications",
                    principal.getName() + "-send: " + message);
        }else {
            template.convertAndSendToUser("wjc", "/queue/notifications",
                    principal.getName() + "-send: " + message);
        }
    }
}
<!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
    var stomp = Stomp.over(sock);
    stomp.connect('guest', 'guest', function (frame) {
        stomp.subscribe("/user/queue/notifications", handleNotification);//2
    });


    function handleNotification(message) {
        $('#output').append("<b>Received: " + message.body + "</b><br/>")
    }

    function sendSpittle(text) {
        stomp.send("/chat", {}, text);//3
    }
    $('#stop').click(function () {
        sock.close()
    });
</script>

<div id="output"></div>
</body>
</html>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<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>
  • 2
    点赞
  • 1
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

打赏
文章很值,打赏犒劳作者一下
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页

打赏

Wang-Junchao

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值