<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.6.RELEASEversion>
parent>
<groupId>com.examplegroupId>
<artifactId>websocketartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>urlWebsocketname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.47version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
server:
port: 7001
servlet:
session.timeout: 300
logging:
level:
#日志优先级(只会输出指定优先级及以上的日志信息):trace
org.springframework.web: debug
cn.zifangsky: debug
# file: logs/stomp-websocket.log
#Thymeleaf
thymeleaf:
mode: LEGACYHTML5
prefix: classpath:/templates/
suffix: .html
template-resolver-order: 0
cache: false
auth:
aes:
key: **************
#redis连接
spring.redis.host= 192.168.0.9
1.WebSocketConfig.java
配置
@Slf4j
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Autowired
private MyChannelInterceptor myChannelInterceptor;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp-websocket")
.setAllowedOrigins("*")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//客户端需要把消息发送到/message/xxx地址
registry.setApplicationDestinationPrefixes("/message");
//服务端广播消息的路径前缀,客户端需要相应订阅/topic/xxx这个地址的消息
registry.enableSimpleBroker(Constant.BROKER_DESTINATION_USER_PREFIX,Constant.BROKER_DESTINATION_PREFIX);
//给指定用户发送消息的路径前缀,默认值是/user/
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
//配置连接用户关系保存
registration.interceptors(myChannelInterceptor);
}
}
2、MyChannelInterceptor.java
拦截保存获取token中的userId会员id设置到socket中
@Component
@Slf4j
public class MyChannelInterceptor implements ChannelInterceptor {
@Value("${auth.aes.key}")
private String aesKey;
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
Object raw = message.getHeaders().get(SimpMessageHeaderAccessor.NATIVE_HEADERS);
if (raw instanceof Map) {
Object token = ((Map) raw).get("token");
if(token != null){
// 设置当前访问的认证用户
String tokenString = ((LinkedList)token).get(0).toString();
String tokenValue = AESUtils.decrypt(tokenString, aesKey);
String[] tokenValues = tokenValue.split("##");
String userId = tokenValues[1];//todo 小程序登录时取0
CustomPrincipal customPrincipal = new CustomPrincipal(userId);
accessor.setUser(customPrincipal);
}
}
}
return message;
}
@Override
public boolean preReceive(MessageChannel channel){
return true;
}
@Override
public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, Exception ex) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
StompCommand command = accessor.getCommand();
//用户已经断开连接
if(StompCommand.DISCONNECT.equals(command)){
String user = "";
Principal principal = accessor.getUser();
if(principal != null && !StringUtils.isEmpty(principal.getName())){
user = principal.getName();
}else{
user = accessor.getSessionId();
}
log.info(MessageFormat.format("用户{0}的WebSocket连接已经断开", user));
}
}
}
3.CustomPrincipal.java
,socket用户凭证
public class CustomPrincipal implements Principal {
private String userId;
public CustomPrincipal(String userId) {
this.userId = userId;
}
@Override
public String getName() {
return userId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
}
4.RedisConfig.java
配置redis监听
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
@Configuration
public class RedisConfig {
@Autowired
private LettuceConnectionFactory lettuceConnectionFactory;
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer() {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(lettuceConnectionFactory);
return container;
}
}
5.RedisChannelListener.java
实现redis订阅自定义的channel,用户接受代码中向通道发送的信息,接受到后根据消息体的不同发送到广播,或者指定用户
@Slf4j
@Component
public class RedisChannelListener implements MessageListener {
@Autowired
SimpMessagingTemplate simpMessagingTemplate;
@Override
public void onMessage(Messachange message, byte[] pattern) {
String channel = new String(pattern);
log.info("channel:" + channel + "receives message :" + message.getBody());
if (!StringUtils.isEmpty(message) && Constant.STOMP_MESSAGE_CHANNEL.equals(channel)) {
try {
StompMessage msg = JSON.parseObject(message.getBody(), StompMessage.class);
//如果消息包含会员id表示单独发送给小程序会员,小程序会员响应广播,否则发送到指定用户
if(msg.getMemberId() == null){
String destination = Constant.BROKER_DESTINATION_PREFIX + "/" + msg.getMarketId();
simpMessagingTemplate.convertAndSend(destination, JSON.toJSON(msg));
}else{
String destination = Constant.BROKER_DESTINATION_USER_PREFIX + "/" + msg.getMarketId();
simpMessagingTemplate.convertAndSendToUser(msg.getMemberId().toString(),destination,JSON.toJSON(msg));
}
} catch (Exception e) {
log.info("onMessage error:{}", e.getMessage());
}
}
}
}
6.MyApplicationRunner.java
项目启动则订阅redis通道
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Autowired
private SubscribeListener subscribeListener;
@Autowired
RedisMessageListenerContainer redisMessageListenerContainer;
@Override
public void run(ApplicationArguments args) throws Exception {
this.subWebsocketChannel();
}
/**
* 订阅redis频道
*/
private void subWebsocketChannel(){
redisMessageListenerContainer.addMessageListener(subscribeListener,new ChannelTopic(webSocketChannel));
}
}
7.PublishService
将消息发布到通道
/**
* 通道发布
*/
@Component
public class PublishService {
@Autowired
StringRedisTemplate redisTemplate;
/**
* @param channel 消息发布订阅 主题
* @param message 消息信息
*/
public void publish(String channel, Object message) {
redisTemplate.convertAndSend(channel, message);
}
}
8.根据实际业务处理后需要向socket推送消息,则直接推送到redis通道,由redis订阅后再根据消息向订阅路径推送
@Autowired
private PublishService publishServerice;
//推送到广播
@GetMapping("/publish/topic")
public void toTopic(long marketId){
StompMessage stompMessage = new StompMessage();
stompMessage.setMarketId(marketId);
stompMessage.setMessageType(2);
stompMessage.setData("from publish topic");
publishServerice.publish(Constant.STOMP_MESSAGE_CHANNEL, JSON.toJSONString(stompMessage));
}
//推送到指定用户
@GetMapping("/publish/user")
public void toUser(long marketId,long memberId){
StompMessage stompMessage = new StompMessage();
stompMessage.setMarketId(marketId);
stompMessage.setMemberId(memberId);
stompMessage.setMessageType(1);
stompMessage.setData("from publish user *");
publishServerice.publish(Constant.STOMP_MESSAGE_CHANNEL, JSON.toJSONString(stompMessage));
}
@MessageMapping("/send/to/{marketId}")
public void sendToUser(Principal principal,@DestinationVariable Long marketId){
//从principal.getName()可以获取连接时设置到socket中的userId,参数中获取marketId则可以处理对应业务,比如摇一摇计数 simpMessagingTemplate.convertAndSendToUser(principal.getName(),"/queue","from to user");
}
9.StompMessage.java
publish到redis再推送到socket订阅路径的消息体,订阅者根据messageType处理相应业务
@Data
public class StompMessage {
/**
* 活动id
*/
private Long marketId;
/**
* 推送消息类型
*/
private Integer messageType;
/**
* 用户会员id
*/
private Long memberId;
/**
* 消息内容
*/
private Object data;
}
10.前端socket连接、订阅和发送部分代码
var stompClient = null;
//连接
function connect() {
var target = $("#target").val();
//target = http://localhost:7001/stomp-websocket
var ws = new SockJS(target);
stompClient = Stomp.over(ws);
//建立连接是将token作为header中参数,用于后端获取userId保存关系指定用户发送消息 stompClient.connect({"token":"40A070633494FFEF9050390AEF5C51761E067B8F4C9C446954785E25ED687B8D882C5F33A63DCBAA5605FA6431E60CBB"}, function () {
setConnected(true);
log('Info: STOMP connection opened.');
//订阅广播 /topic/marketId ,根据接收消息类型相应处理
stompClient.subscribe("/topic/1", function (greeting) {
log('Received topic: ' + greeting.body);
});
//订阅指定用户发送消息 /user/queue/marketId ,接收后端对这个用户的指定发送,这里默认/user为前缀,会接收到后端根据保存在socket中的Principal中的userName的消息
stompClient.subscribe("/user/queue/1", function (greeting) {
log('Received to user: ' + greeting.body);
});
},function () {
//断开处理
setConnected(false);
log('Info: STOMP connection closed.');
});
}
//断开连接
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
stompClient = null;
}
setConnected(false);
log('Info: STOMP connection closed.');
}
//向服务端发送姓名
function sendName() {
if (stompClient != null) {
var username = $("#username").val();
var mapping = $("#mapping").val();
log('Sent: ' + username);
stompClient.send("/message/"+mapping, {}, JSON.stringify({'name': username}));
} else {
alert('STOMP connection not established, please connect.');
}
}
//日志输出
function log(message) {
console.log(message);
}