补习系列(20)-大话websocket与"尬聊"的实现(代码片段)

littleatp littleatp     2022-11-30     718

关键词:

技术图片

一、聊聊 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\\n\\n\\u0000


SEND
destination:/app/message\\ncontent-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-tvpadding-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="width: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篇" ,期待更多精彩内容^-^

大话ion系列

...eVideoStack”关注我们作者|王朋闯本文为王朋闯老师创作的系列ion文章,LiveVideoStack已获得授权发布,未来将持续更新。大话ion系列(一)大话ion系列(二)五、offer与answer流程1.前言之前的文章已经介绍了前... 查看详情

大话ion系列

...eVideoStack”关注我们作者|王朋闯本文为王朋闯老师创作的系列ion文章,LiveVideoStack已获得授权发布,未来将持续更新。大话ion系列(一)三、演进与模块说明1.演进ion-sfu最早从ion抽出来,经过长时间的演变和社... 查看详情

补习系列(15)-springboot分布式会话原理

...、原理进阶A.序列化B.会话代理C.数据老化小结一、背景在补习系列(3)-springboot几种scope一文中,笔者介绍过Session的部分,如下:对于服务器而言,Session通常是存储在本地的,比如Tomcat默认将Session存储在内存(ConcurrentHashMap)中。但... 查看详情

补习系列-springboot实现拦截的五种姿势

目录简介姿势一、使用Filter接口1.注册FilterRegistrationBean2.@WebFilter注解姿势二、HanlderInterceptor姿势三、@ExceptionHandler注解姿势四、RequestBodyAdvice/ResponseBodyAdviceRequestBodyAdvice的用法ResponseBodyAdvice用法姿势五、@Aspect注解思考小结 查看详情

补习系列(10)-springboot之配置读取

目录简介一、配置样例二、如何注入配置1.缺省配置文件2.使用注解3.启动参数还有..三、如何读取配置@Value注解Environment接口@ConfigurationProperties注解四、不同环境中的配置1.区别开发、测试、发布环境2.声明多配置文件参考文档简... 查看详情

补习系列-springboot-restful应用

一、目标了解Restful是什么,基本概念及风格;能使用SpringBoot实现一套基础的Restful风格接口;利用Swagger生成清晰的接口文档。二、Restful入门什么是REST摘自百科的定义:REST即表述性状态转移(英文:RepresentationalStateTransfer,简称RE... 查看详情

补习系列(11)-springboot文件上传原理

目录一、文件上传原理二、springboot文件机制临时文件定制配置三、示例代码A.单文件上传B.多文件上传C.文件上传异常D.Bean配置四、文件下载小结一、文件上传原理一个文件上传的过程如下图所示:浏览器发起HTTPPOST请求,指定请... 查看详情

补习系列-springboot单元测试之道(代码片段)

目录目标一、About单元测试二、AboutJunit三、SpringBoot-单元测试项目依赖测试样例四、Mock测试五、最后目标了解单元测试的背景了解如何利用springboot实现接口的测试了解如何利用mokito做代码的mock一、About单元测试单元测试其实是... 查看详情

websocket基础与应用系列-抓个websocket的包(代码片段)

1为什么需要WebSocketWebSocket是为了满足基于Web的日益增长的实时通信需求而产生的。在传统的Web中,要实现实时通信,通用的方式是采用HTTP协议不断发送请求,即轮询(Polling)。但这种方式既浪费带宽(HTT... 查看详情

补习系列-springboot定时器,你用对了吗

目录简介一、应用启动任务二、JDK自带调度线程池三、@Scheduled定制@Scheduled线程池四、@Async定制@Async线程池小结简介大多数的应用程序都离不开定时器,通常在程序启动时、运行期间会需要执行一些特殊的处理任务。比如资源初... 查看详情

websocket基础与应用系列——engine.io原理了解(代码片段)

packetpacketpacketmessagepacketpacketpacketpacketpacketpacketpacketid>[<data>]example:4hello对于二进制数据,不包括数据包类型(packettype),因为只有“message”数据包类型可以包括二进制数据。packettype0open新传输通道建立的时候,从服务端... 查看详情

大话云原生煮饺子与dockerkubernetes之间的关系

...雾罩,要么曲高和寡。所以笔者就有了写《大话云原生》系列文章的想法,期望用最通俗、简单的语言说明白云原生生态系统内的组成及应用关系。那么,开始吧,这是第一篇!这真的是一篇讲架构技术的文章,不是小说!建议... 查看详情

Chrome 20 websocket 握手

】Chrome20websocket握手【英文标题】:Chrome20websockethandshake【发布时间】:2012-07-0220:27:57【问题描述】:我正在使用lemmingzshadow(web)的PHPWebsocket服务器。到目前为止,一切都很好。更新到chrome20后,如果我想与服务器握手,它会以这... 查看详情

大话ion系列

...eVideoStack”关注我们作者|王朋闯本文为王朋闯老师创作的系列ion文章,LiveVideoStack已获得授权发布,未来将持续更新。大话ion系列(一)大话ion系列(二)大话ion系列(三)大话ion系列(四)... 查看详情

大话云原生微服务篇-五星级酒店的服务方式

《大话云原生》系列文章期望用最通俗、简单的语言说明云原生生态系统内的组成及应用关系。此专栏的前两篇文章《【大话云原生】煮饺子与docker、kubernetes之间的关系》《【大话云原生】负载均衡篇-小饭馆的流量变大了》欢... 查看详情

大话ion系列

...eVideoStack”关注我们作者|王朋闯本文为王朋闯老师创作的系列ion文章,LiveVideoStack已获得授权发布,未来将持续更新。一、为什么用ion-sfu1.简介ion-sfu作为ion分布式架构里的核心模块,SFU是选择转发单元的简称,可... 查看详情

大话云原生负载均衡篇-小饭馆客流量变大了

一、前言这是《大话云原生》系列的第二篇,第一篇《煮饺子与docker、kubernetes之间的关系》推出之后受到大家的欢迎,很多朋友联系到我给我加油打气,感谢!我会继续写下去!书接上回介绍了《煮饺子与docker、kubernetes之间的... 查看详情

大话ion系列

...eVideoStack”关注我们作者|王朋闯本文为王朋闯老师创作的系列ion文章,LiveVideoStack已获得授权发布,未来将持续更新。大话ion系列(一)大话ion系列(二)大话ion系列(三)七、Simulcast流程1.Simulcast... 查看详情