WebSocket实战(广播式、点对点式)

WebSocket实战(广播式、点对点式)

  • WebSocket为浏览器和服务端提供了双工异步通信的功能,即浏览器可以向服务端发送消息,服务端也可以向浏览器发送消息。但是直接使用WebSocket或者SockJS,WebSocket协议的模拟,增加了当浏览器不支持WebSocket的时候的兼容支持,协议开发程序显得特别麻烦,我们会使用它的子协议STOMP,它是使用一个基于帧(frame)的格式来定义消息,与HTTP的requesth和和responses类似。

添加依赖:

<dependency>
   <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-thymeleafartifactId>
dependency>

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-websocketartifactId>
    <version>2.1.7.RELEASEversion>
dependency>
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-securityartifactId>
    <version>2.1.8.RELEASEversion>
dependency>

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

  1. 配置WebSocket
/**
 * 配置WebSocket
 * 广播式   (将消息发送给所有连接了当前endpoint的浏览器)
 */

@Configuration
@EnableWebSocketMessageBroker //注解开启使用STOMP协议来传输基于代理(message broker)的消息
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {
    //注册STOMP协议的节点
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //映射指定的 URL,指定使用SockJS协议
        registry.addEndpoint("/endpointWisely").withSockJS();  //注册STOMP协议的节点endpoint,并指定SockJS协议
        
    }

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

(2)浏览器向服务端发送的消息用此类接收

public class WiselyMessage {
    private String name;
 
    public String getName() {
        return name;
    }
}

(3)服务端向浏览器发送此类的消息

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

(4) 控制器示例

@Controller
public class WsController {

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

    @MessageMapping("/welcome")  //类似RequestMapping,地址映射
    @SendTo("/topic/getResponse")//当服务端有消息时,会对订阅了@SendTo中的路径的浏览器发送消息
    public WiselyResponse say(WiselyMessage message) throws Exception{
        Thread.sleep(3000);
        return new WiselyResponse("welcome,"+ message.getName()+"!");
    }
}

(5)添加脚本

  • stomp.min.js STOPM协议的客户端脚本
  • sockjs.min.js SockJS的客户端脚本
  • jquery

(6) 演示页面 在src/main/resources/templates下新建 ws.html, 下载js包,添加thymeleaf依赖


<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" xmlns="http://www.w3.org/1999/xhtml">
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>


<script th:src="@{/static/jquery-3.3.1.js}" type="text/javascript">script>
<script th:src="@{/static/sockjs.min.js}" type="text/javascript">script>
<script th:src="@{/static/stomp.min.js}" type="text/javascript">script>

<head>
    <title>Spring Boot+WebSocket+广播式title>

head>

<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">貌似你的浏览器不支持websocketh2>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('/endpointWisely'); //1    连接SockJS的endpoint名称为  “/endpointWisely”
        stompClient = Stomp.over(socket);  //2  使用STOMP子协议WebSocket客户端
        //3 连接WebSocket服务端
        stompClient.connect({}, function(frame) {
            setConnected(true);
            console.log('Connected: ' + frame);
            //4   订阅服务端发送的消息,这个是在控制器的@SendTo中定义的
            stompClient.subscribe('/topic/getResponse', function(respnose){ //2
                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   向服务端接口发送消息,这个在控制器@MessageMapping中定义的
        stompClient.send("/welcome", {}, JSON.stringify({ 'name': name }));
    }

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

(7)配置viewController

@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
   @Override
   public void addViewControllers(ViewControllerRegistry registry){
       //访问localhost:8787/ws  跳转ws.html页面
       registry.addViewController("/ws").setViewName("/ws");

   }
}

ps:开启3个浏览器窗口,并访问http://localhost:8787/ws

点对点的广播方式

  • 例子:简单的聊天程序,两个用户互相发送消息给彼此。
    (1)添加安全依赖 spring-boot-starter-security
    (2)Spring Security的简单配置
@Configuration
@EnableWebSecurity  //
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.
                authorizeRequests()
                //1    对/  和  /login  路径不拦截
                .antMatchers("/","/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                //2    设置登录页面的路径
                .loginPage("/login")
                //3     成功后跳转至chat路径
                .defaultSuccessUrl("/chat")
                .permitAll()
                .and()
                .logout()
                .permitAll();

    }

    //4  在内存中配置两个用户名、密码、角色,定义认证用于信息获取来源以及密码校验规则
    //认证信息获取来源是内存获取——inMemoryAuthentication
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{

        //代码报错  There is no PasswordEncoder mapped for the id “null”
        //https://blog.csdn.net/canon_in_d_major/article/details/79675033
        //Spring security 5.0中新增了多种加密方式,也改变了密码的格式
        //要将前端传过来的密码进行某种方式加密,spring security 官方推荐的是使用bcrypt加密方式
        /*auth
                .inMemoryAuthentication()
                .withUser("mmz").password("123456").roles("USER")
                .and()
                .withUser("wisely").password("123456").roles("USER");*/

        auth
                .inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("gdg").password(new BCryptPasswordEncoder().encode("123456")).roles("USER")
                .and()
                .withUser("yq").password(new BCryptPasswordEncoder().encode("123456")).roles("USER");

    }

    //5  指定路径下的静态资源不拦截
    @Override
    public void configure(WebSecurity webSecurity) throws Exception{
        webSecurity.ignoring().antMatchers("/resources/static/**");
    }
}

(3) 配置WebSocket

/**
 * 配置WebSocket
 * 广播式   (将消息发送给所有连接了当前endpoint的浏览器)
 */

@Configuration
@EnableWebSocketMessageBroker //注解开启使用STOMP协议来传输基于代理(message broker)的消息
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {
    //注册STOMP协议的节点
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //映射指定的 URL,指定使用SockJS协议
        registry.addEndpoint("/endpointWisely").withSockJS();
        // 1  注册一个新的endpoint
        registry.addEndpoint("/endpointChat").withSockJS();
    }

    //配置消息代理
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        //广播式应该配置一个topic代理
        registry.enableSimpleBroker("/queue","/topic"); //2  点对点增加一个消息代理  queue
    }
}

(4) 控制器示例

@Controller
public class WsController {

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

    @MessageMapping("/chat")
    public void handleChat(Principal principal, String msg){  //2   springMVC 中可以直接在参数中获取principal,它包含了当前用户的信息
        //3  如果发送人是mmz,则发送给wisely,暂时硬编码测试,生产中看情况
        if(principal.getName().equals("gdg")){
            //4 向用户发送消息,convertAndSendToUser(接收消息的用户,浏览器的订阅地址,消息体)
            messagingTemplate.convertAndSendToUser("yq","/queue/notifications",principal.getName() + "-send:" + msg);
        }else {
            messagingTemplate.convertAndSendToUser("gdg","/queue/notifications",principal.getName() + "-send:" + msg);
        }
    }

}

(5) 登录页面 在src/main/resources/templates下新建 login.html,


<html lang="zh-CN"
      xmlns:th="http://www.thymeleaf.org" xmlns="http://www.w3.org/1999/xhtml"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<meta http-equiv="Content-Type" content="text/html;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,


<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" xmlns="http://www.w3.org/1999/xhtml">
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<head>
    <title>Hometitle>
    
    <script th:src="@{/static/jquery-3.3.1.js}" type="text/javascript">script>
    <script th:src="@{/static/sockjs.min.js}" type="text/javascript">script>
    <script th:src="@{/static/stomp.min.js}" type="text/javascript">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  连接endpoint
    var stomp = Stomp.over(sock);
    stomp.connect('guest', 'guest', function(frame) {
        //2    订阅 /user/queue/notifications 发送的消息,要求与messagingTemplate 中定义的地址一样,多了个user,且是必须的,这样才能把消息发送到指定的用户
        stomp.subscribe("/user/queue/notifications", handleNotification);
    });



    function handleNotification(message) {
        $('#output').append("Received: " + message.body + "
"
) } function sendSpittle(text) { stomp.send("/chat", {}, text);//3 } $('#stop').click(function() {sock.close()});
script> <div id="output">div> body> html>

(7)增加页面的viewController

@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    public void addViewControllers(ViewControllerRegistry registry){
        //访问localhost:8787/ws  跳转ws.html页面
        registry.addViewController("/ws").setViewName("/ws");

        registry.addViewController("/login").setViewName("/login");
        registry.addViewController("/chat").setViewName("/chat");

    }

    /**
     * 自动配置静态资源
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations(ResourceUtils.CLASSPATH_URL_PREFIX + "/static/");
        super.addResourceHandlers(registry);
    }

}

ps:打开两个浏览器窗口,谷歌浏览器可设置两个独立用户,然后访问http://localhost:8787/login
登录 gdg/123456,yq/123456 ,开启郭德纲和于谦私聊。

你可能感兴趣的:(消息推送)