工作七年,对消息推送使用的一些经验和总结
前言:不管是APP还是WEB端都离不开消息推送,尤其是APP端,push消息,小信箱消息;WEB端的代办消息等。因在项目中多次使用消息推送且也是很多项目必不可少的组成部分,故此总结下供自己参考。
一、什么是消息推送
消息推送(Push)指运营人员通过自己的产品或第三方工具对用户当前网页或移动设备进行的主动消息推送。用户可以在网页上或移动设备锁定屏幕和通知栏看到push消息通知
二、消息推送的种类
从数据模型分:推和拉
从终端分:APP端和WEB端
从实现层面分:短论询、Comet(长轮询)、Flash XMLSocket、SSE、Web-Socket
| 类型 | 概念 | 优点 | 缺点 | 备注 |
| 短轮询 | 客户端通过定期向服务器发送请求来获取最新的消息。服务器在接收到请求后立即响应,无论是否有新消息。如果服务器没有新消息可用,客户端将再次发送请求 | 后端编写简单 | 高延迟:因客户端定期发起请求,导致消息延迟,尤其是定期时间设置过长时 高网络负载:无新消息时也会频繁发起请求,消耗服务器资源和网络 时效性差:服务器产生了新消息,客户端不能立马感知到,需等到轮询时间到 | |
| Comet(长轮询) | 客户端发起请求,服务器接到请求后hold住连接,直到有新消息(或超时)才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求 | 减少请求次数:相对于短轮训而言 减少网络负载:没有消息时会保持连接,减少了频繁请求 时效性稍提高:相对于短轮询而言 | 没有新消息时会保持请求挂起,直到有新消息到达或超时。相比于短轮询,长轮询可以更快地获取新消息,减少了不必要的请求。 | |
| Flash XMLSocket | 在 HTML 页面中内嵌入一个使用了 XMLSocket 类的 Flash 程序。JavaScript 通过调用此 Flash 程序提供的socket接口与服务器端的socket进行通信 | 网络聊天室,网络互动游戏使用较多 | ||
| SSE(Server-send Events) | 服务器主动推送 | 时效性好:SSE使用了持久连接,可以实现比短轮询和长轮询更好的实时性 | 单向通道:SSE是单向的,只允许服务器向客户端推送消息,客户端无法向服务器发送消息 不适用低版本浏览器:SSE是HTML5的一部分,不支持低版本的浏览器。在使用SSE时,需要确保客户端浏览器的兼容性 | |
| Web-Socket | WebSocket是一种双向通信协议,允许在单个持久连接上进行全双工通信 | 时效性最佳:WebSocket 提供了真正的双向通信,可以实现实时的双向数据传输,具有最佳的实时性 低延迟:与轮询和长轮询相比,WebSocket 使用单个持久连接,减少了连接建立和断开的开销,从而降低了延迟 双向通信:WebSocket 允许服务器与客户端之间进行双向通信,服务器可以主动向客户端发送消息,同时客户端也可以向服务器发送消息 | 较高的网络负载:WebSocket 使用长连接,会占用一定的网络资源。在大规模并发场景下,需要注意服务器的负载情况 浏览器支持:大多数现代浏览器都支持 WebSocket,但需要注意在开发过程中考虑不同浏览器的兼容性 |
短轮询:客户端定时轮询发起请求
长轮询:客户端发起请求,等待后端响应并再次发起请求
Flash XMLSocket:
原理示意图:

利用Flash XML Socket实现”服务器推”技术前提:
(1)Flash提供了XMLSocket类,服务器利用Socket向Flash发送数据;
(2)JavaScript和Flash的紧密结合JavaScript和Flash可以相互调用。
优点是实现了socket通信,不再利用无状态的http进行伪推送。但是缺点更明显:
1.客户端必须安装 Flash 播放器;
2.因为 XMLSocket 没有 HTTP 隧道功能,XMLSocket 类不能自动穿过防火墙;
3.因为是使用套接口,需要设置一个通信端口,防火墙、代理服务器也可能对非 HTTP 通道端口进行限制。
SSE:当使用Server-Sent Events(SSE)时,客户端(通常是浏览器)与服务器之间建立一种持久的连接,使服务器能够主动向客户端发送数据。这种单向的、服务器主动推送数据的通信模式使得实时更新的数据能够被实时地传送到客户端,而无需客户端进行轮询请求
SSE的工作原理如下:
-
建立连接:客户端通过使用
EventSource对象在浏览器中创建一个与服务器的连接。客户端向服务器发送一个HTTP请求,请求的头部包含Accept: text/event-stream,以表明客户端希望接收SSE数据。服务器响应这个请求,并建立一个持久的HTTP连接。 -
保持连接:服务器保持与客户端的连接打开状态,不断发送数据。这个连接是单向的,只允许服务器向客户端发送数据,客户端不能向服务器发送数据。
-
服务器发送事件:服务器使用
Content-Type: text/event-stream标头来指示响应是SSE数据流。服务器将数据封装在特定的SSE格式中,每个事件都以data:开头,后面是实际的数据内容,以及可选的其他字段,如event:和id:。服务器发送的数据可以是任何文本格式,通常是JSON。 -
客户端接收事件:客户端通过
EventSource对象监听服务器发送的事件。当服务器发送事件时,EventSource对象会触发相应的事件处理程序,开发人员可以在处理程序中获取到事件数据并进行相应的操作。常见的事件是message事件,表示接收到新的消息。 -
断开连接:当客户端不再需要接收服务器的事件时,可以关闭连接。客户端可以调用
EventSource对象的close()方法来显式关闭连接,或者浏览器在页面卸载时会自动关闭连接。
在Spring Boot中,可以使用SseEmitter类来实现SSE:
@RestController
public class SSEController {private SseEmitter sseEmitter;@GetMapping("/subscribe")public SseEmitter subscribe() {sseEmitter = new SseEmitter();return sseEmitter;}@PostMapping("/send-message")public void sendMessage(@RequestBody String message) {try {if (sseEmitter != null) {sseEmitter.send(SseEmitter.event().data(message));}} catch (IOException e) {e.printStackTrace();}}
}
<script>// 创建一个EventSource对象,指定SSE的服务端端点var eventSource = new EventSource('/subscribe');console.log("eventSource=", eventSource)// 监听message事件,接收从服务端发送的消息eventSource.addEventListener('message', function(event) {var message = event.data;console.log("message=", message)var messageContainer = document.getElementById('message-container');messageContainer.innerHTML += '<p>' + message + '</p>';});
</script>
上述过程:客户端可以通过访问/subscribe接口来订阅SSE事件,服务器会返回一个SseEmitter对象。当有新消息到达时,调用SseEmitter对象的send()方法发送消息。
Web-Socket:

HTML代码:
<script>// 创建WebSocket对象,并指定服务器的URLvar socket = new WebSocket('ws://localhost:8080/上下文路径/channel/message/');// 监听WebSocket的连接事件socket.onopen = function(event) {console.log('WebSocket connected');};// 监听WebSocket的消息事件socket.onmessage = function(event) {var message = event.data;var messageContainer = document.getElementById('message-container');messageContainer.innerHTML += '<p>' + message + '</p>';};// 监听WebSocket的关闭事件socket.onclose = function(event) {console.log('WebSocket closed');};// 发送消息到服务器function sendMessage() {var messageInput = document.getElementById('message-input');var message = messageInput.value;socket.send(message);messageInput.value = '';}
</script>
三、项目中使用的消息推送
例子1:Web-Socket
1.引入websocket依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency>
2.websocket配置
/*** @描述 开启WebSocket支持的配置类* 自动注册使用@ServerEndpoint*/
@Configuration
public class WebSocketConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}
3.websocket服务器端代码
说明:@ ServerEndpoint 注解是一个类层次的注解,主要是将当前类定义成一个websocket服务器端, 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
/*** 消息推送**/
@ServerEndpoint("/channel/message/{user-id}")
@Slf4j
@Component
@RequiredArgsConstructor
public class TodoChannel implements ApplicationListener<FlowMessageEvent> {private static final Map<String, Set<Session>> SESSION_MAP = new ConcurrentHashMap<>();private Session session;private String userId;@OnMessagepublic void onMessage(String message) {log.info("websocket消息(id={}): {}", this.session.getId(), message);}@OnOpenpublic void onOpen(Session session, @PathParam("user-id") String userId) {this.session = session;this.userId = userId;val sessionSet = SESSION_MAP.getOrDefault(this.userId, new CopyOnWriteArraySet<>());sessionSet.add(session);SESSION_MAP.put(this.userId, sessionSet);log.info("websocket连接: id={}", this.session.getId());val message = new MessageModel();//往todoModel放业务数据session.getAsyncRemote().sendText(JSON.toJSONString(message));}@OnClosepublic void onClose(CloseReason closeReason) {val sessionSet = SESSION_MAP.get(this.userId);if (sessionSet != null) {sessionSet.remove(session);}log.info("websocket断开: id={} {}", this.session.getId(), closeReason);}@OnErrorpublic void onError(Throwable throwable) {log.warn("websocket异常: id={} throwable:", this.session.getId(), throwable);val sessionSet = SESSION_MAP.get(this.userId);if (sessionSet != null) {sessionSet.remove(this.session);}try {this.session.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, throwable.getMessage()));} catch (IOException e) {log.error("websocket关闭失败", e);}}@Overridepublic void onApplicationEvent(@NotNull FlowMessageEvent event) {Set<String> userIds = CastUtils.cast(event.getSource());userIds.forEach(id -> {val sessionSet = SESSION_MAP.get(id);if (sessionSet == null) {return;}//业务处理todosessionSet.forEach(s -> s.getAsyncRemote().sendObject(JSON.toJSON(message)));});}@Scheduled(fixedRate = 24 * 60 * 60 * 1000L)public void sessionCleaner() {log.info("websocket message channel session清理");val keyToClean = new HashSet<String>();SESSION_MAP.forEach((k, v) -> {val sessionToClean = new HashSet<Session>();v.forEach(s -> {if (!s.isOpen()) {sessionToClean.add(s);}});v.removeAll(sessionToClean);if (v.isEmpty()) {keyToClean.add(k);}});keyToClean.forEach(SESSION_MAP::remove);}@Dataprivate static class MessageModel implements Serializable {private static final long serialVersionUID = 1L;private List<Info> list;private Integer size;@AllArgsConstructor@Valueprivate static class Info implements Serializable {private static final long serialVersionUID = 1L;String type;String name;}}
}
4.事件类代码
/*** message事件**/
public class FlowMessageEvent extends ApplicationEvent {public FlowMessageEvent(Object source) {super(source);}
}
5.使用事件推送消息
applicationEventPublisher.publishEvent(new FlowMessageEvent(user));
例子2:RabbitMq:
1.引入rabbitmq依赖
2.编写rabbitmq配置类
/*** @author wux* @version 1.0.0*/
@SpringBootConfiguration
@Slf4j
public class RabbitMqConfig {@Value("${spring.rabbitmq.host:10.128.30.xxx}")private String host;@Value("${spring.rabbitmq.port:5672}")private int port;@Value("${spring.rabbitmq.username:guest}")private String username;@Value("${spring.rabbitmq.password:guest}")private String password;@Beanpublic ConnectionFactory connectionFactory() {CachingConnectionFactory factory = new CachingConnectionFactory(host, port);factory.setUsername(username);factory.setPassword(password);//连接工厂开启消息确认和消息返回机制
// factory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED);
// factory.setPublisherReturns(true);return factory;}@Beanpublic RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();factory.setConnectionFactory(connectionFactory);//使用json 序列化和反序列化factory.setMessageConverter(new Jackson2JsonMessageConverter());//factory.setAcknowledgeMode(AcknowledgeMode.AUTO);return factory;}@Bean@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)public RabbitTemplate rabbitTemplate() {RabbitTemplate template = new RabbitTemplate();template.setConnectionFactory(connectionFactory());template.setMessageConverter(new Jackson2JsonMessageConverter());return template;}@Beanpublic Queue testDirectQueue() {return new Queue(RabbitMqConsts.TEST_QUE_PHM_WARN_INFO, true, false, false);}@Beanpublic DirectExchange testDirectExchange() {return new DirectExchange(RabbitMqConsts.TEST_EXC_PHM_WARN_INFO, true,false);}@Beanpublic Binding TestBinding() {return BindingBuilder.bind(testDirectQueue()).to(testDirectExchange()).with(RabbitMqConsts.TEST_KEY_PHM_WARN_INFO);}
}
3.编写rabbitmq工具类
/*** @author wux* @version 1.0.0* @description rabbit工具类*/
@Slf4j
public class RabbitMqUtil {private static AmqpAdmin getAmqpAdmin() {return SpringContextUtils.getBean("amqpAdmin",AmqpAdmin.class);}/**通过amqpAdmin动态创建队列、交换机和绑定关系ttlFlag :设置消息过期时间*/public static void createQueueAndExchangeIfNeed(String businessName, ExchangeEnum typeEnum, Integer ttl) {String exchangeName = "exc_" + businessName;String queueName = "que_" + businessName;String routingKey = "key_" + businessName;if (CheckUtil.isNotEmpty(getQueueInfo(queueName))) {return;}//创建队列Queue queue = createAndBindQueue(queueName, ttl);//创建交换机Exchange exchange = createAndBindExchange(exchangeName, typeEnum);//绑定队列和交换机switch (typeEnum){case DIRECT:binding(queueName, exchangeName, routingKey, typeEnum);break;case FANOUT:fanoutBinding(queue, exchange);break;default:binding(queueName, exchangeName, routingKey, typeEnum);}}private static Queue createAndBindQueue(String queueName, Integer ttl) {Map<String, Object> arguments = new HashMap<>();//设置过期时间,单位是毫秒if (CheckUtil.isNotEmpty(ttl)) {arguments.put("x-message-ttl", ttl);}if (CheckUtil.isEmpty(queueName)) {log.error("队列名称为空!queueName=" + queueName);throw new BusinessException("队列名称为空!");}Queue queue = new Queue(queueName, true, false, false, arguments);getAmqpAdmin().declareQueue(queue);return queue;}public static QueueInformation getQueueInfo (String queueName) {if (CheckUtil.isEmpty(queueName)) {return null;}return getAmqpAdmin().getQueueInfo(queueName);}private static Exchange createAndBindExchange(String exchangeName, ExchangeEnum typeEnum){AbstractExchange exchange = null;switch (typeEnum){case DIRECT:exchange = new DirectExchange(exchangeName, true, false);break;case TOPIC:exchange = new TopicExchange(exchangeName, true, false);break;case FANOUT:exchange = new FanoutExchange(exchangeName, true, false);break;case HEADERS:exchange = new HeadersExchange(exchangeName, true, false);break;default:exchange = new DirectExchange(exchangeName, true, false);}getAmqpAdmin().declareExchange(exchange);return exchange;}private static void binding(String queueName, String exchangeName, String routingKey, ExchangeEnum typeEnum) {//绑定队列和交换机Binding binding = new Binding(queueName, Binding.DestinationType.QUEUE, exchangeName, routingKey, null);getAmqpAdmin().declareBinding(binding);}private static void fanoutBinding(Queue queue, Exchange exchange) {BindingBuilder.bind(queue).to(exchange);}
}
4.监听和推送消息
/*** @author wux* @version 1.0.0* @description 监听phm设备状态消息*/
@Component
@Slf4j
public class MotePhmDeviceStatesListener {private static final String CLASS_NAME = "MotePhmDeviceStatesListener";@Autowiredprivate Map<String, AssembleDeviceStatesStrategy> map = new ConcurrentHashMap<String, AssembleDeviceStatesStrategy>();@Resourceprivate MoteMessageService moteMessageService;@Autowiredprotected BeanMapper beanMapper;@RabbitHandler@RabbitListener(bindings = @QueueBinding(value=@Queue("que_phm_device_states"),exchange = @Exchange("exc_phm_device_states"),key = "key_phm_device_states"))public void process(@Payload BaseMessage req, Message msg, Channel channel) {final String METHOD_NAME ="process";log.info(CLASS_NAME + "-" + METHOD_NAME + "-start,req={},msg={}", req, msg);long deliverTag = msg.getMessageProperties().getDeliveryTag();if (!MessageTypeEnum.DEVICE_STATES.getKey().equals(req.getType())) {log.warn(CLASS_NAME + "-" + METHOD_NAME + "-message=消息类型不匹配!");//拒绝,重新回到队列//channel.clearReturnListeners();//channel.basicReject(deliverTag, true);return;}try {String service = MessageSubTypeEnum.getService(req.getSubType());AssembleDeviceStatesStrategy strategy = map.get(service);BaseMessage reqMessage = beanMapper.map(req, BaseMessage.class);BaseMessage retMessage = strategy.assembleDeviceStates(reqMessage);strategy.sendMessage(retMessage, reqMessage, channel);} catch (Exception e) {log.error(CLASS_NAME + "-" + METHOD_NAME + "-异常, e={}", e);return;//拒绝,重新回到队列//channel.basicNack(deliverTag, false,true);}}
}
public void sendMessage(BaseMessage retMessage, BaseMessage req, Channel channel) {retMessage.setDate(new Date());retMessage.setDateStr(DateUtils.format(retMessage.getDate(), DateUtils.DATE_TIME_SECOND));if (CheckUtil.isEmpty(req.getQueueName())) {log.warn("AssembleDeviceStatesStrategy" + "队列名称为空,req={}", req);return;}QueueInformation queueInfo = RabbitMqUtil.getQueueInfo(req.getQueueName());if (CheckUtil.isEmpty(queueInfo) || CheckUtil.isEmpty(queueInfo.getName())) {log.warn("AssembleDeviceStatesStrategy" + "-" + "队列不存在!" + ",req={}", req);return;}
// try {
// long count = channel.messageCount(req.getQueueName());
// if (count >= 5000) {
// channel.queueDelete(req.getQueueName());
// }
//
// } catch (IOException e) {
// log.error("AssembleDeviceStatesStrategy" + "-" + "清除队列消息失败" + ",retMessage={}", retMessage, e);
// }rabbitTemplate.convertAndSend(req.getQueueName(), retMessage);log.info("AssembleDeviceStatesStrategy" + "-" + "sendMessage推给前端信息" + ",retMessage={},req={}", retMessage, req);}
例子3:Kafka:
使用@KafkaListene(topics="xxx", groupId="xxx") 接受消息
四、消息中间件:RabbitMQ、RocketMQ、Kafka
高并发情况下,或者规模较大,推荐使用消息中间件,搭建一个公共平台,统一管理消息推送,项目层面进行隔离即可。
RabbitMQ可看:https://blog.csdn.net/baidu_35160588/article/details/89027810
相关文章:
工作七年,对消息推送使用的一些经验和总结
前言:不管是APP还是WEB端都离不开消息推送,尤其是APP端,push消息,小信箱消息;WEB端的代办消息等。因在项目中多次使用消息推送且也是很多项目必不可少的组成部分,故此总结下供自己参考。 一、什么是消息推…...
计网——应用层
应用层 应用层协议原理 网络应用的体系结构 客户-服务器(C/S)体系结构 对等体(P2P)体系结构 C/S和P2P体系结构的混合体 客户-服务器(C/S)体系结构 服务器 服务器是一台一直运行的主机,需…...
算法面试八股文『 基础知识篇 』
博客介绍 近期在准备算法面试,网上信息杂乱不规整,出于强迫症就自己整理了算法面试常出现的考题。独乐乐不如众乐乐,与其奖励自己,不如大家一起嗨。以下整理的内容可能有不足之处,欢迎大佬一起讨论。 PS:…...
docker-学习-4
docker学习第四天 docker学习第四天1. 回顾1.1. 容器的网络类型1.2. 容器的本质1.3. 数据的持久化1.4. 看有哪些卷1.5. 看卷的详细信息 2. 如何做多台宿主机里的多个容器之间的数据共享2.1. 概念2.2. 搭NFS服务器实现多个容器之间的数据共享的详细步骤2.3. 如果是多台机器&…...
el-upload子组件上传多张图片(上传为files或base64url)
场景: 在表单页,有图片需要上传,表单的操作行按钮中有上传按钮,点击上传按钮。 弹出el-dialog进行图片的上传,可以上传多张图片。 由于多个表单页都有上传多张图片的操作,因此将上传多图的el-upload定义…...
2024美赛数学建模C题思路源码——网球选手的动量
这题挺有意思,没具体看比赛情况,打过比赛的人应该都知道险胜局(第二局、第五局逆转局)最影响心态的,导致第3、5局输了 模型结果需要证明这样的现象 赛题目的 赛题目的:分析网球球员的表现 问题一.球员在比赛特定时间表现力 问题分析 excel数据:每个时间段有16场比赛,…...
金三银四_程序员怎么写简历_写简历网站
你们在制作简历时,是不是基本只关注两件事:简历模板,还有基本信息的填写。 当你再次坐下来更新你的简历时,可能会发现自己不自觉地选择了那个“看起来最好看的模板”,填写基本信息,却没有深入思考如何使简历更具吸引力。这其实是一个普遍现象:许多求职者仍停留在传统简历…...
echarts条形图添加滚动条
效果展示: 测试数据: taskList:[{majorDeptName:测试,finishCount:54,notFinishCount:21}, {majorDeptName:测试,finishCount:54,notFinishCount:21}, {majorDeptName:测试,finishCount:54,notFinishCount:21}, {majorDeptName:测试,finishCount:54,notFinishCount:21}, {maj…...
Java 使用Soap方式调用WebService接口
pom文件依赖 <dependencies><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.13.0</version></dependency><!-- https://mvnrepository.com/artif…...
2024美赛数学建模所有题目思路分析
美赛思路已更新,关注后可以获取更多思路。并且领取资料 C题思路 首先,我们要理解势头是什么。简单来说,势头是一方在比赛中因一系列事件而获得的动力或优势。在网球中,这可能意味着连续赢得几个球,或是在比赛的某个关…...
Docker容器引擎(5)
目录 一.docker-compose docker-compose的三大概念: yaml文件格式: json文件格式: docker-compose 配置模板文件常用的字段: 二.Docker Compose 环境安装: 查看版本: 准备好nginx 的dockerfile的文…...
百分点科技:《数据科学技术: 文本分析和知识图谱》
科技进步带来的便利已经渗透到工作生活的方方面面,ChatGPT的出现更是掀起了新一波的智能化浪潮,推动更多智能应用的涌现。这背后离不开一个朴素的逻辑,即对数据的收集、治理、建模、分析和应用,这便是数据科学所重点研究的对象——…...
LabVIEW传感器通用实验平台
LabVIEW传感器通用实验平台 介绍了基于LabVIEW的传感器实验平台的开发。该平台利用LabVIEW图形化编程语言和多参量数据采集卡,提供了一个交互性好、可扩充性强、使用灵活方便的传感器技术实验环境。 系统由硬件和软件两部分组成。硬件部分主要包括多通道数据采集卡…...
向日葵企业“云策略”升级 支持Android 被控策略设置
此前,贝锐向日葵推出了适配PC企业客户端的云策略功能,这一功能支持管理平台统一修改设备设置,上万设备实时下发实时生效,很好的解决了当远程控制方案部署后,想要灵活调整配置需要逐台手工操作的痛点,大幅提…...
51单片机通过级联74HC595实现倒计时秒表Protues仿真设计
一、设计背景 近年来随着科技的飞速发展,单片机的应用正在不断的走向深入。本文阐述了51单片机通过级联74HC595实现倒计时秒表设计,倒计时精度达0.05s,解决了传统的由于倒计时精度不够造成的误差和不公平性,是各种体育竞赛的必备设…...
深信服技术认证“SCCA-C”划重点:深信服云计算关键技术
为帮助大家更加系统化地学习云计算知识,高效通过云计算工程师认证,深信服特推出“SCCA-C认证备考秘笈”,共十期内容。“考试重点”内容框架,帮助大家快速get重点知识。 划重点来啦 *点击图片放大展示 深信服云计算认证(…...
Redis stream特性了解
在发布订阅中我们了解到发布订阅模式存在的无法持久化保存消息和对于离线重连的客户端不能读取历史消息的缺陷,以下就来了解一下stream是如何解决这个问题的 steam是类似于仅添加log的数据结构,提供了以下基本命令 XADD: 添加新条目到stream # 语法xadd…...
苍穹外卖项目可以写的简历和如何优化简历
文章目录 重点写中规写添加自己个性的项目面试会问道的问题 我是一名双非大二计算机本科生,希望我的分享对你有帮助,点赞关注不迷路。 简历编写一直是很多人求职人的心病,我自己上学期有一门课程是去校内企业面试,当时我就感受出…...
C++:智能指针
C在用引用取缔掉指针的同时,模板的引入带给了指针新的发挥空间 智能指针简单的来说就是带有不同特性和内存管理的指针模板 unique_ptr 1.不能有多个对象指向一块内存 2.对象释放时内部指针指向地址也随之释放 3.对象内数据只能通过接口更改绑定 4.对象只能接收右值…...
用户界面(UI)、用户体验(UE)和用户体验(UX)的差异
对一个应用程序而言,UX/UE (user experience) 设计和 UI (user interface) 设计非常重要。UX设计包括可视化布局、信息结构、可用性、图形、互动等多个方面。UI设计也属于UX范畴。正是因为三者在一定程度上具有重叠的工作内容,很多从业多年的设计师都分不…...
React hook之useRef
React useRef 详解 useRef 是 React 提供的一个 Hook,用于在函数组件中创建可变的引用对象。它在 React 开发中有多种重要用途,下面我将全面详细地介绍它的特性和用法。 基本概念 1. 创建 ref const refContainer useRef(initialValue);initialValu…...
k8s从入门到放弃之Ingress七层负载
k8s从入门到放弃之Ingress七层负载 在Kubernetes(简称K8s)中,Ingress是一个API对象,它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress,你可…...
【力扣数据库知识手册笔记】索引
索引 索引的优缺点 优点1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度(创建索引的主要原因)。3. 可以加速表和表之间的连接,实现数据的参考完整性。4. 可以在查询过程中,…...
全球首个30米分辨率湿地数据集(2000—2022)
数据简介 今天我们分享的数据是全球30米分辨率湿地数据集,包含8种湿地亚类,该数据以0.5X0.5的瓦片存储,我们整理了所有属于中国的瓦片名称与其对应省份,方便大家研究使用。 该数据集作为全球首个30米分辨率、覆盖2000–2022年时间…...
基于当前项目通过npm包形式暴露公共组件
1.package.sjon文件配置 其中xh-flowable就是暴露出去的npm包名 2.创建tpyes文件夹,并新增内容 3.创建package文件夹...
12.找到字符串中所有字母异位词
🧠 题目解析 题目描述: 给定两个字符串 s 和 p,找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义: 若两个字符串包含的字符种类和出现次数完全相同,顺序无所谓,则互为…...
C++使用 new 来创建动态数组
问题: 不能使用变量定义数组大小 原因: 这是因为数组在内存中是连续存储的,编译器需要在编译阶段就确定数组的大小,以便正确地分配内存空间。如果允许使用变量来定义数组的大小,那么编译器就无法在编译时确定数组的大…...
华为OD机考-机房布局
import java.util.*;public class DemoTest5 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseSystem.out.println(solve(in.nextLine()));}}priv…...
群晖NAS如何在虚拟机创建飞牛NAS
套件中心下载安装Virtual Machine Manager 创建虚拟机 配置虚拟机 飞牛官网下载 https://iso.liveupdate.fnnas.com/x86_64/trim/fnos-0.9.2-863.iso 群晖NAS如何在虚拟机创建飞牛NAS - 个人信息分享...
为什么要创建 Vue 实例
核心原因:Vue 需要一个「控制中心」来驱动整个应用 你可以把 Vue 实例想象成你应用的**「大脑」或「引擎」。它负责协调模板、数据、逻辑和行为,将它们变成一个活的、可交互的应用**。没有这个实例,你的代码只是一堆静态的 HTML、JavaScript 变量和函数,无法「活」起来。 …...
