目录

一. WebSocket 简介

WebSocket 是一种基于 TCP 的网络协议。在 2009 年诞生,于 2011 年被 IETF 定为标准 RFC 6455 通信标准,并由 RFC7936 补充规范。WebSocket API 也被 W3C 定为标准。

WebSocket 也是一种全双工通信的协议,既允许客户端向服务器主动发送消息,也允许服务器主动向客户端发送消息。在 WebSocket 中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,进行双向数据传输。

二. WebSocket 特点

  1. 连接握手阶段使用 HTTP 协议;

  2. 协议标识符是 ws,如果采用加密则是 wss

  3. 数据格式比较轻量,性能开销小,通信高效;

  4. 没有同源限制,客户端可以与任意服务器通信;

  5. 建立在 TCP 协议之上,服务器端的实现比较容易;

  6. 通过 WebSocket 可以发送文本,也可以发送二进制数据;

  7. HTTP 协议有着良好的兼容性。默认端口也是 80443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器;

三. 为什么需要 WebSocket?

谈起为什么需要 WebSocket 前,那得先了解在没有 WebSocket 那段时间说起,那时候基于 Web 的消息基本上是靠 Http 协议进行通信,而经常有”聊天室”、”消息推送”、”股票信息实时动态”等这样需求,而实现这样的需求常用的有以下几种解决方案:

1. 短轮询(Traditional Polling)

短轮询是指客户端每隔一段时间就询问一次服务器是否有新的消息,如果有就接收消息。这样方式会增加很多次无意义的发送请求信息,每次都会耗费流量及处理器资源。

优点:短连接,服务器处理简单,支持跨域、浏览器兼容性较好。

缺点:有一定延迟、服务器压力较大,浪费带宽流量、大部分是无效请求。

2. 长轮询(Long Polling)

长轮询是段轮询的改进,客户端执行 HTTP 请求发送消息到服务器后,等待服务器回应,如果没有新的消息就一直等待,知道服务器有新消息传回或者超时。

这也是个反复的过程,这种做法只是减小了网络带宽和处理器的消耗,但是带来的问题是导致消息实时性低,延迟严重。而且也是基于循环,最根本的带宽及处理器资源占用并没有得到有效的解决。

优点:减少轮询次数,低延迟,浏览器兼容性较好。

缺点:服务器需要保持大量连接。

3. 服务器发送事件(Server-Sent Event)

目前除了 IE/Edge,其他浏览器都支持。

服务器发送事件是一种服务器向浏览器客户端发起数据传输的技术。一旦创建了初始连接,事件流将保持打开状态,直到客户端关闭。该技术通过传统的 HTTP 发送,并具有 WebSockets 缺乏的各种功能,例如”自动重新连接”、”事件ID” 及 “发送任意事件”的能力。

服务器发送事件是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。

优点:适用于更新频繁、低延迟并且数据都是从服务端发到客户端。

缺点:浏览器兼容难度高。

总结

显然,上面这几种方式都有各自的优缺点,虽然靠轮询方式能够实现这些一些功能,但是其对性能的开销和低效率是非常致命的,尤其是在移动端流行的现在。

现在客户端与服务端双向通信的需求越来越多,且现在的浏览器大部分都支持 WebSocket。所以对实时性和双向通信及其效率有要求的话,比较推荐使用 WebSocket

四. WebSocket 连接流程

第一步

客户端先用带有 Upgrade:Websocket 请求头的 HTTP 请求,向服务器端发起连接请求,实现握手(HandShake)。

客户端 HTTP 请求的 Header 头信息如下:

Connection: Upgrade
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: IRQYhWINfX5Fh1zdocDl6Q==
Sec-WebSocket-Version: 13
Upgrade: websocket
  • Connection: Upgrade 表示要升级协议。

  • Upgrade: Websocket 要升级协议到 websocket 协议。

  • Sec-WebSocket-Extensions: 表示客户端所希望执行的扩展(如消息压缩插件)。

  • Sec-WebSocket-Key: 主要用于WebSocket协议的校验,对应服务端响应头的 Sec-WebSocket-Accept

  • Sec-WebSocket-Version: 表示 websocket 的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。

第二步

握手成功后,由 HTTP 协议升级成 Websocket 协议,进行长连接通信,两端相互传递信息。

服务端响应的 HTTP Header 头信息如下:

Connection: upgrade
Sec-Websocket-Accept: TSF8/KitM+yYRbXmjclgl7DwbHk=
Upgrade: websocket
  • Connection: Upgrade 表示要升级协议。

  • Upgrade: Websocket 要升级协议到 websocket 协议。

  • Sec-Websocket-Accept: 对应 Sec-WebSocket-Key 生成的值,主要是返回给客户端,让客户端对此值进行校验,证明服务端支持 WebSocket

五. WebSocket 使用场景

  1. 数据流状态: 比如说上传下载文件,文件进度,文件是否上传成功。

  2. 协同编辑文档: 同一份文档,编辑状态得同步到所有参与的用户界面上。

  3. 多玩家游戏: 很多游戏都是协同作战的,玩家的操作和状态肯定需要及时同步到所有玩家。

  4. 多人聊天: 很多场景下都需要多人参与讨论聊天,用户发送的消息得第一时间同步到所有用户。

  5. 社交订阅: 有时候我们需要及时收到订阅消息,比如说开奖通知,比如说在线邀请,支付结果等。

  6. 股票虚拟货币价格: 股票和虚拟货币的价格都是实时波动的,价格跟用户的操作息息相关,及时推送对用户跟盘有很大的帮助。

六. WebSocket 中子协议支持

WebSocket 确实指定了一种消息传递体系结构,但并不强制使用任何特定的消息传递协议。而且它是 TCP 上的一个非常薄的层,它将字节流转换为消息流(文本或二进制)仅此而已。由应用程序来解释消息的含义。

HTTP(它是应用程序级协议)不同,在 WebSocket 协议中,传入消息中根本没有足够的信息供框架或容器知道如何路由或处理它。因此,对于非常琐碎的应用程序而言 WebSocket 协议的级别可以说太低了。

可以做到的是引导在其上面再创建一层框架。这就相当于当今大多数 Web 应用程序使用的是 Web 框架,而不直接仅使用 Servlet API 进行编码一样。

WebSocket RFC 定义了子协议的使用。在握手过程中,客户机和服务器可以使用头 Sec-WebSocket 协议商定子协议,即使不需要使用子协议,而是用更高的应用程序级协议,但应用程序仍需要选择客户端和服务器都可以理解的消息格式。且该格式可以是自定义的、特定于框架的或标准的消息传递协议。

Spring 框架支持使用 STOMP,这是一个简单的消息传递协议,最初创建用于脚本语言,框架灵感来自 HTTPSTOMP 被广泛支持,非常适合在 WebSocketweb 上使用。

七. 什么是 STOMP 协议

(1). STOMP 协议概述

STOMP(Simple Text-Orientated Messaging Protocol)是一种简单的面向文本的消息传递协议。

它提供了一个可互操作的连接格式,允许 STOMP 客户端与任意 STOMP 消息代理(Broker)进行交互。STOMP 协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。

(2). 简单介绍可以分为以下几点:

STOMP 是基于帧的协议,其帧以 HTTP 为模型。

STOMP 框架由命令,一组可选的标头和可选的主体组成。

STOMP 基于文本,但也允许传输二进制消息。

STOMP 的默认编码为 UTF-8,但它支持消息正文的替代编码的规范。

(3). STOMP 客户端是一种用户代理

作为生产者,通过 SEND 帧将消息发送到目标服务器上。

作为消费者,对目标地址发送 SUBSCRIBE 帧,并作为 MESSAGE 帧从服务器接收消息。

(4). STOMP 帧

STOMP 是基于帧的协议,其帧以 HTTP 为模型。STOMP 结构为:

COMMAND
header1:value1
header2:value2Body^@

客户端可以使用 SENDSUBSCRIBE 命令发送或订阅消息,还可以使用 “destination” 头来描述消息的内容和接收者。

这支持一种简单的发布-订阅机制,可用于通过代理将消息发送到其他连接的客户端,或将消息发送到服务器以请求执行某些工作。

(5). Stomp 常用帧

STOMP 的客户端和服务器之间的通信是通过”“(Frame)实现的,每个帧由多”“(Line)组成,其包含的帧如下:

  • Connecting Frames:

    • CONNECT(连接)

    • CONNECTED(成功连接)

  • Client Frames:

    • SEND(发送)

    • SUBSRIBE(订阅)

    • UNSUBSCRIBE(取消订阅)

    • BEGIN(开始)

    • COMMIT(提交)

    • ABORT(中断)

    • ACK(确认))

    • NACK(否认))

    • DISCONNECT(断开连接))

  • Server Frames:

    • MESSAGE(消息))

    • RECEIPT(接收))

    • ERROR(错误))

(6). Stomp 与 WebSocket 的关系

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

HTTPTCP 套接字上添加请求-响应模型层一样,STOMPWebSocket 之上提供了一个基于帧的线路格式层,用来定义消息语义。

(7). 使用 STOMP 作为 WebSocket 子协议的好处

  1. 无需发明自定义消息格式

  2. 在浏览器中 使用现有的stomp.js客户端

  3. 能够根据目的地将消息路由到

  4. 可以使用成熟的消息代理(例如RabbitMQActiveMQ等)进行广播的选项

  5. 使用STOMP(相对于普通 WebSocket)使 Spring Framework 能够为应用程序级使用提供编程模型,就像 Spring MVC 提供基于 HTTP 的编程模型一样。

八. Spring 封装的 STOMP

使用 SpringSTOMP 支持时,Spring WebSocket 应用程序充当客户端的 STOMP 代理。

消息被路由到 @Controller 消息处理方法或简单的内存中代理,该代理跟踪订阅并向订阅的用户广播消息。

还可以将 Spring 配置为与专用的 STOMP 代理(例如 RabbitMQActiveMQ等)一起使用,以实际广播消息。在那种情况下,Spring 维护与代理的 TCP 连接,将消息中继到该代理,并将消息从该代理向下传递到已连接的 WebSocket 客户端。

因此 Spring Web 应用程序可以依赖基于统一 HTTP 的安全性,通用验证以及熟悉的编程模型消息处理工作。

Spring 官方提供的处理流图:

上面中的一些概念关键词:

  • Message: 消息,里面带有 headerpayload

  • MessageHandler: 处理 client 消息的实体。

  • MessageChannel: 解耦消息发送者与消息接收者的实体

    • clientInboundChannel:用于从 WebSocket 客户端接收消息。

    • clientOutboundChannel:用于将服务器消息发送给 WebSocket 客户端。

    • brokerChannel:用于从服务器端、应用程序中向消息代理发送消息

  • Broker: 存放消息的中间件,client 可以订阅 broker 中的消息。

上面的设置包括3个消息通道:

  1. clientInboundChannel: 用于来自WebSocket客户端的消息。

  2. clientOutboundChannel: 用于向WebSocket客户端发送消息。

  3. brokerChannel: 从应用程序内部发送给代理的消息。

九. 示例一:实现简单的广播模式

WebSocket 常分为广播队列模式,广播模式是向订阅广播的用户发送信息,只要订阅相关广播就能收到对应信息。

队列模式常用于点对点模式,为单个用户向另一个用户发送信息,这里先介绍下广播模式的实现示例。

1. Maven 引入相关依赖

这里使用 Maven 工具管理依赖包,分别引入下面依赖:

  1. lombok: Lombok 工具依赖,便于生成实体对象的 Get 与 Set 方法。

  2. spring-boot-starter-websocket:SpringBoot 实现 WebSocket 的依赖,里面对 WebSocket 进行了一些列封装,并且也包含了 SpringBoot Web 依赖。

<dependencies><!-- SpringBoot WebSocket --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency>
</dependencies>

2. 创建测试实体类

创建便于传输消息的实体类,里面字段内容如下:

import lombok.Data;
@Data
public class MessageBody {/** 消息内容 */private String content;/** 广播转发的目标地址(告知 STOMP 代理转发到哪个地方) */private String destination;
}

3. 创建 WebSocket 配置类

创建 WebSocket 配置类,配置进行连接注册的端点 /mydlq 和消息代理前缀 /topic 及接收客户端发送消息的前缀 /app

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {/*** 配置 WebSocket 进入点,及开启使用 SockJS,这些配置主要用配置连接端点,用于 WebSocket 连接** @param registry STOMP 端点*/@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/mydlq").withSockJS();}/*** 配置消息代理选项** @param registry 消息代理注册配置*/@Overridepublic void configureMessageBroker(MessageBrokerRegistry registry) {// 设置一个或者多个代理前缀,在 Controller 类中的方法里面发生的消息,会首先转发到代理从而发送到对应广播或者队列中。registry.enableSimpleBroker("/topic");// 配置客户端发送请求消息的一个或多个前缀,该前缀会筛选消息目标转发到 Controller 类中注解对应的方法里registry.setApplicationDestinationPrefixes("/app");}}

4. 创建测试 Controller 类

创建 Controller 类,该类也类似于正常 Web 项目中 Controller 写法一样,在方法上面添加 @MessageMapping 注解,当客户端发送消息请求的前缀匹配上 WebSocket 配置类中的 /app 前缀后,会进入到 Controller 类中进行匹配,如果匹配成功则执行注解所在的方法内容。

@Controller
public class MessageController {/** 消息发送工具对象 */@Autowiredprivate SimpMessageSendingOperations simpMessageSendingOperations;/** 广播发送消息,将消息发送到指定的目标地址 */@MessageMapping("/test")public void sendTopicMessage(MessageBody messageBody) {// 将消息发送到 WebSocket 配置类中配置的代理中(/topic)进行消息转发simpMessageSendingOperations.convertAndSend(messageBody.getDestination(), messageBody);}}

5. 创建测试脚本

创建用于操作 WebSocket 的 JS 文件 app-websocket.js,内容如下:

// 设置 STOMP 客户端
var stompClient = null;
// 设置 WebSocket 进入端点
var SOCKET_ENDPOINT = "/mydlq";
// 设置订阅消息的请求前缀
var SUBSCRIBE_PREFIX = "/topic"
// 设置订阅消息的请求地址
var SUBSCRIBE = "";
// 设置服务器端点,访问服务器中哪个接口
var SEND_ENDPOINT = "/app/test";/* 进行连接 */
function connect() {// 设置 SOCKETvar socket = new SockJS(SOCKET_ENDPOINT);// 配置 STOMP 客户端stompClient = Stomp.over(socket);// STOMP 客户端连接stompClient.connect({}, function (frame) {alert("连接成功");});
}/* 订阅信息 */
function subscribeSocket(){// 设置订阅地址SUBSCRIBE = SUBSCRIBE_PREFIX + $("#subscribe").val();// 输出订阅地址alert("设置订阅地址为:" + SUBSCRIBE);// 执行订阅消息stompClient.subscribe(SUBSCRIBE, function (responseBody) {var receiveMessage = JSON.parse(responseBody.body);$("#information").append("<tr><td>" + receiveMessage.content + "</td></tr>");});
}/* 断开连接 */
function disconnect() {stompClient.disconnect(function() {alert("断开连接");});
}/* 发送消息并指定目标地址(这里设置的目标地址为自身订阅消息的地址,当然也可以设置为其它地址) */
function sendMessageNoParameter() {// 设置发送的内容var sendContent = $("#content").val();// 设置待发送的消息内容var message = '{"destination": "' + SUBSCRIBE + '", "content": "' + sendContent + '"}';// 发送消息stompClient.send(SEND_ENDPOINT, {}, message);
}

6. 创建 WebSocket HTML

创建用于展示 WebSocket 相关功能的 WEB HTML 页面 index.html,内容如下:

<!DOCTYPE html>
<html>
<head><title>Hello WebSocket</title><link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script><script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script><script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script><script src="app-websocket.js"></script>
</head>
<body><div id="main-content" class="container" style="margin-top: 10px;"><div class="row"><form class="navbar-form" style="margin-left:0px"><div class="col-md-12"><div class="form-group"><label>WebSocket 连接:</label><button class="btn btn-primary" type="button" onclick="connect();">进行连接</button><button class="btn btn-danger" type="button" onclick="disconnect();">断开连接</button></div><label>订阅地址:</label><div class="form-group"><input type="text" id="subscribe" class="form-control" placeholder="订阅地址"></div><button class="btn btn-warning" onclick="subscribeSocket();" type="button">订阅</button></div></form></div></br><div class="row"><div class="form-group"><label for="content">发送的消息内容:</label><input type="text" id="content" class="form-control" placeholder="消息内容"></div><button class="btn btn-info" onclick="sendMessageNoParameter();" type="button">发送</button></div></br><div class="row"><div class="col-md-12"><h5 class="page-header" style="font-weight:bold">接收到的消息:</h5><table class="table table-striped"><tbody id="information"></tbody></table></div></div></div>
</body>
</html>

7. 启动并进行测试

输入地址 http://localhost:8080/index.html 访问测试的前端页面,然后执行下面步骤进行测试:

  1. 点击进行连接按钮,连接 WebSocket 服务端;

  2. 在订阅地址栏输入订阅地址(因为本人设置的订阅地址和接收消息的地址是一个,所以随意输入);

  3. 点击订阅按钮订阅对应地址的消息;

  4. 在发送消息内容的输入框中输入hello world!,然后点击发送按钮发送消息;

执行完上面步骤成后,可以观察到成功接收到订阅地址的消息,如下:

十. 示例二:实现点对点模式(引入 Spring Security 实现鉴权)

1. Maven 引入相关依赖

这里使用 Maven 工具管理依赖包,分别引入下面依赖:

  1. lombok: Lombok 工具依赖,便于生成实体对象的 Get 与 Set 方法。

  2. spring-boot-starter-websocket:SpringBoot 实现 WebSocket 的依赖,里面对 WebSocket 进行了一些列封装,并且也包含了 SpringBoot Web 依赖。

  3. spring-boot-starter-security:Spring Security,这是一种基于 Spring AOP 和 Servlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。

<dependencies><!-- SpringBoot WebSocket --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency><!-- Security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency></dependencies>

2. 创建测试实体类

创建便于传输消息的实体类,里面字段内容如下:

@Data
public class MessageBody {/** 发送消息的用户 */private String from;/** 消息内容 */private String content;/** 目标用户(告知 STOMP 代理转发到哪个用户) */private String targetUser;/** 广播转发的目标地址(告知 STOMP 代理转发到哪个地方) */private String destination;
}

3. 创建 WebSocket 配置类

创建 WebSocket 配置类,配置进行连接注册的端点/mydlq 和消息代理前缀 /queue 及接收客户端发送消息的前缀 /app

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {/*** 配置 WebSocket 进入点,及开启使用 SockJS,这些配置主要用配置连接端点,用于 WebSocket 连接** @param registry STOMP 端点*/@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/mydlq").withSockJS();}/*** 配置消息代理选项** @param registry 消息代理注册配置*/@Overridepublic void configureMessageBroker(MessageBrokerRegistry registry) {// 设置一个或者多个代理前缀,在 Controller 类中的方法里面发生的消息,会首先转发到代理从而发送到对应广播或者队列中。registry.enableSimpleBroker("/queue");// 配置客户端发送请求消息的一个或多个前缀,该前缀会筛选消息目标转发到 Controller 类中注解对应的方法里registry.setApplicationDestinationPrefixes("/app");// 服务端通知特定用户客户端的前缀,可以不设置,默认为userregistry.setUserDestinationPrefix("/user");}}

5. 创建 Security 配置

Spring Security 的配置类,可以在该类中配置权限认证及测试的两个用户相关信息:

  1. 测试用户名/密码1:mydlq1/123456

  2. 测试用户名/密码2:mydlq2/123456

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {/*** 设置密码编码的配置参数,这里设置为 NoOpPasswordEncoder,不配置密码加密,方便测试。** @return 密码编码实例*/@BeanPasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();}/*** 设置权限认证参数,这里用于创建两个用于测试的用户信息。** @param auth SecurityBuilder 用于创建 AuthenticationManager。* @throws Exception 抛出的异常*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.inMemoryAuthentication().withUser("mydlq1").password("123456").roles("admin").and().withUser("mydlq2").password("123456").roles("admin");}/*** 设置 HTTP 安全相关配置参数** @param http HTTP Security 对象* @throws Exception 抛出的异常信息*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().formLogin().permitAll();}}

5. 创建测试 Controller 类

跟上面介绍广播模式一样,作用也是根据 WebSocket 配置类中 /app 前缀匹配后进入 Controller 类进行逻辑处理操作。

@Controller
public class MessageController {@Autowiredprivate SimpMessageSendingOperations simpMessageSendingOperations;/*** 点对点发送消息,将消息发送到指定用户*/@MessageMapping("/test")public void sendUserMessage(Principal principal, MessageBody messageBody) {// 设置发送消息的用户messageBody.setFrom(principal.getName());// 调用 STOMP 代理进行消息转发simpMessageSendingOperations.convertAndSendToUser(messageBody.getTargetUser(), messageBody.getDestination(), messageBody);}
}

6. 创建 WebSocket JS

创建用于操作 WebSocket 的 JS 文件 app-websocket.js,内容如下:

// 设置 STOMP 客户端
var stompClient = null;
// 设置 WebSocket 进入端点
var SOCKET_ENDPOINT = "/mydlq";
// 设置订阅消息的请求前缀
var SUBSCRIBE_PREFIX = "/topic"
// 设置订阅消息的请求地址
var SUBSCRIBE = "";
// 设置服务器端点,访问服务器中哪个接口
var SEND_ENDPOINT = "/app/test";/* 进行连接 */
function connect() {// 设置 SOCKETvar socket = new SockJS(SOCKET_ENDPOINT);// 配置 STOMP 客户端stompClient = Stomp.over(socket);// STOMP 客户端连接stompClient.connect({}, function (frame) {alert("连接成功");});
}/* 订阅信息 */
function subscribeSocket(){// 设置订阅地址SUBSCRIBE = SUBSCRIBE_PREFIX + $("#subscribe").val();// 输出订阅地址alert("设置订阅地址为:" + SUBSCRIBE);// 执行订阅消息stompClient.subscribe(SUBSCRIBE, function (responseBody) {var receiveMessage = JSON.parse(responseBody.body);$("#information").append("<tr><td>" + receiveMessage.content + "</td></tr>");});
}/* 断开连接 */
function disconnect() {stompClient.disconnect(function() {alert("断开连接");});
}/* 发送消息并指定目标地址 */
function sendMessageNoParameter() {// 设置发送的内容var sendContent = $("#content").val();// 设置待发送的消息内容var message = '{"destination": "' + SUBSCRIBE + '", "content": "' + sendContent + '"}';// 发送消息stompClient.send(SEND_ENDPOINT, {}, message);
}

7. 创建 WebSocket HTML

创建用于展示 WebSocket 相关功能的 WEB HTML 页面 index.html,内容如下:

<!DOCTYPE html>
<html>
<head><title>Hello WebSocket</title><link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script><script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script><script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script><script src="app-websocket.js"></script>
</head>
<body><div id="main-content" class="container" style="margin-top: 10px;"><div class="row"><form class="navbar-form" style="margin-left:0px"><div class="col-md-12"><div class="form-group"><label>WebSocket 连接:</label><button class="btn btn-primary" type="button" onclick="connect();">进行连接</button><button class="btn btn-danger" type="button" onclick="disconnect();">断开连接</button></div><label>订阅地址:</label><div class="form-group"><input type="text" id="subscribe" class="form-control" placeholder="订阅地址"></div><button class="btn btn-warning" onclick="subscribeSocket();" type="button">订阅</button></div></form></div></br><div class="row"><div class="form-group"><label for="content">发送的消息内容:</label><input type="text" id="content" class="form-control" placeholder="消息内容"></div><button class="btn btn-info" onclick="sendMessageNoParameter();" type="button">发送</button></div></br><div class="row"><div class="col-md-12"><h5 class="page-header" style="font-weight:bold">接收到的消息:</h5><table class="table table-striped"><tbody id="information"></tbody></table></div></div></div>
</body>
</html>

8. 启动并进行测试

为了方便测试,需要打开两个不同类型浏览器(因为用户登录后会存 Session,如果一个浏览器不同用户登录会使之前 Session 失效)来进行测试,两个浏览器同时输入地址 http://localhost:8080/index.html 访问测试的前端页面,然后可以看到并没有进入 /index.html 页面,而是跳转到Spring Security 提供的登录的 /login 页面,如下:

两个浏览器中都输入用户名/密码 mydlq1/123456mydlq2/123456 进行登录,然后会回到 /index.html 页面,然后执行下面步骤进行测试:

  1. ”浏览器1”和”浏览器2”点击”进行连接”按钮,连接 WebSocket 服务端;

  2. ”浏览器1”和”浏览器2”中同时设置订阅地址为”/abc”,然后点击订阅按钮进行消息订阅;

  3. ”浏览器1”(用户 mydlq1)设置发送目标用户为”/mydlq2”,”浏览器2”(用户 mydlq2)设置发送目标用户为”/mydlq1”;

  4. ”浏览器1”(用户 mydlq1)设置发送消息为Hi, I’m mydlq1,”浏览器2”(用户 mydlq2)设置发送消息为Hi, I’m mydlq2

  5. 点击发送按钮发送消息;

执行完上面步骤成后,可以在两个不同浏览器中观察到如下内容:

十一. 示例三:实现点对点模式(根据请求头 Header 实现鉴权)

1. Maven 引入相关依赖

同示例二

2. 创建测试实体类

@Data
public class MessageBody {/** 发送消息的用户 */private String from;/** 消息内容 */private String content;/** 目标用户(告知 STOMP 代理转发到哪个用户) */private String targetUser;/** 广播转发的目标地址(告知 STOMP 代理转发到哪个地方) */private String destination;
}@Data
@AllArgsConstructor
public class User {private String username;private String token;
}

3. 配置 WebSocket 通道拦截器

配置 WebSocket 通道拦截器,里面添加两个模拟用户:

  1. 用户 mydlq1Token:123456-1

  2. 用户 mydlq2Token:123456-2

/*** WebSocket 通道拦截器(这里模拟两个测试 Token 方便测试,不做具体 Token 鉴权实现)** @author mydlq*/
public class MyChannelInterceptor implements ChannelInterceptor {/** 测试用户与 token 1 */private User mydlq1 = new User("","123456-1");/** 测试用户与 token 2 */private User mydlq2 = new User("","123456-2");/*** 从 Header 中获取 Token 进行验证,根据不同的 Token 区别用户** @param message 消息对象* @param channel 通道对象* @return 验证后的用户信息*/@Overridepublic Message<?> preSend(Message<?> message, MessageChannel channel) {StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);String token = getToken(message);if (token!=null && accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {Principal user = null;// 提前创建好两个测试 token 进行匹配,方便测试if (mydlq1.getToken().equals(token)){user = () -> mydlq1.getUsername();} else if (mydlq2.getToken().equals(token)){user = () -> mydlq2.getUsername();}accessor.setUser(user);}return message;}/*** 从 Header 中获取 TOKEN** @param message 消息对象* @return TOKEN*/private String getToken(Message<?> message){Map<String,Object> headers = (Map<String, Object>) message.getHeaders().get("nativeHeaders");if (headers !=null && headers.containsKey("token")){List<String> token = (List<String>)headers.get("token");return String.valueOf(token.get(0));}return null;}}

4. 创建 WebSocket 配置类

创建 WebSocket 配置类,配置进行连接注册的端点 /mydlq 和消息代理前缀 /queue 及接收客户端发送消息的前缀 /app

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {/*** 配置 WebSocket 进入点,及开启使用 SockJS,这些配置主要用配置连接端点,用于 WebSocket 连接** @param registry STOMP 端点*/@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/mydlq").withSockJS();}/*** 配置消息代理选项** @param registry 消息代理注册配置*/@Overridepublic void configureMessageBroker(MessageBrokerRegistry registry) {// 设置一个或者多个代理前缀,在 Controller 类中的方法里面发生的消息,会首先转发到代理从而发送到对应广播或者队列中。registry.enableSimpleBroker("/queue");// 配置客户端发送请求消息的一个或多个前缀,该前缀会筛选消息目标转发到 Controller 类中注解对应的方法里registry.setApplicationDestinationPrefixes("/app");// 服务端通知特定用户客户端的前缀,可以不设置,默认为userregistry.setUserDestinationPrefix("/user");}/*** 配置通道拦截器,用于获取 Header 的 Token 进行鉴权** @param registration 注册通道配置类*/@Overridepublic void configureClientInboundChannel(ChannelRegistration registration) {registration.interceptors(new MyChannelInterceptor());}}

5. 创建测试 Controller 类

@Controller
public class MessageController {@Autowiredprivate SimpMessageSendingOperations simpMessageSendingOperations;/*** 点对点发送消息,将消息发送到指定用户*/@MessageMapping("/test")public void sendUserMessage(Principal principal, MessageBody messageBody) {// 设置发送消息的用户messageBody.setFrom(principal.getName());// 调用 STOMP 代理进行消息转发simpMessageSendingOperations.convertAndSendToUser(messageBody.getTargetUser(), messageBody.getDestination(), messageBody);}}

6. 创建 WebSocket JS

创建用于操作 WebSocket 的 JS 文件 app-websocket.js,内容如下:

// 设置 STOMP 客户端
var stompClient = null;// 设置 WebSocket 进入端点
var SOCKET_ENDPOINT = "/mydlq";
// 设置订阅消息的请求地址前缀
var SUBSCRIBE_PREFIX  = "/queue";
// 设置订阅地址
var SUBSCRIBE = "";
// 设置服务器端点,访问服务器中哪个接口
var SEND_ENDPOINT = "/app/test";/* 进行连接 */
function connect() {// 设置 SOCKETvar socket = new SockJS(SOCKET_ENDPOINT);// 配置 STOMP 客户端stompClient = Stomp.over(socket);// 获取 TOKENvar myToken = $("#myToken").val();// STOMP 客户端连接stompClient.connect({token: myToken}, function (frame) {alert("连接成功");});
}/* 订阅信息 */
function subscribeSocket(){// 设置订阅地址SUBSCRIBE = SUBSCRIBE_PREFIX + $("#subscribe").val();// 输出订阅地址alert("设置订阅地址为:" + SUBSCRIBE);// 执行订阅消息stompClient.subscribe("/user" + SUBSCRIBE, function (responseBody) {var receiveMessage = JSON.parse(responseBody.body);console.log(receiveMessage);$("#information").append("<tr><td>" + receiveMessage.content + "</td></tr>");});
}/* 断开连接 */
function disconnect() {stompClient.disconnect(function() {alert("断开连接");});
}/* 发送消息并指定目标地址 */
function sendMessageNoParameter() {// 设置发送的内容var sendContent = $("#content").val();// 设置发送的用户var sendUser = $("#targetUser").val();// 设置待发送的消息内容var message = '{"targetUser":"' + sendUser + '", "destination": "' + SUBSCRIBE + '", "content": "' + sendContent + '"}';// 发送消息stompClient.send(SEND_ENDPOINT, {}, message);
}

7. 创建 WebSocket HTML

创建用于展示 WebSocket 相关功能的 WEB HTML 页面 index.html,内容如下:

<!DOCTYPE html>
<html>
<head><title>Hello WebSocket</title><link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script><script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script><script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script><script src="app-websocket.js"></script>
</head>
<body>
<div id="main-content" class="container" style="margin-top: 10px;"><div class="row"><form class="navbar-form" style="margin-left:0px"><div class="col-md-12"><div class="form-group"><label>WebSocket 连接:</label><button class="btn btn-primary" type="button" onclick="connect();">进行连接</button><button class="btn btn-danger" type="button" onclick="disconnect();">断开连接</button></div><label>订阅地址:</label><div class="form-group"><input type="text" id="subscribe" class="form-control" placeholder="订阅地址"></div><button class="btn btn-warning" onclick="subscribeSocket();" type="button">订阅</button></div></form></div></br><div class="row"><div class="form-group"><label>TOKEN 信息:</label><input type="text" id="myToken" class="form-control" placeholder="TOKEN 信息"><label>发送的目标用户:</label><input type="text" id="targetUser" class="form-control" placeholder="发送的用户"><label for="content">发送的消息内容:</label><input type="text" id="content" class="form-control" placeholder="消息的内容"></div><button class="btn btn-info" onclick="sendMessageNoParameter();" type="button">发送</button></div></br><div class="row"><div class="col-md-12"><h5 class="page-header" style="font-weight:bold">接收到的消息:</h5><table class="table table-striped"><tbody id="information"></tbody></table></div></div>
</div>
</body>
</html>

8. 启动并进行测试

为了方便测试,需要打开两个不同类型浏览器(这里模拟通过 Header 传 Token 的方式进行用户验证,具体登录逻辑不实现,而是直接使用事先配置好的两个用户 Token 进行模拟)来进行测试,两个浏览器同时输入地址 http://localhost:8080/index.html 访问测试的前端页面 ``/index.html` 如下:

  • 浏览器1:

    • 用户:mydlq1

    • Token:123456789-1

  • 浏览器2:

    • 登录的用户:mydlq2

    • Token:123456789-2

两个浏览器中都执行下面步骤进行测试:

  1. 浏览器1浏览器2点击进行连接按钮,连接 WebSocket 服务端;

  2. 浏览器1浏览器2中同时设置订阅地址为/abc,然后点击订阅按钮进行消息订阅;

  3. 浏览器1(用户 mydlq1)在 TOken 信息一栏中填写模拟用户 mydlq1 的 Token 串,浏览器2(用户 mydlq2)填写模拟用户 mydlq2 的 Token 串;

  4. 浏览器1(用户 mydlq1)设置发送目标用户为/mydlq2浏览器2(用户 mydlq2)设置发送目标用户为/mydlq1

  5. 浏览器1(用户 mydlq1)设置发送消息为Hi, I’m mydlq1浏览器2(用户 mydlq2)设置发送消息为Hi, I’m mydlq2

  6. 点击发送按钮发送消息;

执行完上面步骤成后,可以在两个不同浏览器中观察到如下内容:

十二. SpringBoot 结合 WebSocket 的常用方法示例

1. WebSocket 开启跨域选项

WebSocket 配置类,里面设置允许跨域,内容如下:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {@Overridepublic void configureMessageBroker(MessageBrokerRegistry registry) {registry.enableSimpleBroker("/queue");registry.setApplicationDestinationPrefixes("/app");}@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/mydlq")// 设置允许跨域,设置为"*"则为允许全部域名.setAllowedOrigins("*").withSockJS();}}

2. WebSocket 用户上、下线监听

创建 WebSocket 用户上线、下线处理器,内容如下:

@Configuration
public class HttpWebSocketHandlerDecoratorFactory implements WebSocketHandlerDecoratorFactory {/*** 配置 webSocket 处理器** @param webSocketHandler webSocket 处理器* @return webSocket 处理器*/@Overridepublic WebSocketHandler decorate(WebSocketHandler webSocketHandler) {return new WebSocketHandlerDecorator(webSocketHandler) {/*** websocket 连接时执行的动作* @param session    websocket session 对象* @throws Exception 异常对象*/@Overridepublic void afterConnectionEstablished(final WebSocketSession session) throws Exception {// 输出进行 websocket 连接的用户信息if (session.getPrincipal() != null) {String username = session.getPrincipal().getName();System.out.println("用户:" + username + "上线");super.afterConnectionEstablished(session);}}/*** websocket 关闭连接时执行的动作* @param session websocket session 对象* @param closeStatus 关闭状态对象* @throws Exception 异常对象*/@Overridepublic void afterConnectionClosed(final WebSocketSession session, CloseStatus closeStatus) throws Exception {// 输出关闭 websocket 连接的用户信息if (session.getPrincipal() != null) {String username = session.getPrincipal().getName();System.out.println("用户:" + username + "下线");super.afterConnectionClosed(session, closeStatus);}}};}}

WebSocket 配置类中实现 configureWebSocketTransport() 方法,将上面 WebSocket 处理器加到其中,如下:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {@Overridepublic void configureMessageBroker(MessageBrokerRegistry registry) {registry.enableSimpleBroker("/queue");registry.setApplicationDestinationPrefixes("/app");}@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/mydlq").withSockJS();}/*** 添加 WebSocket 用户上、下线监听器*/@Overridepublic void configureWebSocketTransport(WebSocketTransportRegistration registry) {registry.addDecoratorFactory(new HttpWebSocketHandlerDecoratorFactory());}}

十三. 总结

本文从原理到实践详细的介绍了WebSocket,希望你们喜欢.....

特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:长按订阅更多精彩▼如有收获,点个在看,诚挚感谢

3w字带你揭开WebSocket的神秘面纱~相关推荐

  1. 对“熵”一知半解?带你揭开“熵”的神秘面纱

    大家好啊,我是董董灿. 你还记得是在什么时候了解的"熵"这个概念吗?现在还能说清楚这个概念是什么意思吗? 也许记得物理热力定律的你,会说"熵代表物质的混乱程度,在热力学零 ...

  2. 来自未来的缓存 Caffeine,带你揭开它的神秘面纱

    作者 | Garnett 来源 | Garnett的Java之路(ID:gh_009246af52d4) 头图 |  CSDN 下载自东方IC caffeine是什么,它和redis什么区别,有哪些作 ...

  3. 性能再提升70%?大咖前瞻带你揭开.NET6的神秘面纱!

    本月初微软官宣 .NET6 的RC1即将在11月正式发布,这意味着 .NET6 正式版跟我们见面的时间又近了一步.在之前的 .NET6 预览版本中,微软加入了大量新功能特性,而在最终版本中将不再额外加 ...

  4. 全网首篇深度剖析PoolFormer模型,带你揭开MetaFormer的神秘面纱

    文章目录 摘要 作者简介 模型分析 Input Emb模块 to_2tuple函数 nn.Conv2d nn.Identity() Input Emb模块源码 PoolFormerBlock Norm ...

  5. 图解二进制,带你揭开二进制的神秘面纱

    你好,这里是网络技术联盟站. 二进制,多么熟悉的字眼,相信只要是学计算机的,二进制绝对是入门的第一节课必学的知识点.你肯定经常会听说"计算机底层数据传输就是通过二进制流".&quo ...

  6. 了解黑客的关键工具---揭开Shellcode的神秘面纱

    2019独角兽企业重金招聘Python工程师标准>>> ref:  http://zhaisj.blog.51cto.com/219066/61428/ 了解黑客的关键工具---揭开 ...

  7. [转]揭开正则表达式的神秘面纱

    揭开正则表达式的神秘面纱 关闭高亮 [原创文章,转载请保留或注明出处:http://www.regexlab.com/zh/regref.htm] 引言 正则表达式(regular expressio ...

  8. 揭开木马的神秘面纱 2

    揭开木马的神秘面纱zz 2 离冰河二的问世已经快一年了,大家对于木马这种远程控制软件也有了一定的认 识,比如:他会改注册表,他会监听端口等等,和一年前几乎没有人懂得木马是什么东   西相比,这是一个质 ...

  9. 【翻译】揭开HTML5的神秘面纱

    写在前面的话: 这篇文章摘自Mozilla官网,主要针对HTML5和本地应用发表了一些.没有设计到技术,所以基本是逐字翻译,但愿我蹩脚的英语水平能把大师的 Chris Heilmann的思想整理明白. ...

最新文章

  1. 2021年春季学期-信号与系统-第九次作业参考答案-第三小题
  2. C++ IPv4与IPv6的兼容编码(转,出自http://blog.csdn.net/ligt0610/article/details/18667595)...
  3. redhat6.4中手动创建oracle11g数据库
  4. python网络通信效率_Python之网络通信
  5. Linux主机无法安装软件故障排查
  6. 关于类类型的隐式类型转换
  7. 如何在 Mac 上设置图片或视频的默认打开应用?
  8. 如何下载MySQL的JDBC驱动包
  9. python导入包如果找不到
  10. 服务器自动关闭远程打印服务,实现远程打印的方法:无线网络打印服务器
  11. 深信服各种设备管理地址
  12. js原生后代选择器_js 后代选择器
  13. 全名k歌自定义图文链接(卡片)
  14. 又是暴力裁员?腾讯 7 年老员工一朝被裁,官方回应了...
  15. 0.进校的第一张Excel表:“住宿分布表” ——《Excel“智能化”之路》 系列文章
  16. 综述笔记-多无人机多目标任务分配1
  17. Autojs+VSCode 的基本使用
  18. 服务器修复 dns,AD 林恢复 - 配置 DNS 服务器服务
  19. 43. Systemd的Unit配置详解,unit文件位置,优先级,unit类型,unit文件字段详解,Unit/Service/Install字段,添加mysql服务等例子
  20. CAN数据采集助手 Python

热门文章

  1. Linux三剑客之grep详解
  2. 线段树分治 ---- F. Extending Set of Points(线段树分治 + 可撤销并查集)
  3. forms身份验证 不跳转_“东湖24小时”玩不够?收好这份指南,365天不重样
  4. Dirichlet前缀和及其拓展
  5. codeblocks安装及使用教程(手把手傻瓜式教学)
  6. 解题报告:P5960 【模板】差分约束算法(及常用技巧)
  7. 【数据结构】堆,大根堆,小根堆,优先队列 详解
  8. sql%notfound与exception
  9. etc下没有mysql_我在linux下,安装mysql的时候,cp support-files/my-medium.cnf /etc/my.cnf找不到my-medium.cnf...
  10. 许昌科技学校工业机器人_【调研】省人大常委会副主任徐济超到许昌科技学校进行专题调研...