# spring-cloud-websocket-mq
**Repository Path**: rasonyang/spring-cloud-websocket-mq
## Basic Information
- **Project Name**: spring-cloud-websocket-mq
- **Description**: spring-cloud-websocket + rabbitmq
source from: https://github.com/lijian0706/spring-cloud-websocket.git
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 4
- **Created**: 2020-11-26
- **Last Updated**: 2022-05-26
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# spring-cloud-websocket-mq
#### 介绍
spring-cloud-websocket + rabbitmq
source from: https://github.com/lijian0706/spring-cloud-websocket.git
### 背景介绍
- 前段时间项目中有服务端推送的业务场景,首先映入脑海的便是`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/**
```
- 限于文章篇幅,只贴出核心的`js`代码
```
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的配置
```
stomp.server: localhost
stomp.port: 61613
stomp.username: rabbitmq
stomp.password: rabbitmq
```
- WebSocket配置类
```
@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,向该用户推送
}
};
}
}
```
- 测试WebSocket,简单的接收和发送
```
@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);
}
```
- 测试过程
- 启动服务发现`discovery-server`、`gateway`、业务微服务`web-socket`启动两份实例,端口号分别是8080、8081(`java -jar web-socket.jar --server.port=8081`)
- 打开页面`http://localhost:7000/110000.html`,点击Connect按钮,打开浏览器控制台,可看到连接成功的信息,点击Send按钮,可向服务端发送消息,服务端控制台我们会看到,只有其中一个实例接收到消息。
```
2019-05-28 22:02:37.233 INFO 8192 --- [boundChannel-55] c.lijian.websocket.WebSocketApplication : message: ssss
```
- 通过PostMan调用测试接口分别向8080,8081端口发送请求,测试多实例下的服务端推送,发现无论向哪个实例发送,页面都能接收到消息。
### 最后
- 到此处为止,便实现了在多个实例下,向WebSocket发送消息的功能。
- 源码地址:https://github.com/lijian0706/spring-cloud-websocket
- 未经允许,请勿转载
=======