美码师 2019-05-03 21:33:09 浏览 150 评论 0
服务器
配置
浏览器
websocket
控制台
- string
- void
- session
- Blog
摘要: [TOC] 一, 聊聊 WebSocket 从 html5 技术流行至今, WebSocket 已经有非常广泛的应用: 在线游戏, 提供实时的操作交互体验 社交平台, 与好友实时的私信对话 新闻动态, 获得感兴趣的主题信息推送 ... 这些场景, 都需要服务器能主动实时的给浏览器或客户端推送消息, 注意关键词是主动, 还有实时! 而在 HTML5 一统江湖之前, 由于 HTTP 在推送场景下的 "薄弱", 我们需要借助一些复杂或者非标准的手段来实现.
[TOC]
一, 聊聊 WebSocket
从 HTML5 技术流行至今, WebSocket 已经有非常广泛的应用:
在线游戏, 提供实时的操作交互体验
社交平台, 与好友实时的私信对话
新闻动态, 获得感兴趣的主题信息推送
...
这些场景, 都需要服务器能主动实时的给浏览器或客户端推送消息, 注意关键词是主动, 还有实时!
而在 HTML5 一统江湖之前, 由于 HTTP 在推送场景下的 "薄弱", 我们需要借助一些复杂或者非标准的手段来实现.
这些方式包括有:
Ajax 轮询, 比如每隔 5 秒钟, 由浏览器对服务器主动请求数据后返回.
在这种方案下, 浏览器需要不断的向服务器发出请求, 问题是比较明显的, 包括:
HTTP 请求头部会浪费一些带宽;
频繁重建连接会造成很大的开销.
Comet, 这个词好像翻译为 "彗星"? 这个是采用 streaming 或 long-pulling 的长连接技术:
服务器在收到请求时先挂起, 等待有事件发生时才返回数据.
Comet 效率提升了不少, 它解决了 Ajax 轮询的部分问题, 利用 HTTP 长连接的特性尽可能的避免了连接, 带宽资源的浪费等等, 于是在很长一段时间 Comet 成为了 Web 推送技术的主流.
But ,.. Comet 的实现技术比较复杂, 不同框架下的实现方式差异很大, 在灵活性, 性能上也有些欠缺.
关于服务端 Comet 的技术可以参考下面这篇经典文章:
Flash, 通过 Flash 插件代码实现 Socket 通讯, 本质上是基于 TCP 的通讯模式, 由于 Flash 需要安装插件以及浏览器的兼容性问题, 目前已经逐渐废弃.
WebSocket 出场
WebSocket 出现的目的没有别的, 就是干掉前面的东西, Both!
最开始 WebSocket 协议由 RFC6455 https://tools.ietf.org/html/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-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="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');
- Windows.stompClient = Stomp.over(socket);
- Windows.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://zh.wikipedia.org/wiki/WebSocket
- https://halfrost.com/websocket/
欢迎继续关注 "美码师的补习系列 - springboot 篇", 如果有任何问题, 欢迎在我的公众号留言联系. 在工作闲暇之时, 我会尽力进行答复.
来源: https://yq.aliyun.com/articles/701027