• 补习系列(20)-大话 WebSocket 与 "尬聊"的实现


    一、聊聊 WebSocket

    从HTML5技术流行至今,WebSocket已经有非常广泛的应用:

    • 在线游戏,提供实时的操作交互体验
    • 社交平台,与好友实时的私信对话
    • 新闻动态,获得感兴趣的主题信息推送

    ...

    这些场景,都需要服务器能主动实时的给浏览器或客户端推送消息,注意关键词是主动,还有实时!
    而在HTML5一统江湖之前,由于HTTP在推送场景下的"薄弱",我们需要借助一些复杂或者非标准的手段来实现。

    这些方式包括有:

    • Ajax轮询,比如每隔5秒钟,由浏览器对服务器主动请求数据后返回。

    在这种方案下,浏览器需要不断的向服务器发出请求,问题是比较明显的,包括:

    1. HTTP请求头部会浪费一些带宽;
    2. 频繁重建连接会造成很大的开销。
    • Comet,这个词好像翻译为"彗星"? 这个是采用 streaming 或 long-pulling 的长连接技术:
      服务器在收到请求时先挂起,等待有事件发生时才返回数据。

    Comet 效率提升了不少,它解决了Ajax轮询的部分问题,利用HTTP长连接的特性尽可能的避免了连接、带宽资源的浪费等等,于是在很长一段时间 Comet 成为了Web推送技术的主流。
    But ,.. Comet 的实现技术比较复杂,不同框架下的实现方式差异很大,在灵活性、性能上也有些欠缺。
    关于服务端Comet的技术可以参考下面这篇经典文章:
    https://www.ibm.com/developerworks/cn/web/wa-lo-comet/

    • Flash,通过Flash 插件代码实现Socket通讯,本质上是基于TCP的通讯模式,由于Flash 需要安装插件以及浏览器的兼容性问题,目前已经逐渐废弃。

    WebSocket 出场

    WebSocket 出现的目的没有别的,就是干掉前面的东西,Both!
    最开始WebSocket 协议由 RFC6455 定义,其API标准包含于HTML5 范畴之中。
    目前各大主流浏览器已经能完全支持该技术。然后可以看看下面这个图:

    如上图,WebSocket 协议中, 浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
    那么相比以往的方式,这种方案更加节省资源了,它的实时性、灵活性都要强大不少。
    当然,有HTML5标准给它站台,后台杠杠的~

    那么一个 WebSocket 的请求响应长成怎么样呢?
    看下面这个图:

    二、Stomp 是个什么鬼

    一开始我一直认为 Stomp是暴风雨(误看为 Storm),然后觉得说这个技术挺犀利的。
    然后在看了 Stomp 的协议介绍后发现,它是如此的简单..
    Stomp 的 全称叫 Simple Text Orientated Messaging Protocol,就是一个简单的文本定向消息协议,
    除了设计为简单易用之外,它的支持者也非常多。就比如目前主流的消息队列服务器如RabbitMQ、ActiveMQ都支持Stomp 协议。

    开源地址:
    http://stomp.github.io/

    Stomp 定义了一些简单的指令,如下:

    命令 说明
    CONNECT 建立连接
    SEND 发送消息
    SUBSCRIBE 订阅主题
    UNSUBSCRIBE 取消订阅
    BEGIN 开启事务
    COMMIT 提交事务
    ABORT 回滚事务
    ACK 确认消费
    NACK 消息丢弃
    DISCONNECT 断开连接

    一个简单的STOMP消息大致如下:

    CONNECT
    accept-version:1.1,1.0
    heart-beat:10000,10000
    
    u0000
    
    
    SEND
    destination:/app/message
    content-length:6
    
    发送内容u0000
    

    好的,你现在应该了解 Stomp是个什么了,那么为什么要介绍这个?

    WebSocket 为我们提供了Web 双向通信的通道,但对于消息的交互协议还需要我们来自己实现(WebSocket 果然不够意思)
    借助Stomp 协议,可以很方便的实现一种"订阅-发布"的通用机制,这个就是非常具有竞争力的一个特性了。

    三、SpringBoot 整合 WebSocket

    在介绍完WebSocket 之后,接下来干什么呢?
    可能你看完前面的东西会觉得 WebSocket 是如此之强大,以至于很多场景都应该使用这个技术来实现。
    那么如何做? 在此前我所介绍的 SpringBoot 也是如此之强大,那么能不能通过SpringBoot 轻松整合WebSocket 呢?这当然可以!

    思索了很久,我决定做一个最简单的应用展示: 尬聊!

    为什么是"尬聊”,而不是聊天室...

    那么,下面开始讲这个案例,在该样例中会包含一个Controller类、一个HTML页面以及一个JS脚本。
    步骤如下:

    A. 引入依赖

    
           <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
                <version>${springboot.version}</version>
                <exclusions>
                    <exclusion>
                        <groupId>org.slf4j</groupId>
                        <artifactId>slf4j-api</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
    
            <!--websocket-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-websocket</artifactId>
                <version>${springboot.version}</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <version>${springboot.version}</version>
                <optional>true</optional>
            </dependency>
    
            <dependency>
                <groupId>org.webjars</groupId>
                <artifactId>webjars-locator-core</artifactId>
                <version>0.32</version>
            </dependency>
            <dependency>
                <groupId>org.webjars</groupId>
                <artifactId>sockjs-client</artifactId>
                <version>1.0.2</version>
            </dependency>
            <dependency>
                <groupId>org.webjars</groupId>
                <artifactId>stomp-websocket</artifactId>
                <version>2.3.3</version>
            </dependency>
            <dependency>
                <groupId>org.webjars</groupId>
                <artifactId>jquery</artifactId>
                <version>2.1.4</version>
            </dependency>
    
            <dependency>
                <groupId>org.foo.springboot</groupId>
                <artifactId>base</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
    
            <!-- jackson version -->
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>2.8.3</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-core</artifactId>
                <version>2.8.3</version>
            </dependency>
               
    

    添加spring-boot-starter-websocket 会自动引入spring-websocket的依赖,而后者就实现了WebSocket 操作的高级封装。
    还有一个好消息,就是spring-websocket 也默认支持了 Stomp协议(看吧,Stomp支持者太多了)。
    而除此之外,还内置了一个叫 SocketJS 的东西。

    SocketJS是一个流行的JS库,主要是在WebSocket之上封装了一层API,用于支持浏览器不兼容WebSocket的情况。
    其项目地址:
    https://github.com/sockjs/sockjs-client

    其他组件的说明

    • webjars 主要是将一些前端的框架打包到Jar包中以方便我们使用,这里我们添加了socketJS、stompWebSocket相关的一些包;
    • jackson 用于支持WebSocket消息的编解码,是必须添加的。

    B. WebSocket 配置

    参考下面的代码,添加一个JavaConfig风格的配置类:

    WebSocketConfig.java

    @Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
    
        private static final Logger logger = LoggerFactory.getLogger(WebSocketConfig.class);
    
        @Override
        public void configureMessageBroker(MessageBrokerRegistry config) {
    
            //设置订阅通道(客户端可订阅)
            config.enableSimpleBroker("/topic");
    
            //接收APP(客户端)消息的路由前缀,可通过@MessageMapping 映射到方法
            config.setApplicationDestinationPrefixes("/app");
        }
    
        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
    
            //websocket 连接端点
            registry.addEndpoint("/backend").withSockJS();
        }
    
        @Override
        public void configureWebSocketTransport(final WebSocketTransportRegistration registration) {
    
            //配置拦截器
            registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
                @Override
                public WebSocketHandler decorate(final WebSocketHandler handler) {
                    return new WebSocketHandlerDecorator(handler) {
                        @Override
                        public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
                            String username = session.getPrincipal() != null? session.getPrincipal().getName(): "GUEST";
                            logger.info("{} connect.", username);
                            super.afterConnectionEstablished(session);
                        }
    
                        @Override
                        public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
                            String username = session.getPrincipal() != null? session.getPrincipal().getName(): "GUEST";
                            logger.info("{} disconnect.", username);
                            super.afterConnectionClosed(session, closeStatus);
                        }
                    };
                }
            });
            super.configureWebSocketTransport(registration);
        }
    }
    
    

    在WebSocketConfig的配置中,有两点需要关注:

    • registerStompEndpoints 用于添加端点,即浏览器通过 ws://xxx 能访问到的路径
    • configureMessageBroker 用于做消息路由配置,包括订阅主题、方法映射路径

    C. 控制器

    控制层除了支持页面的渲染,还需要对WebSocket消息进行处理,实现如下:

    ConsoleController

    @Controller
    public class ConsoleController {
    
        //输出数据频道
        public static final String CHANNEL_CONSOLE = "/topic/console";
    
    
        @Autowired
        private SimpMessagingTemplate template;
    
        /**
         * 控制台页面
         *
         * @return
         */
        @GetMapping("/console")
        public String console() {
            return "console";
        }
    
        /**
         * 接收WebSocket消息方法
         * @param message
         */
        @MessageMapping("/message")
        public void onMessage(String message) {
            template.convertAndSend(CHANNEL_CONSOLE, "我收到了你的消息:" + message);
        }
    }
    

    D. 前端实现

    先做一个HTML页面,编辑templates/console.html

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8"></meta>
        <title>Web控制台</title>
        <script th:src="@{/webjars/sockjs-client/sockjs.min.js}"></script>
        <script th:src="@{/webjars/stomp-websocket/stomp.min.js}"></script>
        <script th:src="@{/webjars/jquery/jquery.min.js}"></script>
        <script type="text/javascript" th:src="@{/static/console.js}"></script>
    
        <style type="text/css">
            body { font-family: "Microsoft YaHei" ;}
            .span-tv{padding-right:12px}
            #console p {padding: 0px; margin: 0px;}
        </style>
    
    </head>
    <body>
    
    <div style="background-color:#AAA; padding: 5px; border-bottom: 1px solid #333">
        <input type="text" id="word" style="100px"></input>
        <button onclick="sendMessage()">发送消息</button>
        <button onclick="reconnect()">重新连接</button>
        <button onclick="clearConsole()">清空内容</button>
    </div>
    
    <div id="console" style="padding:5px; font-size:10px"></div>
    </body>
    </html>
    

    然后是实现 JS 脚本,编辑public/static/console.js

    $(document).ready(function(){
       //首次打开页面自动连接
       connect();
    })
    
    //执行连接
    function connect() {
    
        //接入端点/backend
        var socket = new SockJS('/backend');
        window.stompClient = Stomp.over(socket);
        window.stompClient.connect({}, function (frame) {
            log('Connected: ' + frame);
    
            //订阅服务端输出的 Topic
            stompClient.subscribe('/topic/console', function (message) {
                log("[服务器说]:" + message.body);
            });
        });
    
    }
    
    //断开连接
    function disconnect() {
        if (stompClient !== null) {
            stompClient.disconnect();
        }
        log("Disconnected");
    }
    
    //重新连接
    function reconnect(){
      clearConsole();
      disconnect();
      connect();
    }
    
    //发送消息
    function sendMessage(){
        var content = $("#word").val();
        if(!content){
            alert("请输入消息!")
            return;
        }
        //向应用Topic发送消息
        stompClient.send("/app/message", {}, content);
        log("[你说]:" + content);
    }
    
    //记录控制台消息
    function log(message){
        $("<p></p>").text(message).appendTo($("#console"));
    }
    
    //清空控制台
    function clearConsole(){
        $("#console").empty();
    }
    

    这样,Web控制台已经制作好了,运行主程序后,打开地址
    http://localhost:8080/console
    进行体验,如下:

    好了,这个案例的确很尴尬..
    但是我认为,在这上面做一做改造,应该可以实现一个诸如"美女聊天室" 的功能的,或者,你可以动手试试。

    码云同步代码

    四、参考文档

    https://spring.io/guides/gs/messaging-stomp-websocket/
    https://blog.coding.net/blog/spring-static-resource-process
    https://zh.wikipedia.org/wiki/WebSocket
    https://halfrost.com/websocket/

    欢迎继续关注"美码师的补习系列-springboot篇" ,期待更多精彩内容-

  • 相关阅读:
    flask ajax
    python 符合条件跳过下一次循环
    python使用openpyxl excel 合并拆分单元格
    等价类划分法
    python 同级目录包导入问题,使用"."错误
    django:查询,反向查询
    Python实现程序执行次数的计数
    python 2x SSH通道连接服务器读取数据库和中文编码问题
    Python for 循环中使用append()添加可变元素,前面的值被覆盖,循环中内存应用地址不变
    以概率列表选择对应元素,轮盘概率选择Python实现
  • 原文地址:https://www.cnblogs.com/littleatp/p/10771814.html
Copyright © 2020-2023  润新知