一.SSE
实现消息推送的方式有很多:轮询、长轮询、SSE、WebSocket等等,上篇文章已经介绍了WebSocket,但在有些时候并不需要客户端利用WebSocket发送数据,即客户端通过HTTP协议握手后,只负责接收服务端数据。此时,使用SSE不需引入其他依赖,有效降低开发成本。
SSE基于HTTP协议,一般意义上的 HTTP 协议是无法做到服务端主动向客户端推送消息的,但SSE变换了一种思路,就是服务器向客户端声明,发送的是流信息,本质上,这种通信就是以流信息的方式。SSE在服务器和客户端之间打开一个单向通道,服务端响应的不再是一次性的数据包而是text/event-stream
类型的数据流信息,在有数据变更时从服务器流式传输到客户端。
整体的实现思路有点类似于在线视频播放,视频流会连续不断的推送到浏览器,你也可以理解成,客户端在完成一次用时很长(网络不畅)的下载。
二.SSE+Redis实时消息推送
需求分析:
后台实时获取Redis里写入的数据,前端实时展示。我这里应用场景是MQTT服务推送消息,经过计算后将消息存入Redis,后端将消息实时推送前端页面进行展示。
代码:SpringBoot+SSE+Redisson
1.SseEmitter
@RestController
@RequestMapping("/sse")
public class SseController {
private static final Logger logger = LogManager.getLogger(SseController.class);
private static final Map<String, SseEmitter> sseCache = new ConcurrentHashMap<>();
public static void remove(String key) {
sseCache.remove(key);
logger.info("移除连接:{}", key);
}
private static Runnable completionCallBack(String key) {
return () -> {
logger.info("结束连接:{}", key);
remove(key);
};
}
private static Runnable timeoutCallBack(String key) {
return () -> {
logger.info("结束连接:{}", key);
remove(key);
};
}
private static Consumer<Throwable> errorCallBack(String key) {
return throwable -> {
logger.info("连接异常:{}", key);
remove(key);
};
}
@GetMapping(path = "/subscribe", produces = TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribe(String id) {
if (sseCache.get(id) != null) {
return sseCache.get(id);
}
// 超时时间设置为1小时
SseEmitter sseEmitter = new SseEmitter(3_600_000L);
sseCache.put(id, sseEmitter);
sseEmitter.onTimeout(timeoutCallBack(id));
sseEmitter.onCompletion(completionCallBack(id));
sseEmitter.onError(errorCallBack(id));
return sseEmitter;
}
public void sendMessage(List<RMap<Object, Object>> message) {
for (SseEmitter sseEmitter : sseCache.values()) {
try {
sseEmitter.send(message);
} catch (IOException e) {
logger.error("sse send error.", e);
}
}
}
}
2.Redis配置
1)导入Redisson依赖,也可使用Jedis实现
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.1</version>
</dependency>
2)配置application.yml文件
spring:
redis:
host: 127.0.0.1
port: 6379
database: 3
password: 123456
lettuce:
pool:
max-active: 8
max-idle: 8
max-wait: -1ms
min-idle: 0
shutdown-timeout: 100ms
# 消息订阅主题
subscribe: sse
3)RedissonConfig,配置Redis的连接
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;
@Value("${spring.redis.password}")
private String password;
@Bean
RedissonClient redisson() {
Config config = new Config();
config.setCodec(new StringCodec())
.useSingleServer()
.setAddress("redis://" + host + ":" + port);
if (!StringUtils.isEmpty(password)) {
config.useSingleServer().setPassword(password);
}
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
4)RedisConfig监听Redis的发布订阅消息
@Configuration
public class RedisConfig {
@Value(value = "${spring.redis.subscribe}")
private String subscribe;
@Autowired
private RedissonClient redissonClient;
@Autowired
private SseController sseController;
@Bean
public RedisMessageListenerContainer listenerContainer(RedisConnectionFactory factory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
//监听设备MQTT频道并发送SSE消息
container.addMessageListener((a, b) -> {
String msg = new String(a.getBody());
//根据订阅消息查询Hash
List<RMap<Object, Object>> list = new ArrayList<>();
list.add(redissonClient.getMap(msg));
sseController.sendMessage(list);
}, new PatternTopic(subscribe));
return container;
}
}
三.postman测试
1.新建Http请求
2.创建一个Hash作为模拟存入的消息,然后使用Redis的发布。实际项目中,消息存入或变更时,自动触发发布命令。
3.此时postman持续接收消息