一、 什么是 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
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
创建WebSocket服务器就像实现WebSocketHandler一样简单,扩展TextWebSocketHandler或BinaryWebSocketHandler。
以下示例使用TextWebSocketHandler:
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;
public class MyHandler extends TextWebSocketHandler {
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
// 处理简单消息
}
}
1.4 SockJS
SockJS是一个浏览器JavaScript库
SockJS的目标是让应用程序使用WebSocket API,但在运行时必要时使用非WebSocket替代方案,而无需更改应用程序代码。
SockJS的一大好处在于提供了浏览器兼容性。
优先使用原生WebSocket,如果在不支持websocket的浏览器中,会自动降为轮询的方式。
Spring也对socketJS提供了支持。如果代码中添加了withSockJS(),服务器也会支持轮询方式。
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,来获得不同的推送消息,不需要开发人员去管理这些订阅与推送目的地之前的关系
走app/url的消息会被你设置到的@MassageMapping拦截到,进行你自己定义的具体逻辑处理
走topic/url的消息就不会被拦截,直接到Simplebroker节点中将消息推送出去。其中simplebroker是spring的一种基于内存的消息队列,你也可以使用activeMQ,rabbitMQ代替。
/ topic / ..表示发布-订阅(一对多)
/ queue /表示点对点(一对一)消息
2.3 配置 WS Server
simple broker配置(支持心跳)
内置的简单消息代理处理来自客户端的订阅请求,将其存储在内存中,并将消息广播到具有匹配目标的已连接客户端。该代理支持类似路径的目标,包括对Ant样式目标模式的订阅。
@Configuration
//注解开启使用STOMP协议来传输基于代理(message broker)的消息
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
// WebSocket握手的终结点的HTTP URL
.addEndpoint("/portfolio")
// 握手处理器
.setHandshakeHandler(handshakeHandler())
// 握手拦截器
// 允许非同源
.setAllowedOrigins("*")
.withSockJS();;
}
@Bean
public DefaultHandshakeHandler handshakeHandler() {
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000);
return new DefaultHandshakeHandler(
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 定义用于心跳检测的调度器
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.initialize();
// 其目标标头以/ app开头的STOMP消息将路由到@Controller类中的@MessageMapping方法。
registry.setApplicationDestinationPrefixes("/app");
//使用内置的消息代理进行订阅和广播,并将目标标头以/ topic`或`/ que开头的消息路由到代理。
registry.enableSimpleBroker("/queue/", "/topic/")
//第一个数字表示服务器写入或发送心跳的频率。 第二个是客户应该写的频率
.setHeartbeatValue(new long[] {10000, 20000})
.setTaskScheduler(taskScheduler);
//点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/
registry.setUserDestinationPrefix("/user");
}
}
https://blog.csdn.net/liyongzhi1992/article/details/81221103
Server 向客户端发送
任何应用程序组件都可以将消息发送到brokerChannel。最简单的方法是注入SimpMessagingTemplate并使用它发送消息。通常,您将按类型注入它,如以下示例所示:
@Controller
public class GreetingController {
private SimpMessagingTemplate template;
@Autowired
public GreetingController(SimpMessagingTemplate template) {
this.template = template;
}
@RequestMapping(path="/greetings", method=POST)
public void greet(String greeting) {
String text = "[" + getTimestamp() + "]:" + greeting;
this.template.convertAndSend("/topic/greetings", text);
}
}
广播与一对一通信接口
@Controller
public class WebSocketController {
@Autowired
private SimpMessagingTemplate template;
//客户端主动发送消息到服务端,服务端马上回应指定的客户端消息
//类似http无状态请求,但是有质的区别
//websocket可以从服务器指定发送哪个客户端,而不像http只能响应请求端
@MessageMapping("/massRequest")
//SendTo 群发发送至 Broker 下的指定订阅路径
@SendTo("/mass/getResponse")
public ChatRoomResponse mass(ChatRoomRequest chatRoomRequest){
//方法用于群发测试
System.out.println("name = " + chatRoomRequest.getName());
System.out.println("chatValue = " + chatRoomRequest.getChatValue());
ChatRoomResponse response=new ChatRoomResponse();
response.setName(chatRoomRequest.getName());
response.setChatValue(chatRoomRequest.getChatValue());
return response;
}
//单独聊天
@MessageMapping("/aloneRequest")
public ChatRoomResponse alone(ChatRoomRequest chatRoomRequest){
//方法用于一对一测试
System.out.println("userId = " + chatRoomRequest.getUserId());
System.out.println("name = " + chatRoomRequest.getName());
System.out.println("chatValue = " + chatRoomRequest.getChatValue());
ChatRoomResponse response=new ChatRoomResponse();
response.setName(chatRoomRequest.getName());
response.setChatValue(chatRoomRequest.getChatValue());
this.template.convertAndSendToUser(chatRoomRequest.getUserId()+"","/alone/getResponse",response);
return response;
}
}
认证方式
1. 握手处理
每个通过WebSocket进行的STOMP消息传递会话均以HTTP请求开头。这可以是升级到WebSockets的请求(即WebSocket握手)
在SockJS后备情况下,是一系列SockJS HTTP传输请求。
因此,对于WebSocket握手或SockJS HTTP传输请求,通常已经有一个可以通过HttpServletRequest#getUserPrincipal()访问的经过身份验证的用户。Spring会自动将该用户与为其创建的WebSocket或SockJS会话相关联,随后将其与通过用户标头在该会话上传输的所有STOMP消息相关联。
握手时获取用户信息并保存 session-代码
2. 握手拦截器
3. 整合 SpringSecurity JWT OAuth
消息顺序问题
来自代理的消息被发布到clientOutboundChannel,从那里被写入WebSocket会话。由于该通道由ThreadPoolExecutor支持,因此消息将在不同的线程中处理,并且客户端接收到的结果序列可能与发布的确切顺序不匹配。
当然,这也是可以配置解决的
@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
@Override
protected void configureMessageBroker(MessageBrokerRegistry registry) {
// ...
registry.setPreservePublishOrder(true);
}
}
事件
通过实施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 客户端
2.4 错误码
https://www.cnblogs.com/gxp69/archive/2019/10/25/11736749.html