一、WebSocket概述

1.1 建立WebSocket基本流程

WebSocket协议(RFC 6455)是HTML5开始提供的一种标准化方法,可通过单个TCP连接在客户端和服务器之间建立全双工双向通信通道,实现长连接,它是与HTTP不同的TCP协议。WebSocket协议允许服务端主动向客户端推送数据。
  WebSocket交互始于一个HTTP请求,该请求使用HTTP Upgrade请求头进行升级,或在这种情况下切换到WebSocket协议:
image.png
  Upgrade和Connection用于声明自身发起的是WebSocket协议;Sec-WebSocket-Key是浏览器随机生成的,用于验证服务器; Sec-WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。
  而具有WebSocket支持的服务器代替通常的200状态代码,返回类似于以下内容的响应,表示已经接受到请求,成功建立了WebSocket:
image.png
  成功握手后,HTTP升级请求的基础TCP套接字将保持打开状态,客户端和服务器均可继续发送和接收消息。
image.png

1.2 WebSocket和HTTP的区别

  在HTTP和REST中,应用程序被建模为许多URL。为了与应用程序交互,采用请求-响应方式,服务器根据HTTP URL、方法和请求头将请求路由到适当的处理程序。而且HTTP通信只能由客户端发起,是无状态、无连接的。
  这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦,只能使用轮询或长轮询。但是它们效率低,非常浪费资源(因为必须不停连接,或者HTTP连接始终打开)。
  相比之下,在WebSocket中,通常只有一个URL用于初始连接,随后,所有应用程序消息都在同一TCP连接上流动。这指向了一种完全不同的异步的、事件驱动的消息传递体系结构。它的最大特点是服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息。
  WebSocket客户端和服务器可以通过HTTP握手请求上的Sec-WebSocket-Protocol请求头,协商使用更高级别的消息传递协议(如STOMP)。
image.png
注意:
  WebSocket URL使用了ws://协议标识符,表明这是一个基本的WebSocket连接,如果是安全WebSocket的话,协议标识符将会变为wss://。

二、Spring的WebSocket使用

2.1 WebSocket API

  Spring框架提供了一个WebSocket API,可用于编写处理WebSocket消息的客户端和服务器端应用程序。具体的文档可以在Spring官网查看。

2.1.1 WebSocketHandler

  为了在Spring使用相应的API来处理消息,实现对WebSocket的连接回调及消息发送的操作,必须编写一个继承WebSocketHandler的类,或者使用扩展的TextWebSocketHandler或BinaryWebSocketHandler。
  TextWebSocketHandler是AbstractWebSocketHandler(抽象类)的子类,它会拒绝处理二进制消息。它重载了handleBinaryMessage()方法,如果收到二进制消息,将会关闭WebSocket连接。其中,对于几个重要的方法,需要进行自定义实现:

  • afterConnectionEstablished:建立连接后触发的回调;
  • afterConnectionClosed:断开连接后触发的回调;
  • handleTextMessage:收到客户端消息时触发的回调;
  • handleTransportError:传输消息出错时触发的回调。

    2.1.2 HandshakeInterceptor 和 WebSocketConfigurer

      定制初始HTTP WebSocket握手请求的最简单方法就是通过HandshakeInterceptor,它提供了“握手之前”和“握手之后”要进行的方法:

  • beforeHandshake:可以在握手升级之前进行一些操作,比如将连接的用户信息进行保存等;

  • afterHandshake:握手升级之后的处理。

  配置类实现WebSocketConfigurer接口,可以选择使用内置的拦截器将HTTP会话属性传递到WebSocket会话,也可以使用实现了HandshakeInterceptor接口的自定义拦截器:

  • addHandler:添加Handler处理器;
  • addInterceptors:添加拦截器。

    2.1.3 设置跨域请求——setAllowedOrigins

      从Spring Framework 4.1.5开始,WebSocket和SockJS的默认行为是仅接受起源相同的请求,此检查主要用于浏览器客户端,三种可能的行为是:

  • 仅允许相同来源的请求(Allow only same-origin requests):此模式是默认情况,启用此模式时,不支持IE6和IE7。

  • 允许指定来源列表(Allow a specified list of origins):每个允许的来源必须以http://或https://开头。在此模式下,启用SockJS时,将禁用IFrame传输。因此,启用此模式时,不支持IE6到IE9。
  • 允许所有来源(Allow a specified list of origins):进行跨域设置,要启用此模式,应提供 * 作为允许值,在这种模式下,所有传输都可用。

  setAllowedOrigins方法在WebSocketConfigurer进行注册时就可以调用使用。

2.1.4 SockJS支持——withSockJS

  WebSocket是一个相对比较新的规范,在公共Internet上,控件之外的限制性代理可能会阻止WebSocket交互,所以需要一种WebSocket备选方案,解决此问题的方法是WebSocket仿真。
  SockJS是WebSocket技术的一种模拟,在表面上,它尽可能对应WebSocket API,但是在底层非常智能。如果WebSocket技术不可用的话,就会选择另外的通信方式。SockJS的目标是让应用程序使用WebSocket API,但在运行的过程中,在必要时使用非WebSocket替代方案,而无需更改应用程序代码。
  withSockJS方法在WebSocketConfigurer进行注册时就可以调用使用,应对浏览器不支持WebSocket协议的时候降级为轮询处理。

2.2 WebSocket在项目中的使用

  项目内容:客户端登陆之后,Vue加载页面时创建WebSocket连接,此时,Spring MVC拦截器拦截到WebSocket请求,保存用户信息,建立连接,同时调用SpringWebSocketHandler中重写的相应的回调方法。
image.png

第一步:继承HttpSessionHandshakeInterceptor编写自定义拦截器

从前端发来的请求中的name参数获取登录者信息,并进行保存

  1. public class SpringWebSocketHandlerInterceptor extends HttpSessionHandshakeInterceptor {
  2. @Override
  3. public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
  4. ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
  5. //获取请求URL的参数name的值
  6. String name=servletRequest.getServletRequest().getParameter("name");
  7. //若存在会话则返回该会话,否则新建一个会话
  8. HttpSession session = servletRequest.getServletRequest().getSession(true);
  9. if (session != null) {
  10. //将连接的用户信息保存在Map中
  11. attributes.put("webSocketUser",name);
  12. }
  13. return super.beforeHandshake(request, response, wsHandler, attributes);
  14. }
  15. @Override
  16. public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {
  17. System.out.println("After Handshake");
  18. super.afterHandshake(request, response, wsHandler, ex);
  19. }
  20. }

第二步:继承TextWebSocketHandler编写自定义处理器

编写回调方法用于连接后处理

  1. @Component
  2. public class SpringWebSocketHandler extends TextWebSocketHandler {
  3. private static Map<String, WebSocketSession> users;
  4. //应用ConcurrentHashMap保证多线程安全
  5. static {
  6. users = new ConcurrentHashMap<>();
  7. }
  8. /**
  9. * 建立连接后触发的回调
  10. * 记录用户的连接标识,便于后面发信息
  11. */
  12. @Override
  13. public void afterConnectionEstablished(WebSocketSession session) throws Exception {
  14. String userName = getClientId(session);
  15. if(userName != null){
  16. System.out.println("用户:"+userName+"已经登录!");
  17. users.put(userName, session);
  18. session.sendMessage(new TextMessage("server:成功建立socket连接"));
  19. System.out.println("当前在线用户" + users.size() + ": " + users);
  20. }
  21. }
  22. /**
  23. * 断开连接后触发的回调
  24. * 连接已关闭,移除在Map集合中的记录
  25. */
  26. @Override
  27. public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
  28. String userName = getClientId(session);
  29. System.out.println("\n用户" + userName + "已退出: " + status);
  30. users.remove(userName);
  31. System.out.println("剩余在线用户" + users.size() + ": " + users);
  32. }
  33. /**
  34. * 收到客户端消息时触发的回调
  35. * 对H5 Websocket的send方法进行处理
  36. */
  37. @Override
  38. protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
  39. //显示客户端内容
  40. System.out.println("client:" + message.getPayload());
  41. WebSocketMessage message1 = new TextMessage("server:已经收到你发来的数据");
  42. try {
  43. session.sendMessage(message1);
  44. } catch (IOException e) {
  45. e.printStackTrace();
  46. }
  47. }
  48. /**
  49. * 传输消息出错时触发的回调
  50. * 连接出错处理,主要是关闭出错会话的连接,和删除在Map集合中的记录
  51. */
  52. @Override
  53. public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
  54. if (session.isOpen()) {
  55. session.close();
  56. }
  57. System.out.println("连接出错");
  58. users.remove(getClientId(session));
  59. }
  60. /**
  61. * 发送消息给指定用户
  62. */
  63. public boolean sendMessageToUser(String clientId, TextMessage message) {
  64. if (users.get(clientId) == null) return false;
  65. WebSocketSession session = users.get(clientId);
  66. System.out.println("sendMessage:" + session);
  67. if (!session.isOpen()) return false;
  68. try {
  69. session.sendMessage(message);
  70. } catch (IOException e) {
  71. e.printStackTrace();
  72. return false;
  73. }
  74. return true;
  75. }
  76. /**
  77. * 广播消息
  78. */
  79. public boolean sendMessageToAllUsers(TextMessage message){
  80. boolean allSendSuccess = true;
  81. Set<String> clientIds = users.keySet();
  82. WebSocketSession session = null;
  83. for (String clientId : clientIds) {
  84. try {
  85. session = users.get(clientId);
  86. if(session.isOpen()){
  87. session.sendMessage(message);
  88. }
  89. } catch (IOException e) {
  90. e.printStackTrace();
  91. allSendSuccess=false;
  92. }
  93. }
  94. return allSendSuccess;
  95. }
  96. private String getClientId(WebSocketSession session){
  97. try{
  98. String clientId = (String) session.getAttributes().get("webSocketUser");
  99. return clientId;
  100. }catch (Exception e){
  101. return null;
  102. }
  103. }
  104. }

第三步:配置类实现WebSocketConfigurer接口

作为配置类用于配置Spring WebSocket

  1. @Configuration
  2. @EnableWebMvc
  3. @EnableWebSocket
  4. public class SpringWebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{
  5. @Override
  6. public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
  7. //添加自定义的Handler及拦截器,用于处理对应的websocket请求
  8. registry.addHandler(webSocketHandler(),"/websocket/socketServer")
  9. .addInterceptors(new SpringWebSocketHandlerInterceptor())
  10. .setAllowedOrigins("*");
  11. registry.addHandler(webSocketHandler(), "/sockjs/socketServer")
  12. .addInterceptors(new SpringWebSocketHandlerInterceptor())
  13. .setAllowedOrigins("*").withSockJS();
  14. }
  15. @Bean
  16. public TextWebSocketHandler webSocketHandler(){
  17. return new SpringWebSocketHandler();
  18. }
  19. }

前端Vue在mounted阶段初始化WebSocket连接
  1. initWebSocket() {
  2. let name = this.$store.state.peopleList.name;
  3. console.log("name", name);
  4. const wsuri = `wss://www.quanjiaoxiaofang.cn/websocket/socketServer?name=${name}-accident`;
  5. this.websock = new WebSocket(wsuri);
  6. this.websock.onmessage = this.websocketonmessage;
  7. this.websock.onopen = this.websocketonopen;
  8. this.websock.onerror = this.websocketonerror;
  9. this.websock.onclose = this.websocketclose;
  10. }

三、XML配置

3.1 引入jar包

  1. <dependency>
  2. <groupId>org.springframework</groupId>
  3. <artifactId>spring-orm</artifactId>
  4. <version>4.2.4.RELEASE</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework</groupId>
  8. <artifactId>spring-webmvc</artifactId>
  9. <version>4.2.4.RELEASE</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.springframework</groupId>
  13. <artifactId>spring-websocket</artifactId>
  14. <version>4.2.4.RELEASE</version>
  15. </dependency>
  16. <dependency>
  17. <groupId>org.springframework</groupId>
  18. <artifactId>spring-messaging</artifactId>
  19. <version>4.2.4.RELEASE</version>
  20. </dependency>

3.2 配置spring-websocket.xml文件

这里注意的是:xsi:schemaLocation属性后面两行一定要有。

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:websocket="http://www.springframework.org/schema/websocket"
  5. xsi:schemaLocation="
  6. http://www.springframework.org/schema/beans
  7. http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
  8. http://www.springframework.org/schema/websocket
  9. http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd">
  10. <bean id="websocket" class="com.web.common.util.websocket.WebSocketHander" />
  11. <websocket:handlers>
  12. <websocket:mapping path="/echo" handler="websocket" />
  13. <websocket:handshake-interceptors>
  14. <bean class="com.web.common.util.websocket.HandshakeInterceptor" />
  15. </websocket:handshake-interceptors>
  16. </websocket:handlers>
  17. </beans>

3.3 在applicationContext.xml中引入spring-websocket.xml文件

  1. <!-- 引入websocket -->
  2. <import resource="spring-websocket.xml"/>

3.4 在web.xml中引入applicationContext.xml

  1. <!-- Spring配置文件 -->
  2. <context-param>
  3. <param-name>contextConfigLocation</param-name>
  4. <param-value>classpath:applicationContext.xml</param-value>
  5. </context-param>

参考文档

https://blog.csdn.net/weixin_43415481/article/details/114526426
https://www.runoob.com/html/html5-websocket.html
spring.io官网