在給導師做項目的時候,APP端需要通過後端服務推送消息,該消息與userId綁定。
(1)服務端實現
實現的話首先說下服務端,基於微服務,將該模塊設計成單獨模塊,部署端口8100。
首先引入maven依賴(根據自己的需求,只要包含websocket即可)
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.edu.bjfu</groupId>
<artifactId>fdcp</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>fdcp-provider-websocket</artifactId>
<dependencies>
<dependency><!-- 引入自己定義的api通用包 -->
<groupId>cn.edu.bjfu</groupId>
<artifactId>fdcp-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<!--整合redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 修改後立即生效,熱部署 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>springloaded</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- 將微服務provider側註冊進eureka -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<!-- SpringBoot健康監控 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
實現原理如下:
(1)先配置一個ws的配置類WebSocketConfig
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/fdcp").setAllowedOrigins("*").withSockJS();;
}
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
// TODO Auto-generated method stub
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
// TODO Auto-generated method stub
}
@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
// TODO Auto-generated method stub
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
// TODO Auto-generated method stub
}
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
// TODO Auto-generated method stub
}
@Override
public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
// TODO Auto-generated method stub
return false;
}
}
(2)連接ws監聽類STOMPConnectEventListener,需實現監聽接口:ApplicationListener,開啓ws連接。客戶端將userId傳入進來,服務端將該userId保存到map中,key爲userId,value爲sessionId。該map是一個全局變量,保存在springboot啓動類WebSocketApp中。
【注】此時可確定客戶端連接的地址爲:http://localhost:8100/fdcp
@Component
public class STOMPConnectEventListener implements ApplicationListener<SessionConnectEvent> {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Override
public void onApplicationEvent(SessionConnectEvent event) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
String userId = sha.getNativeHeader("userId").get(0);
String sessionId = sha.getSessionId();
//System.out.println("STOMPConnectEventListener........"+userId+"-"+sessionId);
WebSocketApp.userMap.put(userId, sessionId);
}
}
啓動類:
@EnableScheduling//開啓定時任務
@EnableAsync//開啓異步
@SpringBootApplication
@EnableEurekaClient
public class WebSocketApp {
public static Map<String, String> userMap = new ConcurrentHashMap<>();//userId:sessionId
public static void main(String[] args) {
SpringApplication.run(WebSocketApp.class, args);
}
}
(3)開啓定時任務:每15min監聽一次消息,該消息的是通過客戶端傳入的userId獲取redis中的數據,當獲取完之後清空該redis的key。(2)中提到,userId保存在一個全局map中,可以獲取到。獲取的value是一個存儲的json字符串,格式大致是這樣:
{
"userId": "a56sd1a6s51dxzcs5",
"islook": 0,
"message": "推送消息內容",
"createtime": "2020-05-15 19:57:29",
"messageId": "de91d3f80d0841219caeb9298f3fc09a",
"title": "消息標題"
}
定時任務類AppPushTask:
@Component
public class AppPushTask {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
private static final String appPushPrefix = "APP_PUSH_MESSAGE";//消息推送app中redis的前綴
@Scheduled(fixedRate = 1000*60*15)//15分鐘執行一次
public void callback() throws Exception {
Map<String, String> userMap = WebSocketApp.userMap;
for(Entry<String, String> entry : userMap.entrySet()) {
String userId = entry.getKey();
String key = appPushPrefix+":"+userId;
String message = stringRedisTemplate.opsForValue().get(key);
if(message != null) {
messagingTemplate.convertAndSend("/topic/callback", message);
stringRedisTemplate.delete(key);
}else {
//不執行任何操作
}
}
}
}
服務端的大致代碼結構如下:
(2)客戶端
首先需要安裝sockJS和StompJS
cnpm install sockjs-client --save
cnpm install stompjs --save
cnpm install net --save
直接貼代碼:
<template>
<div>
<button @click="connect">連接ws</button><br/>
消息標題:<span v-text="title"></span><br/>
消息內容:<span v-html="message"></span>
</div>
</template>
<script>
import SockJS from "sockjs-client";
import Stomp from "stompjs";
export default {
data() {
return {
messageId: "", //推送消息的id
userId: "1", //當前該設備用戶id(消息推送接收者)
title: "", //消息標題
message: "", //消息
stompClient: null//stomp
};
},
methods:{
connect(){
let socket=new SockJS('http://localhost:8100/fdcp')
this.stompClient = Stomp.over(socket)
this.stompClient.connect({"userId": this.userId }, this.onConnected)
},
onConnected(frame) {
this.stompClient.subscribe('/topic/callback', this.callback)
},
callback(msg){
let body= JSON.parse(msg.body)
//console.log(body)
this.message = body.message;
this.title = body.title;
},
}
};
</script>
最終實現的效果如下,點擊“連接ws”之後會與服務端建立連接,之後服務端向客戶端推送數據。