背景介绍
- 前段时间项目中有服务端推送的业务场景,首先映入脑海的便是
WebSocket
和SSE
,相较之下,我们决定使用WebSocket
,但经过仔细分析与尝试以后,发现如下难点亟需攻克,否则方案就得搁浅。
zuul 1.0
对WebSocket
支持并不理想,连接一段时间后就会自动掉线:Whoops! Lost connection to http://localhost:7000/web-socket/ws
- 由于上线后业务微服务会有多个无状态实例,而
WebSocket
是长连接且有状态的,它只会与其中一个实例保持连接,其他实例无法获得它的连接,因此无法主动向其推送消息。
使用Spring Cloud Gateway
代替Spring Cloud zuul
- 对于第一个难点,我们查阅官方文档发现,
zuul
从2.0开始才会支持WebSocket
,而Spring Cloud
似乎并不打算集成zuul 2.0
,而是推出了自己的反向代理(智能路由)Spring Cloud Gateway
,因此我们便决定从Spring Cloud zuul
切换到Spring Cloud Gateway
上,对于Eureka
相关的内容不是本篇文章的重点,请参考源码。
- 添加
Spring Cloud Gateway
依赖,此处没有贴出jquery
、bootstrap
相关依赖,请参考源码
org.springframework.cloud
spring-cloud-starter-gateway
spring.application.name: gateway
server:
port: 7000
compression:
enabled: true
mime-types: text/html,text/css,application/javascript,application/json
#spring.resources.static-locations: classpath:/resources/static
spring:
cloud:
gateway:
routes:
- id: web-socket
uri: lb://web-socket
predicates:
- Path=/web-socket/**
filters:
- RewritePath=/web-socket/(?.*), /$\{path}
# SockJS route
- id: websocket_sockjs_route
uri: lb://web-socket
predicates:
- Path=/web-socket/info/**
# Normwal Websocket route
- id: websocket_route
uri: lb:ws://web-socket
predicates:
- Path=/web-socket/**
function connect() {
// var socket = new SockJS('/web-socket/ws?access_token=xxxxx'); // 支持oauth授权
var socket = new SockJS('/web-socket/ws');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/user/topic/greetings', function (greeting) {
showGreeting(greeting.body);
});
});
}
多实例下的WebSocket
配置
- 本项目中有用到消息中间件,最开始的解决方案是:负载到哪个实例,那么由他通过中间件向其他实例广播,通知他们向WebSocket写入数据,所有实例监听到消息,一起向WebSocket发送消息,其实WebSocket只会与其中一个实例建立连接,也就是说,只有其中一个实例写成功,这样便达到了在多实例下,服务端推送的效果。此方案可行,却并不是最优解决方案,最优解决方案是:RabbitMQ+STOMP,下面会进行详细介绍。
RabbitMQ的安装
- 使用docker compose 安装RabbitMQ
version: '3'
services:
rabbitmq:
image: "rabbitmq:3-management"
hostname: "rabbit"
environment:
RABBITMQ_DEFAULT_USER: "rabbitmq"
RABBITMQ_DEFAULT_PASS: "rabbitmq"
ports:
- "15672:15672"
- "5672:5672"
- "61613:61613"
labels:
NAME: "rabbitmq"
volumes:
- ./rabbitmq-isolated.conf:/etc/rabbitmq/rabbitmq.config
- 开启
STOMP
- 登录容器:
docker exec -it containerId bash
- 开启rabbitmq的stomp端口:
rabbitmq-plugins enable rabbitmq_stomp
业务微服务web-socket的关键代码
stomp.server: localhost
stomp.port: 61613
stomp.username: rabbitmq
stomp.password: rabbitmq
@Configuration
@EnableWebSocketMessageBroker
@EnableConfigurationProperties(StompProperties.class)
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Autowired
private StompProperties stompProperties;
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {//配置消息代理 Message Broker 点对点式
registry.setApplicationDestinationPrefixes("/app") // 配置请求都以/app打头,没有特殊意义,例如:@MessageMapping("/hello"),其实真实路径是/app/hello
.enableStompBrokerRelay("/topic", "/queue")
.setRelayHost(stompProperties.getServer())
.setRelayPort(stompProperties.getPort())
.setClientLogin(stompProperties.getUsername())
.setClientPasscode(stompProperties.getPassword())
.setSystemLogin(stompProperties.getUsername())
.setSystemPasscode(stompProperties.getPassword())
.setVirtualHost("/")
.setUserDestinationBroadcast("/topic/greetings")
.setUserRegistryBroadcast("/topic/greetings");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setAllowedOrigins("*").setHandshakeHandler(defaultHandshakeHandler()).withSockJS();//注册STOMP协议的节点 指定使用SockJS协议,setAllowedOrigins 添加允许跨域访问
}
private DefaultHandshakeHandler defaultHandshakeHandler() {
return new DefaultHandshakeHandler() {
@Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map attributes) {
return () -> "lijian"; // 模拟用户 lijian,向该用户推送
}
};
}
}
@MessageMapping("/hello")
@SendToUser("/topic/greetings")
public String greeting(String message) {
log.info("message: {}", message);
return "hello";
}
@Autowired
private SimpMessagingTemplate messagingTemplate;
@GetMapping("/test")
public void test(String username){
messagingTemplate.convertAndSendToUser(username, "/topic/greetings", "hello:"+username);
}
最后
- 到此处为止,便实现了在多个实例下,向WebSocket发送消息的功能。
- 源码地址:https://github.com/lijian0706/spring-cloud-websocket
- 未经允许,请勿转载