一、聊聊 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-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');
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篇" ,期待更多精彩内容-

补习系列(20)-大话 WebSocket 与 "尬聊"的实现的更多相关文章

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

    目录 一.聊聊 WebSocket 二.Stomp 是个什么鬼 三.SpringBoot 整合 WebSocket A. 引入依赖 B. WebSocket 配置 C. 控制器 D. 前端实现 四.参 ...

  2. 补习系列(19)-springboot JPA + PostGreSQL

    目录 SpringBoot 整合 PostGreSQL 一.PostGreSQL简介 二.关于 SpringDataJPA 三.整合 PostGreSQL A. 依赖包 B. 配置文件 C. 模型定义 ...

  3. 补习系列(1)-springboot项目基础搭建课

    目录 前言 一.基础结构 二.添加代码 三.应用配置 四.日志配置 五.打包部署 小结 前言 springboot 最近火的不行,目前几乎已经是 spring 家族最耀眼的项目了.抛开微服务.技术社区 ...

  4. 补习系列(17)-springboot mongodb 内嵌数据库

    目录 简介 一.使用 flapdoodle.embed.mongo A. 引入依赖 B. 准备测试类 C. 完善配置 D. 启动测试 细节 二.使用Fongo A. 引入框架 B. 准备测试类 C.业 ...

  5. 补习系列(17)-springboot mongodb 内嵌数据库【华为云技术分享】

    目录 简介 一.使用 flapdoodle.embed.mongo A. 引入依赖 B. 准备测试类 C. 完善配置 D. 启动测试 细节 二.使用Fongo A. 引入框架 B. 准备测试类 C.业 ...

  6. 一例完整的websocket实现群聊demo

    前言 业余我都会花一些时间在tcp.http和websocket等领域的学习,现在觉得有点收获,所以把一个基于websocket的群聊功能的例子提供给大家玩玩.当然这是一个很完整的例子,包括webso ...

  7. ASP.NET MVC+EF框架+EasyUI实现权限管理系列(20)-多条件模糊查询和回收站还原的实现

    原文:ASP.NET MVC+EF框架+EasyUI实现权限管理系列(20)-多条件模糊查询和回收站还原的实现 ASP.NET MVC+EF框架+EasyUI实现权限管系列 (开篇)   (1):框架 ...

  8. 补习系列(18)-springboot H2 迷你数据库

    目录 关于 H2 一.H2 用作本地数据库 1. 引入依赖: 2. 配置文件 3. 样例数据 二.H2 用于单元测试 1. 依赖包 2. 测试配置 3. 测试代码 小结 关于 H2 H2 数据库是一个 ...

  9. 补习系列(16)-springboot mongodb 数据库应用技巧

    目录 一.关于 MongoDB 二.Spring-Data-Mongo 三.整合 MongoDB CRUD A. 引入框架 B. 数据库配置 C. 数据模型 D. 数据操作 E. 自定义操作 四.高级 ...

随机推荐

  1. Angular TypeScript开发环境集成jQuery扩展插件

    集成步骤: 1.安装jquery极其扩展插件库ts定义文件 npm install jquery --save npm install --save-dev @types/jquery npm ins ...

  2. 转载 Elasticsearch开发环境搭建(Eclipse\MyEclipse + Maven)

    概要: 1.使用Eclipse搭建Elasticsearch详情参考下面链接 2.Java Elasticsearch 配置 3.ElasticSearch Java Api(一) -添加数据创建索引 ...

  3. RabbitMQ In JAVA 介绍及使用

    介绍: RabbitMQ是开源的消息中间件,它是轻量级的,支持多种消息传递协议,可以部署在分布式和联合配置中,以满足高级别.高可用性需求.并且可在许多操作系统和云环境上运行,并为大多数流行语言提供了广 ...

  4. leetCode刷题(将字符串转成W形状的字符串再以Z形字符串输出)

    The string "PAYPALISHIRING" is written in a zigzag pattern on a given number of rows like ...

  5. lintcode 在O(1)时间复杂度删除链表节点

    题目要求 给定一个单链表中的一个等待被删除的节点(非表头或表尾).请在在O(1)时间复杂度删除该链表节点. 样例 Linked list is 1->2->3->4, and giv ...

  6. EOS技术研究:合约与数据库交互

    智能合约操作链数据库是很常见的应用场景.EOS提供了专门的工具来做这件事(相当于Ethereum的leveldb),专业术语叫做持久化API,本文将完整严密地介绍这个工具以及对它的使用测试. 关键字: ...

  7. mybatis查询异常-Error querying database. Cause: java.lang.ClassCastException: org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.List

    背景,mybatis查询的时候直接取的sqlsession,没有包装成SqlSessionTemplate,没有走spring提供的代理. 然后我写的获取sqlsession的代码没有考虑到并发的情况 ...

  8. Python高级教程

    关键字is 和 == 的区别 a = 'hello world' b = 'hello world' a == b #返回True a is b #返回False 注意:is 判断是否是一个ID, = ...

  9. Activity,Window,View之间是什么关系?

    Activity在onCreate之前调用attach方法,在attach方法中会创建window对象.window对象创建时并没有创建 Decor对象对象.用户在Activity中调用setCont ...

  10. css3 绘制图形

    星形: .star-six { width:; height:; border-left: 50px solid transparent; border-right: 50px solid trans ...