目录
一、引入jar
二、yml配置
三、配置类
websocket配置
redis序列化配置
redis stream配置-绑定消费者监听类
四、写监听类
五、WebSocket接口类编写
六、生产者生产消息到redis的stream中
七、测试
八、使用webSocket实现对数据的实时推送详解
1.什么是webSocket?
2.实时推送数据的实现方式以及应用场景
实现方式
九、封装工具类
十、补充
一、引入jar
<!--WebSocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
二、yml配置
server:
port: 9085
spring:
profiles:
active: dev
application:
name: websocket
redis:
host: 127.0.0.1
port: 6379
timeout: 3000ms
password: 123456
database: 1
ws:
listen:
sever:
name: websocket
三、配置类
websocket配置
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter
* 这个bean会自动注册 使用了@ServerEndpoint注解声明的websocket endpoint
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
public class ServerEncoder implements Encoder.Text<ValueHolder> {
@Override
public void destroy() {
// TODO Auto-generated method stub
// 这里不重要
}
@Override
public void init(EndpointConfig arg0) {
// TODO Auto-generated method stub
// 这里也不重要
}
/*
* encode()方法里的参数和Text<T>里的T一致,如果你是Student,这里就是encode(Student student)
*/
@Override
public String encode(Result类 responseMessage) throws EncodeException {
try {
/*
* 这里是重点,只需要返回Object序列化后的json字符串就行
* 你也可以使用gosn,fastJson来序列化。
*/
return JSONObject.toJSONString(responseMessage, SerializerFeature.WriteDateUseDateFormat);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
redis序列化配置
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@EnableCaching
@Configuration
public class RedisSerializerConfig {
/**
* 设置redis序列化规则
*/
@Bean
public Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer(){
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(om);
return jackson2JsonRedisSerializer;
}
/**
* RedisTemplate配置
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory,
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer) {
// 配置redisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
RedisSerializer<?> stringSerializer = new StringRedisSerializer();
// key序列化
redisTemplate.setKeySerializer(stringSerializer);
// value序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// Hash key序列化
redisTemplate.setHashKeySerializer(stringSerializer);
// Hash value序列化
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
redis stream配置-绑定消费者监听类
import lombok.extern.slf4j.Slf4j;
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.RedisConnectionFactory;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
import java.time.Duration;
import java.util.Collections;
@Slf4j
@Configuration
public class RedisStreamConfig {
@Autowired
private Listener Consummer;
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Bean
public StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String,String,String>> workOrderListenerOptions(){
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
return StreamMessageListenerContainer.StreamMessageListenerContainerOptions
.builder()
//block读取超时时间
.pollTimeout(Duration.ofSeconds(3))
//count 数量(一次只获取一条消息)
.batchSize(1)
//序列化规则
.serializer( stringRedisSerializer )
.build();
}
/**
* 开启监听器接收消息 注意此方法的结尾是Container
*/
@Bean
public StreamMessageListenerContainer<String,MapRecord<String,String,String>> ListenerContainer(RedisConnectionFactory factory,
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String,String,String>> streamMessageListenerContainerOptions){
StreamMessageListenerContainer<String,MapRecord<String,String,String>> listenerContainer = StreamMessageListenerContainer.create(factory,
streamMessageListenerContainerOptions);
//如果 流不存在 创建 stream 流
if( !redisTemplate.hasKey(stringKey){
redisTemplate.opsForStream().add(streamKey, Collections.singletonMap("", ""));
log.info("初始化stream {} success", streamKey);
}
//创建消费者组
try {
redisTemplate.opsForStream().createGroup(streamKey, groupName);
} catch (Exception e) {
log.info("消费者组 {} 已存在", groupName);
}
//注册消费者 消费者名称,从哪条消息开始消费,消费者类
// > 表示没消费过的消息
// $ 表示最新的消息
listenerContainer.receive(
Consumer.from(groupName, consumerName),
StreamOffset.create(stringKey, ReadOffset.lastConsumed()),
Consummer); //Consummer为消费者的监听类
listenerContainer.start();
return listenerContainer;
}
}
四、写监听类
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.stream.StreamListener;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Set;
@Slf4j
@Component
public class Listener implements StreamListener<String, MapRecord<String, String, String>> {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private WebsocketService websocketService;
//redis的stream数据发生变化 监听到的数据 我存储的是map形式如<"count":"6">
@Override
public void onMessage(MapRecord<String, String, String> message) {
log.info("stream名称-->{}", message.getStream());
log.info("消息ID-->{}", message.getId());
log.info("消息内容-->{}", message.getValue());
Map<String, String> msgMap = message.getValue();
String changeCounts = msgMap.get("counts");
//消息确认
stringRedisTemplate.opsForStream().acknowledge(streamKey, groupName, message.getId());
//发送消息
websocketService.sendCountMessage(changeCounts);
//删除消息
try{
stringRedisTemplate.opsForStream().delete(message.getStream(),message.getId());
}catch (Exception e){
e.printStackTrace();
}
}
}
五、WebSocket接口类编写
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.dubbo.config.spring.ReferenceBean;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
@Slf4j
@Component
@ServerEndpoint(value = "/monitor/count", encoders = {ServerEncoder.class})
public class WebsocketService {
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
/**
* 连接成功调用的方法
*/
@OnOpen
public void onOpen(Session session) {
try {
this.session = session;
websocketServices.add(this);
log.info("【websocket消息】有新的连接,总数为:{}", websocketServices.size());
} catch (Exception e) {
log.info("【websocket消息】有新的连接,总数为:{}", websocketServices.size());
}
}
/**
* 连接关闭调用的方法
*
* @param session
*/
@OnClose
public void onClose(Session session) {
try {
session.close();
websocketServices.remove(this);
log.info("【websocket消息】连接断开,总数为:{}", websocketServices.size());
} catch (IOException e) {
}
}
/**
* 发送错误时的处理
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("【websocket消息】错误原因:{}", error.getMessage());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message) {
try{
result.setMessage("连接成功");
} catch (Exception ex) {
result.setMessage("连接失败");
}
}
/**
* 发送消息
* @param message
*/
public void sendMessage(String message) {
try {
if (null == session) {
return;
}
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 推送消息
* @param message
*/
public void sendCountMessage(String message) {
try {
if (null == session) {
return;
}
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
六、生产者生产消息到redis的stream中
@RequestMapping("/test")
public void createMessage() {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
//redis的消息数量发生改变
Long count= 5;
Map<String, Object> msgMap = new HashMap<>();
msgMap.put("counts", count);
redisTemplate.opsForStream().add(streamkey, msgMap);
});
try {
future.get();
} catch (Exception e) {
e.printStackTrace();
}
}
七、测试
发送请求 创建连接 请求test接口 查看消息是否自动返回给前端
八、使用webSocket实现对数据的实时推送详解
1.什么是webSocket?
相对于 HTTP 这种非持久的协议来说,websocket是 HTML5 出的一个持久化的协议。
2.实时推送数据的实现方式以及应用场景
实现方式
1.轮询:客户端通过代码定时向服务器发送AJAX请求,服务器接收请求并返回响应信息。
优点:代码相对简单,适用于小型应用。
缺点:在服务器数据没有更新时,会造成请求重复数据,请求无用,浪费带宽和服务器资源。2.长连接:在页面中嵌入一个隐藏的iframe,将这个隐藏的iframe的属性设置为一个长连接的请求或者xrh请求,服务器通过这种方式往客户端输入数据。
优点:数据实时刷新,请求不会浪费,管理较简洁。
缺点:长时间维护保持一个长连接会增加服务器开销。3.webSocket:websocket是HTML5开始提供的一种客户端与服务器之间进行通讯的网络技术,通过这种方式可以实现客户端和服务器的长连接,双向实时通讯。
优点:减少资源消耗;实时推送不用等待客户端的请求;减少通信量;
缺点:少部分浏览器不支持,不同浏览器支持的程度和方式都不同
应用场景:聊天室、智慧大屏、消息提醒、股票k线图监控等。
九、封装工具类
import org.springframework.dao.DataAccessException;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisZSetCommands;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StreamOperations;
import java.time.Duration;
import java.util.List;
import java.util.Map;
/*Redis消息队列的工具类*/
public class RedisStreamUtil {
private static RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
private static StreamOperations<String, Object, Object> streamOperations = redisTemplate.opsForStream();
/**
* 创建消费组
* XGROUP CREATE key groupname id-or-$
* XGROUP SETID key groupname id-or-$ (消费组已创建,重新设置读取消息顺序)
* id为0表示组从stream的第一条数据开始读,
* id为$表示组从新的消息开始读取。(默认)
*/
public static String xGroupCreate(String key, String group){
return streamOperations.createGroup(key, group);
}
public String xGroupCreate(String key, ReadOffset offset, String group) {
return streamOperations.createGroup(key, offset, group);
}
/**
* 生产消息
* XADD key * hkey1 hval1 [hkey2 hval2...]
* key不存在,创建键为key的Stream流,并往流里添加消息
* key存在,往流里添加消息
*/
public static String xAdd(String key, Map<String, String> value){
return streamOperations.add(key, value).getValue();
}
/**
* 消息确认(从PEL中删除一条或多条消息)
* XACK key group ID[ID ...]
*/
public static Long xAck(String key, String group, String... recordIds){
return streamOperations.acknowledge(key, group, recordIds);
}
/**
* 批量删除消息
* XDEL key ID [ID ...]
*/
public static Long xDel(String key, RecordId... recordIds){
return streamOperations.delete(key, recordIds);
}
/**
* 查看Stream的详情
* XINFO STREAM key
*/
public StreamInfo.XInfoStream xInfo(String key) {
return streamOperations.info(key);
}
/**
* 查看Stream的消息个数
* XLEN key
*/
public Long xLen(String key) {
return streamOperations.size(key);
}
/**
* 查询消息
* XRANGE key start end [COUNT count]
* range:表示查询区间,比如区间(消息ID,消息ID2),查询消息ID到消息ID2之间的消息,特殊值("-","+")表示流中可能的最小ID和最大ID
* Range.unbounded():查询所有
* Range.closed(消息ID,消息ID2):查询[消息ID,消息ID2]
* Range.open(消息ID,消息ID2):查询(消息ID,消息ID2)
* limit:表示查询出来后限制显示个数
* Limit.limit().count(限制个数)
*/
public List<MapRecord<String, Object, Object>> xRange(String key, Range<String> range, RedisZSetCommands.Limit limit) {
return streamOperations.range(key, range, limit);
}
List<MapRecord<String, Object, Object>> xRange(String key, Range<String> range) {
return this.xRange(key, range, RedisZSetCommands.Limit.unlimited());
}
/**
* 查询消息
* XREVRANGE key end start [COUNT count]
* xReverseRange用法跟xRange一样,只是最后显示的时候是反序的,即消息ID从大到小显示
*/
public List<MapRecord<String, Object, Object>> xReverseRange(String key, Range<String> range, RedisZSetCommands.Limit limit) {
return streamOperations.reverseRange(key, range, limit);
}
/**
* 修剪/保留消息
* XTRIM key MAXLEN | MINID [~] count
* count:保留消息个数,当count是具体的消息ID时,表示移除ID小于count这个ID的所有消息
*/
public Long xTrim(String key, long count) {
return streamOperations.trim(key, count);
}
/**
* 销毁消费组
* XGROUP DESTROY key groupname
*/
public Boolean xGroupDestroy(String key, String group) {
return streamOperations.destroyGroup(key, group);
}
/**
* 查看消费组详情
* XINFO GROUPS key
*/
public StreamInfo.XInfoGroups xInfoGroups(String key) {
return streamOperations.groups(key);
}
/**
* 读取消息
* XREAD [COUNT count] [BLOCK milliseconds] STREAMS key[key ...] id[id ...]
* 从一个或者多个流中读取数据
* 特殊ID=0-0:从队列最先添加的消息读取
* 特殊ID=$:只接收从我们阻塞的那一刻开始通过XADD添加到流的消息,对已经添加的历史消息不感兴趣
* 在阻塞模式中,可以使用$,表示最新的消息ID。(在非阻塞模式下$无意义)。
*/
@SafeVarargs
public final List<MapRecord<String, Object, Object>> xRead(StreamReadOptions options, StreamOffset<String>... offsets) {
return streamOperations.read(options, offsets);
}
/**
* 读取消息,强制带消费组、消费者
* XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key[key ...] ID[ID ...]
* 特殊符号 0-0:表示从pending列表重新读取消息,不支持阻塞,无法读取的过程自动ack
* 特殊符号 > :表示只接收比消费者晚创建的消息,之前的消息不管
* 特殊符号 $ :在xReadGroup中使用是无意义的,报错提示:ERR The $ ID is meaningless in the context of XREADGROUP
*/
@SafeVarargs
public final List<MapRecord<String, Object, Object>> xReadGroup(Consumer consumer, StreamReadOptions options, StreamOffset<String>... offsets) {
return streamOperations.read(consumer, options, offsets);
}
/**
* 消费者详情
* XINFO CONSUMERS key group
*/
public StreamInfo.XInfoConsumers xInfoConsumers(String key, String group) {
return streamOperations.consumers(key, group);
}
/**
* 删除消费者
* XGROUP DELCONSUMER key groupname consumername
*/
public Boolean xGroupDelConsumer(String key, Consumer consumer) {
return streamOperations.deleteConsumer(key, consumer);
}
/**
* Pending Entries List (PEL)
* XPENDING key group [consumer] [start end count]
* 查看指定消费组的待处理列表
*/
public PendingMessagesSummary xPending(String key, String group) {
return streamOperations.pending(key, group);
}
/**
* 查看指定消费者的待处理列表
*/
PendingMessages xPending(String key, Consumer consumer) {
return this.xPending(key, consumer, Range.unbounded(), -1L);
}
public PendingMessages xPending(String key, Consumer consumer, Range<?> range, long count) {
return streamOperations.pending(key, consumer, range, count);
}
/**
* 消息转移
* XCLAIM key group consumer min-idle-time ID[ID ...]
* idleTime:转移条件,进入PEL列表的时间大于空闲时间
*/
List<ByteRecord> xClaim(String key, String group, String consumer, long idleTime, String recordId) {
return xClaim(key, group, consumer, idleTime, RecordId.of(recordId));
}
public List<ByteRecord> xClaim(String key, String group, String consumer, long idleTime, RecordId... recordIds) {
return redisTemplate.execute(new RedisCallback<List<ByteRecord>>() {
@Override
public List<ByteRecord> doInRedis(RedisConnection redisConnection) throws DataAccessException {
return redisConnection.streamCommands().xClaim(key.getBytes(), group, consumer, Duration.ofSeconds(idleTime), recordIds);
}
});
}
}
十、补充
1.测试也可用定时任务实现
2.上文中注入的RedisTemplate与StringRedisTemplate可能有出入 望读者统一下
3.redis的stream也可是配置多个消费者组和消费者 同一消费者组消息只读取一次 类似游标移动
4.上述demo为过程总结,具体代码或有问题存在!!!