美文网首页纵横研究院后端基础技术专题社区
WebSocket广播式和点对点的通信【原创】

WebSocket广播式和点对点的通信【原创】

作者: elijah777 | 来源:发表于2019-07-17 21:13 被阅读0次

    本篇文章主要介绍websocket的两种通信,广播式和点对点的通信。

    一、广播式通讯

    类似广播一样,只要发出,订阅的人便可以接收到

    前端发出消息,通过SockJS连接

    代码示例
    1、pom.xml引入jar
     <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-websocket</artifactId>
     </dependency>
    
    2、WebSocketConfig.java 配置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;
    ​
    /**
     * @description: 配置WebSocket
     *          注释@EnableWebSocketMessageBroker开始使用STOMP协议来传输基于代理(message broker)的消息
     *
     * @author: Shenshuaihu
     * @version: 1.0
     * @data: 2019-07-12 16:39
     */
    @Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
    ​
     /**
     *  注册STOMP协议的节点(endpoint),并映射的对应的URL。
     *  注册一个STOMP的endpoint,并指定使用SickJS协议
     * @param registry
     */
     @Override
     public void registerStompEndpoints(StompEndpointRegistry registry) {
     registry.addEndpoint("/endpointSSH").withSockJS();
     registry.addEndpoint("/endpointChat").withSockJS();
     }
    ​
     /**
     *  配置消息代理(Message Broker)
     *  广播式应配置一个/topic 消息代理
     *  点对点配置 /queue
     * @param registry
     */
     @Override
     public void configureMessageBroker(MessageBrokerRegistry registry) {
     registry.enableSimpleBroker("/queue","/topic");
     }
    ​
    }
    
    3、两个发送和接收消息的实体 ElijahMessage.java ElijahResponse.java
    import lombok.AllArgsConstructor;
    import lombok.Getter;
    ​
    /**
     * @description: 用于服务器想向浏览器发生消息
     *
     * @author: Shenshuaihu
     * @version: 1.0
     * @data: 2019-07-12 17:10
     */
    @AllArgsConstructor
    @Getter
    public class ElijahResponse {
     private String responseMessage;
    }
    
    import lombok.Getter;
    ​
    /**
     * @description: 用于接收服务器发送的消息
     *
     * @author: Shenshuaihu
     * @version: 1.0
     * @data: 2019-07-12 17:09
     */
    @Getter
    public class ElijahMessage {
     private String name;
    }
    
    4、WsController.java WebSocket 控制器
    import com.ch7.domain.ElijahMessage;
    import com.ch7.domain.ElijahResponse;
    import lombok.extern.slf4j.Slf4j;
    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 java.security.Principal;
    ​
    /**
     * @description: WebSocket 控制器
     *
     * @author: Shenshuaihu
     * @version: 1.0
     * @data: 2019-07-12 17:15
     */
    @Controller
    @Slf4j
    public class WsController {
    ​
     /**
     * 通过SimpMessagingTemplate 向浏览器发生消息
     */
     @Autowired
     private SimpMessagingTemplate messagingTemplate;
    ​
     /**
     *  当浏览器向服务端发生请求时,通过@MessageMapping映射/welcome这个地址
     *  注解@MessageMapping使用方法与@RequestMapping相似
     * @param message
     * @return
     * @throws Exception
     */
     @MessageMapping("welcome")
     @SendTo("/topic/getResponse")
     public ElijahResponse say(ElijahMessage message) throws Exception {
     Thread.sleep(3000);
     return new ElijahResponse("Welcome, " + message.getName() + "!");
     }
    ​
     /**
     *  点对点聊天
     *
     * @param principal 包含当前用户的信息
     * @param msg
     */
     @MessageMapping("/chat")
     public void handleChar(Principal principal, String msg) {
    ​
     // 判断发生给谁
     if (principal.getName().equals("ssh")) {
     // 发生消息给用户  接收消息的用户、浏览器订阅地址和消息内容
     messagingTemplate.convertAndSendToUser("elijah",
     "/queue/notifications",
     principal.getName() + "-send: " + msg);
     } else {
     messagingTemplate.convertAndSendToUser("ssh",
     "/queue/notifications",
     principal.getName() + "-send: " + msg);
     }
     }
    ​
    }
    
    5、ws.html 发送消息和接收消息的页面
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    <head>
     <title>spring-boot-WebSocket-广播式</title>
     <link rel="stylesheet" type="text/css" value="">
    </head>
    <body onload="disconnect()">
    <noscript>
     <h2 style="color: #ff0000">貌似你的浏览器不支持websocket</h2>
    </noscript>
    ​
    ​
    <div>
     <h3>WebSocket</h3>
    </div>
    ​
    <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 src="/static/js/sockjs.min.js" charset="utf-8"></script>-->
    <!--<script src="/static/js/stomp.min.js" charset="utf-8"></script>-->
    ​
    <script th:src="@{/static/js/jquery.min.js}"></script>
    <script th:src="@{/static/js/sockjs.min.js}"></script>
    <script th:src="@{/static/js/stomp.min.js}"></script>
    <script type="application/javascript">
    ​
     var stompClient = null;
    ​
     function setConnected(connected) {
     console.log('Connected status: ' + connected);
     document.getElementById("connect").disabled = connected;
     document.getElementById("disconnect").disabled = !disconnect;
     document.getElementById("conversationDiv").style.visibility= (connected ? 'visible' : 'hidden');
    ​
     // $("#conversationDiv").style.visibility = (connected ? 'visible' : 'hidden');
     $('response').html();
     }
    ​
     /**
     * 打开连接
     */
     function connect() {
     // 1、连接SockJS的endpoint
     var socket = new SockJS('/endpointSSH');
     // 2、使用STOMP 子协议的WebSocket客户端
     stompClient = Stomp.over(socket);
     // 3、连接websockst服务端
     stompClient.connect({}, function (frame) {
     setConnected(true);
     console.log('Connected: ' + frame);
     // 4、 通过stomp.subscribe订阅/topic/getResponse目标(destination)发生的消息,后端在@SendTo定义
     stompClient.subscribe('/topic/getResponse', function (respose) {
     showResponse(JSON.parse(respose.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>
    ​
    
    展示结果:
    websocket-广播式.png

    在之前学习过socket连接对象也可通过WebSocket(不通过SockJS)连接

    var socket = new WebSocket(url);

    https://www.jianshu.com/p/bd0667b270ca

    目前的是通过sockjs来

    1、连接SockJS的endpoint

    2、使用STOMP 子协议的WebSocket客户端

    3、连接websockst服务端

    4、 通过stomp.subscribe订阅/topic/getResponse目标(destination)发生的消息,后端在@SendTo定义

    5、通过 stompClient.send 向/welcome 目标发送消息 服务端在@MessageMapping中定义的

    STOMP帧由命令,一个或多个头信息、一个空行及负载(文本或字节)所组成;

    其中可用的COMMAND 包括:

    CONNECT、SEND、SUBSCRIBE、UNSUBSCRIBE、BEGIN、COMMIT、ABORT、ACK、NACK、DISCONNECT;

    数据执行流程

    CONNECT accept-version:1.1,1.0 heart-beat:10000,10000

    连接成功的返回为:

    <<< CONNECTED version:1.1 heart-beat:0,0

    订阅目标(destination)/topic/getResponse:

    SUBSCRIBE id:sub-0 destination:/topic/getResponse

    向目标(destination)/welcome 发生消息的格式为:

    SEND destination:/welcome content-length:17

    {"name":"elijah"}

    从目标(destination)/topic/getResponse接收的格式为:

    <<< MESSAGE destination:/topic/getResponse content-type:application/json;charset=UTF-8 subscription:sub-0 message-id:5nd0pfjf-73 content-length:38

    {"responseMessage":"Welcome, elijah!"}

    二、点对点式通信:

    点对点多用于聊天室,一对一的通信,这里是基础的登录(springsecurity)到聊天室然后进行两天

    代码示例

    相关登录配置
    1、pom.xml 引入jar
     <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-security</artifactId>
     </dependency>
    
    2、WebSecurityConfig.java 鉴权配置
    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;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    ​
    /**
     * @description: 登录时的鉴权配置
     *
     * @author: Shenshuaihu
     * @version: 1.0
     * @data: 2019-07-15 18:31
     */
    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ​
     @Override
     protected void configure(HttpSecurity http) throws Exception {
     http
     .authorizeRequests()
     // /和login不拦截
     .antMatchers("/", "login").permitAll()
     .anyRequest().authenticated()
     .and()
     .formLogin()
     // 页面访问路径
     .loginPage("/login")
     // 登录成功转向/char
     .defaultSuccessUrl("/chat")
     .permitAll()
     .and()
     .logout()
     .permitAll();
    ​
     }
    ​
     /**
     * 内存中分配两个用户
     * @param auth
     * @throws Exception
     */
     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
     auth
     .inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
     .withUser("elijah")
     .password(new BCryptPasswordEncoder().encode("elijah"))
     .roles("USER")
     .and()
     .withUser("ssh")
     .password(new BCryptPasswordEncoder().encode("ssh"))
     .roles("USER");
     }
    ​
     /**
     * 静态资源不拦截
     * @param web
     * @throws Exception
     */
     @Override
     public void configure(WebSecurity web) throws Exception {
     web.ignoring().antMatchers("/resource/static/**");
     }
    }
    
    3、login.html 登录页面
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    <head>
     <title>spring-boot-login</title>
     <link rel="stylesheet" type="text/css" value="">
    </head>
    <body>
    ​
    <div>
     <h3>登录</h3>
    </div>
    ​
     <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>
    
    聊天代码
    1、相关配置

    WebSocketConfig.java

    WsController.java

    方法在上面广播式代码里

    两个页面也需要配置

    2、char.html 聊天窗口
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    <head>
     <title>spring-boot-WebSocket-点对点式-home</title>
     <link rel="stylesheet" type="text/css" value="">
    </head>
    <body>
    ​
    
    <p>聊天室</p>
    ​
    <form id="elijahForm">
     <textarea rows="4" cols="60" name="text"></textarea>
     <input type="submit">
    </form>
    ​
    ​
    ​
    ​
    <script th:src="@{/static/js/jquery.min.js}"></script>
    <script th:src="@{/static/js/sockjs.min.js}"></script>
    <script th:src="@{/static/js/stomp.min.js}"></script>
    <script type="application/javascript">
    ​
    ​
     $('#elijahForm').submit(function (e) {
     e.preventDefault();
     var text = $('#elijahForm').find('textarea[name="text"]').val();
     sendSpittle(text);
     });
    ​
     // 1、连接SockJS的endpoint
     var socket = new SockJS('/endpointChat');
     // 2、使用STOMP 子协议的WebSocket客户端
     stomp = Stomp.over(socket);
     // 3、连接websockst服务端 //默认的和STOMP端点连接
     stomp.connect("guest", "guest", function (frame) {
     // 4、 通过stomp.subscribe订阅/topic/getResponse目标(destination)发生的消息,后端在@SendTo定义
     stomp.subscribe("/user/queue/notifications", function (message) {
     debugger;
     var content = message.body;
     var obj = JSON.parse(content);
     console.log("admin用户特定的消息1:" + obj.message)
     console.log("收到一条新消息:" + JSON.parse(respose.message).responseMessage)
     $('#output').append("<b>Received: " + message.body + "</b><br/>")
     });
     });
    ​
    ​
     function sendSpittle(text) {
     stomp.send("/chat", {}, text);
     }
    ​
     $('#stop').click(function () {
     socket.close();
     })
    </script>
    ​
    <div id="output"></div>
    ​
    </body>
    </html>
    ​
    
    展示结果:
    websocket-点对点式.png

    三、其他说明:

    1、基本概念:

    STOMP:

    STOMP(Simple Text-Orientated Messaging Protocol) 面向消息的简单文本协议

    如何理解 STOMP 与 WebSocket 的关系: 1) HTTP协议解决了 web 浏览器发起请求以及 web 服务器响应请求的细节,假设 HTTP 协议 并不存在,只能使用 TCP 套接字来 编写 web 应用,你可能认为这是一件疯狂的事情;

    1. 直接使用 WebSocket(SockJS) 就很类似于 使用 TCP 套接字来编写 web 应用,因为没有高层协议,就需要我们定义应用间所发送消息的语义,还需要确保连接的两端都能遵循这些语义;

    2. 同 HTTP 在 TCP 套接字上添加请求-响应模型层一样,STOMP 在 WebSocket 之上提供了一个基于帧的线路格式层,用来定义消息语义;

    2、所遇到的坑:

    使用springsecurity时在内容中设置密码没有处理会报错

    There is no PasswordEncoder mapped for the id "null"

    是高版本的security所导致

    .inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
     .withUser("elijah")
     .password(new BCryptPasswordEncoder().encode("elijah"))
     .roles("USER")
    

    参考文档:

    https://blog.csdn.net/jqsad/article/details/77745379

    https://www.jianshu.com/p/bd0667b270ca

    参考书籍汪云飞 SpringBoot实战

    相关文章

      网友评论

        本文标题:WebSocket广播式和点对点的通信【原创】

        本文链接:https://www.haomeiwen.com/subject/puxhlctx.html