一、 什么是 WebSocket?

1.1 简介

WebSocket是HTML5提供的一种协议(可在单个TCP连接上进行全双工通讯)。(属于应用层协议)

单工:单向通信。即只能服务器->客户端。例如: UDP协议 半双工:可以服务器->客户端,服务器<-客户端。但是同一时间,只能是一个方向。例如: http协议。 全双工:双向通信。同一时间内既可以客户端->服务器;也可以服务器->客户端。例如:webSocket协议

建立连接后客户端与服务器端是完全平等的,可以互相主动请求(http 只能由客户端发起)
spring-WebSocket官方文档

1.1.1 产生背景

我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。
这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用“轮询”:每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。
轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)

1.1.2 建立连接流程

握手: WebSocket交互始于客户端发起一个HTTP请求,该请求使用HTTP Upgrade标头进行升级,在这种情况下切换到WebSocket协议

建立连接: 成功握手后,HTTP升级请求的基础TCP套接字将保持打开状态,客户端和服务器均可继续发送和接收消息。

1.1.3 http 与 ws的关系

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

http为了与应用程序交互,客户端访问那些URL,即请求-响应样式。服务器根据HTTP URL,方法和标头将请求路由到适当的处理程序。

WebSocket,通常只有一个URL用于初始连接。随后,所有应用程序消息都在同一TCP连接上流动。

1.2 使用场景

常用于动态的场景,需要实时更新,推送比较频繁,或者常用于服务端给客户端推送消息,防止找不到客户端

1.3 Spring 整合 基础WebSocket(推送简单文本和二进制数据)

WebSocket Java配置和XML名称空间支持,用于将前面的WebSocket处理程序映射到特定的URL

  1. import org.springframework.web.socket.config.annotation.EnableWebSocket;
  2. import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
  3. import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
  4. @Configuration
  5. @EnableWebSocket
  6. public class WebSocketConfig implements WebSocketConfigurer {
  7. @Override
  8. public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
  9. registry.addHandler(myHandler(), "/myHandler");
  10. }
  11. @Bean
  12. public WebSocketHandler myHandler() {
  13. return new MyHandler();
  14. }
  15. }

创建WebSocket服务器就像实现WebSocketHandler一样简单,扩展TextWebSocketHandler或BinaryWebSocketHandler。
以下示例使用TextWebSocketHandler:

  1. import org.springframework.web.socket.WebSocketHandler;
  2. import org.springframework.web.socket.WebSocketSession;
  3. import org.springframework.web.socket.TextMessage;
  4. public class MyHandler extends TextWebSocketHandler {
  5. @Override
  6. public void handleTextMessage(WebSocketSession session, TextMessage message) {
  7. // 处理简单消息
  8. }
  9. }

1.4 SockJS

SockJS是一个浏览器JavaScript库

SockJS的目标是让应用程序使用WebSocket API,但在运行时必要时使用非WebSocket替代方案,而无需更改应用程序代码。

image.pngSockJS的一大好处在于提供了浏览器兼容性。
优先使用原生WebSocket,如果在不支持websocket的浏览器中,会自动降为轮询的方式。

Spring也对socketJS提供了支持。如果代码中添加了withSockJS(),服务器也会支持轮询方式。

  1. registry.addEndpoint("/coordination").withSockJS();

二、 STOMP

STOMP(Simple Text Oriented Messaging Protocol)

2.1 WebSocket 和 STOMP 关系?

WebSocket也是一种低级传输协议,WebSocket协议定义了两种消息类型(文本消息和二进制消息),但是其内容未定义。
与HTTP不同,它不对消息的内容规定任何语义。这意味着除非客户端和服务器就消息语义达成一致,否则就无法路由或处理消息。WebSocket客户端和服务器可以通过HTTP握手请求上的Sec-WebSocket-Protocol标头协商使用更高级别的消息协议(例如STOMP)。

就像HTTP在TCP套接字之上添加了请求-响应模型层一样,STOMP在WebSocket之上提供了一个基于帧的线路格式(frame-based wire format)层,用来定义消息的语义。

帧命令

  • SEND 发送
  • SUBSCRIBE 订阅
  • UNSUBSCRIBE 退订
  • BEGIN 开始
  • COMMIT 提交
  • ABORT 取消
  • ACK 确认
  • DISCONNECT 断开

header body

2.2 STOMP

使用STOMP的好处在于,它完全就是一种消息队列模式,你可以使用生产者与消费者的思想来认识它,发送消息的是生产者,接收消息的是消费者。而消费者可以通过订阅不同的destination,来获得不同的推送消息,不需要开发人员去管理这些订阅与推送目的地之前的关系

image.png
走app/url的消息会被你设置到的@MassageMapping拦截到,进行你自己定义的具体逻辑处理
走topic/url的消息就不会被拦截,直接到Simplebroker节点中将消息推送出去。其中simplebroker是spring的一种基于内存的消息队列,你也可以使用activeMQ,rabbitMQ代替。

/ topic / ..表示发布-订阅(一对多)
/ queue /表示点对点(一对一)消息

2.3 配置 WS Server

simple broker配置(支持心跳)

内置的简单消息代理处理来自客户端的订阅请求,将其存储在内存中,并将消息广播到具有匹配目标的已连接客户端。该代理支持类似路径的目标,包括对Ant样式目标模式的订阅。

  1. @Configuration
  2. //注解开启使用STOMP协议来传输基于代理(message broker)的消息
  3. @EnableWebSocketMessageBroker
  4. public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
  5. @Override
  6. public void registerStompEndpoints(StompEndpointRegistry registry) {
  7. registry
  8. // WebSocket握手的终结点的HTTP URL
  9. .addEndpoint("/portfolio")
  10. // 握手处理器
  11. .setHandshakeHandler(handshakeHandler())
  12. // 握手拦截器
  13. // 允许非同源
  14. .setAllowedOrigins("*")
  15. .withSockJS();;
  16. }
  17. @Bean
  18. public DefaultHandshakeHandler handshakeHandler() {
  19. WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
  20. policy.setInputBufferSize(8192);
  21. policy.setIdleTimeout(600000);
  22. return new DefaultHandshakeHandler(
  23. new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
  24. }
  25. @Override
  26. public void configureMessageBroker(MessageBrokerRegistry registry) {
  27. // 定义用于心跳检测的调度器
  28. ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
  29. taskScheduler.initialize();
  30. // 其目标标头以/ app开头的STOMP消息将路由到@Controller类中的@MessageMapping方法。
  31. registry.setApplicationDestinationPrefixes("/app");
  32. //使用内置的消息代理进行订阅和广播,并将目标标头以/ topic`或`/ que开头的消息路由到代理。
  33. registry.enableSimpleBroker("/queue/", "/topic/")
  34. //第一个数字表示服务器写入或发送心跳的频率。 第二个是客户应该写的频率
  35. .setHeartbeatValue(new long[] {10000, 20000})
  36. .setTaskScheduler(taskScheduler);
  37. //点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/
  38. registry.setUserDestinationPrefix("/user");
  39. }
  40. }

https://blog.csdn.net/liyongzhi1992/article/details/81221103

Server 向客户端发送

任何应用程序组件都可以将消息发送到brokerChannel。最简单的方法是注入SimpMessagingTemplate并使用它发送消息。通常,您将按类型注入它,如以下示例所示:

  1. @Controller
  2. public class GreetingController {
  3. private SimpMessagingTemplate template;
  4. @Autowired
  5. public GreetingController(SimpMessagingTemplate template) {
  6. this.template = template;
  7. }
  8. @RequestMapping(path="/greetings", method=POST)
  9. public void greet(String greeting) {
  10. String text = "[" + getTimestamp() + "]:" + greeting;
  11. this.template.convertAndSend("/topic/greetings", text);
  12. }
  13. }

广播与一对一通信接口

  1. @Controller
  2. public class WebSocketController {
  3. @Autowired
  4. private SimpMessagingTemplate template;
  5. //客户端主动发送消息到服务端,服务端马上回应指定的客户端消息
  6. //类似http无状态请求,但是有质的区别
  7. //websocket可以从服务器指定发送哪个客户端,而不像http只能响应请求端
  8. @MessageMapping("/massRequest")
  9. //SendTo 群发发送至 Broker 下的指定订阅路径
  10. @SendTo("/mass/getResponse")
  11. public ChatRoomResponse mass(ChatRoomRequest chatRoomRequest){
  12. //方法用于群发测试
  13. System.out.println("name = " + chatRoomRequest.getName());
  14. System.out.println("chatValue = " + chatRoomRequest.getChatValue());
  15. ChatRoomResponse response=new ChatRoomResponse();
  16. response.setName(chatRoomRequest.getName());
  17. response.setChatValue(chatRoomRequest.getChatValue());
  18. return response;
  19. }
  20. //单独聊天
  21. @MessageMapping("/aloneRequest")
  22. public ChatRoomResponse alone(ChatRoomRequest chatRoomRequest){
  23. //方法用于一对一测试
  24. System.out.println("userId = " + chatRoomRequest.getUserId());
  25. System.out.println("name = " + chatRoomRequest.getName());
  26. System.out.println("chatValue = " + chatRoomRequest.getChatValue());
  27. ChatRoomResponse response=new ChatRoomResponse();
  28. response.setName(chatRoomRequest.getName());
  29. response.setChatValue(chatRoomRequest.getChatValue());
  30. this.template.convertAndSendToUser(chatRoomRequest.getUserId()+"","/alone/getResponse",response);
  31. return response;
  32. }
  33. }

认证方式

1. 握手处理

每个通过WebSocket进行的STOMP消息传递会话均以HTTP请求开头。这可以是升级到WebSockets的请求(即WebSocket握手)
在SockJS后备情况下,是一系列SockJS HTTP传输请求。

因此,对于WebSocket握手或SockJS HTTP传输请求,通常已经有一个可以通过HttpServletRequest#getUserPrincipal()访问的经过身份验证的用户。Spring会自动将该用户与为其创建的WebSocket或SockJS会话相关联,随后将其与通过用户标头在该会话上传输的所有STOMP消息相关联。
握手时获取用户信息并保存 session-代码

2. 握手拦截器

拦截 http 请求,获取 header 中 token

3. 整合 SpringSecurity JWT OAuth

我不会, 略

消息顺序问题

来自代理的消息被发布到clientOutboundChannel,从那里被写入WebSocket会话。由于该通道由ThreadPoolExecutor支持,因此消息将在不同的线程中处理,并且客户端接收到的结果序列可能与发布的确切顺序不匹配。
当然,这也是可以配置解决的

  1. @Configuration
  2. @EnableWebSocketMessageBroker
  3. public class MyConfig implements WebSocketMessageBrokerConfigurer {
  4. @Override
  5. protected void configureMessageBroker(MessageBrokerRegistry registry) {
  6. // ...
  7. registry.setPreservePublishOrder(true);
  8. }
  9. }

事件

通过实施Spring的ApplicationListener接口,可以发布并接收多个ApplicationContext事件:

BrokerAvailabilityEvent:指示代理何时变为可用或不可用。当“简单”代理在启动时立即可用并保持运行状态时,STOMP“代理中继”可能会失去与全功能代理的连接(例如,如果代理重新启动)。代理中继具有重新连接逻辑,并在代理返回时重新建立与代理的“系统”连接。因此,只要状态从连接变为断开,反之亦然,就会发布此事件。使用SimpMessagingTemplate的组件应该订阅此事件,并避免在代理不可用时避免发送消息。无论如何,他们应该准备在发送消息时处理MessageDeliveryException。

SessionConnectEvent:在收到新的STOMP CONNECT以指示新的客户端会话开始时发布。该事件包含代表连接的消息,包括会话ID,用户信息(如果有)和客户端发送的所有自定义标头。这对于跟踪客户端会话很有用。预订此事件的组件可以使用SimpMessageHeaderAccessor或StompMessageHeaderAccessor包装包含的消息。

SessionConnectedEvent:在SessionConnectEvent之后不久,当代理已发送STOMP CONNECTED帧以响应CONNECT时,发布该消息。此时,可以认为STOMP会话已完全建立。

SessionSubscribeEvent:在收到新的STOMP SUBSCRIBE时发布。
SessionUnsubscribeEvent:在收到新的STOMP UNSUBSCRIBE时发布。
SessionDisconnectEvent:在STOMP会话结束时发布。DISCONNECT可能已经从客户端发送,或者它可能在WebSocket会话关闭时自动生成。在某些情况下,每个会话多次发布此事件。关于多个断开事件,组件应该是幂等的。

事件使用

public class SessionConnectEventListener implements ApplicationListener {

拦截器

事件为STOMP连接的生命周期提供通知,但不是为每条客户端消息提供通知。
应用程序还可以注册ChannelInterceptor来拦截处理链中任何消息以及任何部分。

STOMP 客户端

服务和服务间建立 ws 连接

2.4 错误码

https://www.cnblogs.com/gxp69/archive/2019/10/25/11736749.html