学习WebSocket

Posted Java3y

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学习WebSocket相关的知识,希望对你有一定的参考价值。

一、聊聊 WebSocket

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

  • 在线游戏,提供实时的操作交互体验

  • 社交平台,与好友实时的私信对话

  • 新闻动态,获得感兴趣的主题信息推送

...

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

这些方式包括有:

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

学习WebSocket

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

HTTP 请求头部会浪费一些带宽

频繁重建连接会造成很大的开销...


  • 第二种是 Comet,这个词好像翻译为"彗星"?这个是采用 streaming 或 long-pulling 的长连接技术:服务器在收到请求时先挂起,等待有事件发生时才返回数据。

学习WebSocket

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

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

当然,有HTML5标准给它站台,后台杠杠的~

那么一个 WebSocket 的请求响应长成怎么样呢?

看下面这个图:

学习WebSocket


二、Stomp 是个什么鬼

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

就比如目前主流的消息队列服务器如RabbitMQ、ActiveMQ 都支持 Stomp 协议。


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

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


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

 
   
   
 
  1. CONNECT

  2. accept-version:1.1,1.0

  3. heart-beat:10000,10000\n\n\u0000



  4. SEND

  5. destination:/app/message\ncontent-length:6


  6. 发送内容\u0000

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

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


三、SpringBoot 整合 WebSocket

在介绍完WebSocket 之后,接下来干什么呢?

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

这当然可以!

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

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

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

A. 引入依赖

 
   
   
 
  1. <dependency>

  2. <groupId>org.springframework.boot</groupId>

  3. <artifactId>spring-boot-starter-thymeleaf</artifactId>

  4. <version>${springboot.version}</version>

  5. <exclusions>

  6. <exclusion>

  7. <groupId>org.slf4j</groupId>

  8. <artifactId>slf4j-api</artifactId>

  9. </exclusion>

  10. </exclusions>

  11. </dependency>


  12. <!--websocket-->

  13. <dependency>

  14. <groupId>org.springframework.boot</groupId>

  15. <artifactId>spring-boot-starter-websocket</artifactId>

  16. <version>${springboot.version}</version>

  17. </dependency>


  18. <dependency>

  19. <groupId>org.springframework.boot</groupId>

  20. <artifactId>spring-boot-devtools</artifactId>

  21. <version>${springboot.version}</version>

  22. <optional>true</optional>

  23. </dependency>


  24. <dependency>

  25. <groupId>org.webjars</groupId>

  26. <artifactId>webjars-locator-core</artifactId>

  27. <version>0.32</version>

  28. </dependency>

  29. <dependency>

  30. <groupId>org.webjars</groupId>

  31. <artifactId>sockjs-client</artifactId>

  32. <version>1.0.2</version>

  33. </dependency>

  34. <dependency>

  35. <groupId>org.webjars</groupId>

  36. <artifactId>stomp-websocket</artifactId>

  37. <version>2.3.3</version>

  38. </dependency>

  39. <dependency>

  40. <groupId>org.webjars</groupId>

  41. <artifactId>jquery</artifactId>

  42. <version>2.1.4</version>

  43. </dependency>


  44. <dependency>

  45. <groupId>org.foo.springboot</groupId>

  46. <artifactId>base</artifactId>

  47. <version>1.0-SNAPSHOT</version>

  48. </dependency>


  49. <!-- jackson version -->

  50. <dependency>

  51. <groupId>com.fasterxml.jackson.core</groupId>

  52. <artifactId>jackson-databind</artifactId>

  53. <version>2.8.3</version>

  54. </dependency>

  55. <dependency>

  56. <groupId>com.fasterxml.jackson.core</groupId>

  57. <artifactId>jackson-core</artifactId>

  58. <version>2.8.3</version>

  59. </dependency>

添加 spring-boot-starter-websocket 会自动引入spring-websocket 的依赖,而后者就实现了WebSocket 操作的高级封装。

还有一个好消息,就是spring-websocket 也默认支持了 Stomp协议(看吧,Stomp支持者太多了)。而除此之外,还内置了一个叫 SocketJS 的东西。

SocketJS 是一个流行的JS库,主要是在WebSocket之上封装了一层API,用于支持浏览器不兼容WebSocket的情况。

其他组件的说明

  • webjars 主要是将一些前端的框架打包到Jar包中以方便我们使用,这里我们添加了socketJS、stompWebSocket相关的一些包;

  • jackson 用于支持WebSocket消息的编解码,是必须添加的。

B. WebSocket 配置

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

WebSocketConfig.java

 
   
   
 
  1. @Configuration

  2. @EnableWebSocketMessageBroker

  3. public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {


  4. private static final Logger logger = LoggerFactory.getLogger(WebSocketConfig.class);


  5. @Override

  6. public void configureMessageBroker(MessageBrokerRegistry config) {


  7. //设置订阅通道(客户端可订阅)

  8. config.enableSimpleBroker("/topic");


  9. //接收APP(客户端)消息的路由前缀,可通过@MessageMapping 映射到方法

  10. config.setApplicationDestinationPrefixes("/app");

  11. }


  12. @Override

  13. public void registerStompEndpoints(StompEndpointRegistry registry) {


  14. //websocket 连接端点

  15. registry.addEndpoint("/backend").withSockJS();

  16. }


  17. @Override

  18. public void configureWebSocketTransport(final WebSocketTransportRegistration registration) {


  19. //配置拦截器

  20. registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {

  21. @Override

  22. public WebSocketHandler decorate(final WebSocketHandler handler) {

  23. return new WebSocketHandlerDecorator(handler) {

  24. @Override

  25. public void afterConnectionEstablished(final WebSocketSession session) throws Exception {

  26. String username = session.getPrincipal() != null? session.getPrincipal().getName(): "GUEST";

  27. logger.info("{} connect.", username);

  28. super.afterConnectionEstablished(session);

  29. }


  30. @Override

  31. public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {

  32. String username = session.getPrincipal() != null? session.getPrincipal().getName(): "GUEST";

  33. logger.info("{} disconnect.", username);

  34. super.afterConnectionClosed(session, closeStatus);

  35. }

  36. };

  37. }

  38. });

  39. super.configureWebSocketTransport(registration);

  40. }

  41. }

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

  • registerStompEndpoints 用于添加端点,即浏览器通过 ws://xxx 能访问到的路径

  • configureMessageBroker 用于做消息路由配置,包括订阅主题、方法映射路径

C. 控制器

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

ConsoleController.java

 
   
   
 
  1. @Controller

  2. public class ConsoleController {


  3. //输出数据频道

  4. public static final String CHANNEL_CONSOLE = "/topic/console";



  5. @Autowired

  6. private SimpMessagingTemplate template;


  7. /**

  8. * 控制台页面

  9. *

  10. * @return

  11. */

  12. @GetMapping("/console")

  13. public String console() {

  14. return "console";

  15. }


  16. /**

  17. * 接收WebSocket消息方法

  18. * @param message

  19. */

  20. @MessageMapping("/message")

  21. public void onMessage(String message) {

  22. template.convertAndSend(CHANNEL_CONSOLE, "我收到了你的消息:" + message);

  23. }

  24. }

D. 前端实现

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

 
   
   
 
  1. <!DOCTYPE html>

  2. <html>

  3. <head>

  4. <meta charset="UTF-8"></meta>

  5. <title>Web控制台</title>

  6. <script th:src="@{/webjars/sockjs-client/sockjs.min.js}"></script>

  7. <script th:src="@{/webjars/stomp-websocket/stomp.min.js}"></script>

  8. <script th:src="@{/webjars/jquery/jquery.min.js}"></script>

  9. <script type="text/javascript" th:src="@{/static/console.js}"></script>


  10. <style type="text/css">

  11. body { font-family: "Microsoft YaHei" ;}

  12. .span-tv{padding-right:12px}

  13. #console p {padding: 0px; margin: 0px;}

  14. </style>


  15. </head>

  16. <body>


  17. <div style="background-color:#AAA; padding: 5px; border-bottom: 1px solid #333">

  18. <input type="text" id="word" style="width:100px"></input>

  19. <button onclick="sendMessage()">发送消息</button>

  20. <button onclick="reconnect()">重新连接</button>

  21. <button onclick="clearConsole()">清空内容</button>

  22. </div>


  23. <div id="console" style="padding:5px; font-size:10px"></div>

  24. </body>

  25. </html>


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

 
   
   
 
  1. $(document).ready(function(){

  2. //首次打开页面自动连接

  3. connect();

  4. })


  5. //执行连接

  6. function connect() {


  7. //接入端点/backend

  8. var socket = new SockJS('/backend');

  9. window.stompClient = Stomp.over(socket);

  10. window.stompClient.connect({}, function (frame) {

  11. log('Connected: ' + frame);


  12. //订阅服务端输出的 Topic

  13. stompClient.subscribe('/topic/console', function (message) {

  14. log("[服务器说]:" + message.body);

  15. });

  16. });


  17. }


  18. //断开连接

  19. function disconnect() {

  20. if (stompClient !== null) {

  21. stompClient.disconnect();

  22. }

  23. log("Disconnected");

  24. }


  25. //重新连接

  26. function reconnect(){

  27. clearConsole();

  28. disconnect();

  29. connect();

  30. }


  31. //发送消息

  32. function sendMessage(){

  33. var content = $("#word").val();

  34. if(!content){

  35. alert("请输入消息!")

  36. return;

  37. }

  38. //向应用Topic发送消息

  39. stompClient.send("/app/message", {}, content);

  40. log("[你说]:" + content);

  41. }


  42. //记录控制台消息

  43. function log(message){

  44. $("<p></p>").text(message).appendTo($("#console"));

  45. }


  46. //清空控制台

  47. function clearConsole(){

  48. $("#console").empty();

  49. }

http://localhost:8080/console 

进行体验,如下:

好了,这个案例的确很尴尬..

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

以上是关于学习WebSocket的主要内容,如果未能解决你的问题,请参考以下文章

IOS开发-OC学习-常用功能代码片段整理

“未捕获的类型错误:无法在 Websocket Angular JS 上读取未定义的属性‘延迟’”

java SpringRetry学习的代码片段

python 机器学习有用的代码片段

NodeJS中的Websockets。从服务器端WebSocket客户端调用WebSocketServer

学习笔记:python3,代码片段(2017)