【stomp 实战】spring websocket 接收消息源码分析

后台消息的发送过程,我们通过spring websocket用户消息发送源码分析已经了解了。我们再来分析一下后端接收消息的过程。这个过程和后端发送消息过程有点类似。

前端发送消息

前端发送消息给服务端的示例如下:
发送给目的/app/echo一个消息。

//主动发送消息给服务器,对应的后端topic为/app/echo
function send() {var value = document.getElementById("content").value;var msg = {msgType: 1,content: value};stompClient.send("/app/echo", {}, JSON.stringify(msg));//stompClient.send("/app/echo2", {}, JSON.stringify(msg));
}

后端接收消息的配置

 @Overridepublic void configureMessageBroker(MessageBrokerRegistry registry) {/*** 这里表示前端往/app路径推送, 如果后端定义一个controller ->@MessageMapping("/echo"),* stompClient.send("/app/echo",{},...)* 这时,消息会被推送到注解对应的@MessageMapping("/echo")方法上*/registry.setApplicationDestinationPrefixes("/app");}

后端配置/app前缀。
这个前缀和哪里结合起来用呢,来看下面的代码

@Slf4j
@Controller
@RequiredArgsConstructor(onConstructor_ = {@Autowired})
public class StompController {private final SimpMessageSendingOperations msgOperations;private final SimpUserRegistry simpUserRegistry;/*** 回音消息,将用户发来的消息内容加上 Echo 前缀后推送回客户端*/@MessageMapping("/echo")public void echo(Principal principal, Msg msg) {String username = principal.getName();msg.setContent("Echo: " + msg.getContent());msgOperations.convertAndSendToUser(username, "/topic/answer", msg);int userCount = simpUserRegistry.getUserCount();int sessionCount = simpUserRegistry.getUser(username).getSessions().size();log.info("当前本系统总在线人数: {}, 当前用户: {}, 该用户的客户端连接数: {}", userCount, username, sessionCount);}
}

实际上就和这个echo方法结合一起用的。 @MessageMapping(“/echo”)中的/echo和前缀结合一起,就是/app/echo。
因此,这个echo方法,就是接收前端发送消息的方法入口。

源码分析

消息处理器的注册

在 spring websocket源码分析之握手请求的处理这一节中,在完成websocket握手请求后,我们看到了如下的代码。

	public void onOpen(final javax.websocket.Session session, EndpointConfig config) {this.wsSession.initializeNativeSession(session);// The following inner classes need to remain since lambdas would not retain their// declared generic types (which need to be seen by the underlying WebSocket engine)if (this.handler.supportsPartialMessages()) {session.addMessageHandler(new MessageHandler.Partial<String>() {@Overridepublic void onMessage(String message, boolean isLast) {handleTextMessage(session, message, isLast);}});session.addMessageHandler(new MessageHandler.Partial<ByteBuffer>() {@Overridepublic void onMessage(ByteBuffer message, boolean isLast) {handleBinaryMessage(session, message, isLast);}});}else {session.addMessageHandler(new MessageHandler.Whole<String>() {@Overridepublic void onMessage(String message) {handleTextMessage(session, message, true);}});session.addMessageHandler(new MessageHandler.Whole<ByteBuffer>() {@Overridepublic void onMessage(ByteBuffer message) {handleBinaryMessage(session, message, true);}});}session.addMessageHandler(new MessageHandler.Whole<javax.websocket.PongMessage>() {@Overridepublic void onMessage(javax.websocket.PongMessage message) {handlePongMessage(session, message.getApplicationData());}});try {this.handler.afterConnectionEstablished(this.wsSession);}catch (Exception ex) {ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger);}}

代码总结:

  • 这里入参传了一个javax.websocket.Session。这个可以理解为当前Websocket连接。
  • 原来这个Session可以给自己添加messageHandler,那当有消息来的时候,就会经过这些handler来进行处理。
  • 那这个hander就是处理业务消息的重点了
    看一下这个hander是怎么处理消息的
private void handleTextMessage(javax.websocket.Session session, String payload, boolean isLast) {TextMessage textMessage = new TextMessage(payload, isLast);try {this.handler.handleMessage(this.wsSession, textMessage);}catch (Exception ex) {ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger);}}

这个handler,对应的实现是:SockJsWebSocketHandler
进入handleMessage看一下处理逻辑,原来是将消息分为三类

  • 文本消息
  • 二进制消息
  • 心跳消息
    这三种消息,分别进行处理
@Overridepublic void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {if (message instanceof TextMessage) {handleTextMessage(session, (TextMessage) message);}else if (message instanceof BinaryMessage) {handleBinaryMessage(session, (BinaryMessage) message);}else if (message instanceof PongMessage) {handlePongMessage(session, (PongMessage) message);}else {throw new IllegalStateException("Unexpected WebSocket message type: " + message);}}

我们一般处理的是文本消息

	@Overridepublic void handleTextMessage(WebSocketSession wsSession, TextMessage message) throws Exception {this.sockJsSession.handleMessage(message, wsSession);}

又交给sockJsSession来处理消息。
再看下WebSocketServerSockJsSession的handlerMessage方法。
往下,找到了delegateMessages。

//WebSocketServerSockJsSessionpublic void handleMessage(TextMessage message, WebSocketSession wsSession) throws Exception {String payload = message.getPayload();if (!StringUtils.hasLength(payload)) {return;}String[] messages;try {messages = getSockJsServiceConfig().getMessageCodec().decode(payload);}catch (Exception ex) {logger.error("Broken data received. Terminating WebSocket connection abruptly", ex);tryCloseWithSockJsTransportError(ex, CloseStatus.BAD_DATA);return;}if (messages != null) {delegateMessages(messages);}}public void delegateMessages(String... messages) throws SockJsMessageDeliveryException {for (int i = 0; i < messages.length; i++) {try {if (isClosed()) {logUndeliveredMessages(i, messages);return;}this.handler.handleMessage(this, new TextMessage(messages[i]));}catch (Exception ex) {if (isClosed()) {if (logger.isTraceEnabled()) {logger.trace("Failed to handle message '" + messages[i] + "'", ex);}logUndeliveredMessages(i, messages);return;}throw new SockJsMessageDeliveryException(this.id, getUndelivered(messages, i), ex);}}}

可以看到delegateMessages实际上是把消息一条条处理。交给了handler来处理。
这里的hander是什么?SubProtocolWebSocketHandler。

//SubProtocolWebSocketHandler@Overridepublic void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {WebSocketSessionHolder holder = this.sessions.get(session.getId());if (holder != null) {session = holder.getSession();}SubProtocolHandler protocolHandler = findProtocolHandler(session);protocolHandler.handleMessageFromClient(session, message, this.clientInboundChannel);if (holder != null) {holder.setHasHandledMessages();}checkSessions();}

这里就是通过session取出子协议处理器,这里实际上就一个实现,是StompSubProtocolHandler。

//StompSubProtocolHandler@Overridepublic void handleMessageFromClient(WebSocketSession session,WebSocketMessage<?> webSocketMessage, MessageChannel outputChannel) {List<Message<byte[]>> messages;try {ByteBuffer byteBuffer;if (webSocketMessage instanceof TextMessage) {byteBuffer = ByteBuffer.wrap(((TextMessage) webSocketMessage).asBytes());}else if (webSocketMessage instanceof BinaryMessage) {byteBuffer = ((BinaryMessage) webSocketMessage).getPayload();}else {return;}BufferingStompDecoder decoder = this.decoders.get(session.getId());if (decoder == null) {if (!session.isOpen()) {logger.trace("Dropped inbound WebSocket message due to closed session");return;}throw new IllegalStateException("No decoder for session id '" + session.getId() + "'");}messages = decoder.decode(byteBuffer);if (messages.isEmpty()) {if (logger.isTraceEnabled()) {logger.trace("Incomplete STOMP frame content received in session " +session + ", bufferSize=" + decoder.getBufferSize() +", bufferSizeLimit=" + decoder.getBufferSizeLimit() + ".");}return;}}catch (Throwable ex) {if (logger.isErrorEnabled()) {logger.error("Failed to parse " + webSocketMessage +" in session " + session.getId() + ". Sending STOMP ERROR to client.", ex);}handleError(session, ex, null);return;}for (Message<byte[]> message : messages) {StompHeaderAccessor headerAccessor =MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);Assert.state(headerAccessor != null, "No StompHeaderAccessor");StompCommand command = headerAccessor.getCommand();boolean isConnect = StompCommand.CONNECT.equals(command) || StompCommand.STOMP.equals(command);boolean sent = false;try {headerAccessor.setSessionId(session.getId());headerAccessor.setSessionAttributes(session.getAttributes());headerAccessor.setUser(getUser(session));if (isConnect) {headerAccessor.setUserChangeCallback(user -> {if (user != null && user != session.getPrincipal()) {this.stompAuthentications.put(session.getId(), user);}});}headerAccessor.setHeader(SimpMessageHeaderAccessor.HEART_BEAT_HEADER, headerAccessor.getHeartbeat());if (!detectImmutableMessageInterceptor(outputChannel)) {headerAccessor.setImmutable();}if (logger.isTraceEnabled()) {logger.trace("From client: " + headerAccessor.getShortLogMessage(message.getPayload()));}if (isConnect) {this.stats.incrementConnectCount();}else if (StompCommand.DISCONNECT.equals(command)) {this.stats.incrementDisconnectCount();}try {SimpAttributesContextHolder.setAttributesFromMessage(message);sent = outputChannel.send(message);if (sent) {if (this.eventPublisher != null) {Principal user = getUser(session);if (isConnect) {publishEvent(this.eventPublisher, new SessionConnectEvent(this, message, user));}else if (StompCommand.SUBSCRIBE.equals(command)) {publishEvent(this.eventPublisher, new SessionSubscribeEvent(this, message, user));}else if (StompCommand.UNSUBSCRIBE.equals(command)) {publishEvent(this.eventPublisher, new SessionUnsubscribeEvent(this, message, user));}}}}finally {SimpAttributesContextHolder.resetAttributes();}}catch (Throwable ex) {if (logger.isDebugEnabled()) {logger.debug("Failed to send message to MessageChannel in session " + session.getId(), ex);}else if (logger.isErrorEnabled()) {// Skip unsent CONNECT messages (likely auth issues)if (!isConnect || sent) {logger.error("Failed to send message to MessageChannel in session " + session.getId() +":" + ex.getMessage());}}handleError(session, ex, message);}}}

代码很长,总结一下:

  • 1、消息报文的编码处理,转换成Message对象
  • 2、StompHeaderAccessor的处理,包括设置user、session等
  • 3、调用outputChannel发送消息:outputChannel.send(message);
  • 4、如果发送消息成功,则发送相应的事件消息,有以下几类事件:SessionConnectEvent、SessionSubscribeEvent、SessionUnsubscribeEvent。

MessageChannel发送消息过程

outputChannel.send(message),发送消息,这个似乎似曾相识。在 【stomp 实战】spring websocket用户消息发送源码分析 这一节中,我们也看到过这个类。在服务端往客户端发送消息时,也有这个MessageChannel的出现。

//AbstractMessageChannel@Overridepublic final boolean send(Message<?> message, long timeout) {Assert.notNull(message, "Message must not be null");Message<?> messageToUse = message;ChannelInterceptorChain chain = new ChannelInterceptorChain();boolean sent = false;try {messageToUse = chain.applyPreSend(messageToUse, this);if (messageToUse == null) {return false;}sent = sendInternal(messageToUse, timeout);chain.applyPostSend(messageToUse, this, sent);chain.triggerAfterSendCompletion(messageToUse, this, sent, null);return sent;}catch (Exception ex) {chain.triggerAfterSendCompletion(messageToUse, this, sent, ex);if (ex instanceof MessagingException) {throw (MessagingException) ex;}throw new MessageDeliveryException(messageToUse,"Failed to send message to " + this, ex);}catch (Throwable err) {MessageDeliveryException ex2 =new MessageDeliveryException(messageToUse, "Failed to send message to " + this, err);chain.triggerAfterSendCompletion(messageToUse, this, sent, ex2);throw ex2;}}
  • 构造了一个拦截链,在发送前,可以进行前置处理和后置处理。这个拦截链就是扩展的关键了。我们可以定义自己的拦截器,在发送消息前后进行拦截处理。这里spring给我们的扩展点。
  • 通过sendInternal将消息发送出去
    然后我们Debug看看这个sendInternal
    在这里插入图片描述
    看到有三个MessageHandler
  • WebSocketAnnotationMethodMessageHandler
  • SimpleBrokerMessageHandler
  • UserDestinationMessageHandler
    这里依次会调用这三个handler来发送消息。一般情况下,只会有一个handler来处理
    我们示例中发送的消息destination是/app/echo,对应着一个方法。 这里当然是WebSocketAnnotationMethodMessageHandler来处理了。
    这里封装成一个Task,执行其run方法。在executor不为空的时候,是异步发送的。

进入SendTask,看一下run方法

//
public void run() {Message<?> message = this.inputMessage;try {message = applyBeforeHandle(message);if (message == null) {return;}this.messageHandler.handleMessage(message);triggerAfterMessageHandled(message, null);}catch (Exception ex) {triggerAfterMessageHandled(message, ex);if (ex instanceof MessagingException) {throw (MessagingException) ex;}String description = "Failed to handle " + message + " to " + this + " in " + this.messageHandler;throw new MessageDeliveryException(message, description, ex);}catch (Throwable err) {String description = "Failed to handle " + message + " to " + this + " in " + this.messageHandler;MessageDeliveryException ex2 = new MessageDeliveryException(message, description, err);triggerAfterMessageHandled(message, ex2);throw ex2;}
}

这里的关键点是:this.messageHandler.handleMessage(message);
/app/echo会进入AbstractMethodMessageHandler

// AbstractMethodMessageHandler@Overridepublic void handleMessage(Message<?> message) throws MessagingException {String destination = getDestination(message);if (destination == null) {return;}String lookupDestination = getLookupDestination(destination);if (lookupDestination == null) {return;}MessageHeaderAccessor headerAccessor = MessageHeaderAccessor.getMutableAccessor(message);headerAccessor.setHeader(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, lookupDestination);headerAccessor.setLeaveMutable(true);message = MessageBuilder.createMessage(message.getPayload(), headerAccessor.getMessageHeaders());if (logger.isDebugEnabled()) {logger.debug("Searching methods to handle " +headerAccessor.getShortLogMessage(message.getPayload()) +", lookupDestination='" + lookupDestination + "'");}handleMessageInternal(message, lookupDestination);headerAccessor.setImmutable();}protected void handleMessageInternal(Message<?> message, String lookupDestination) {List<Match> matches = new ArrayList<>();List<T> mappingsByUrl = this.destinationLookup.get(lookupDestination);if (mappingsByUrl != null) {addMatchesToCollection(mappingsByUrl, message, matches);}if (matches.isEmpty()) {// No direct hits, go through all mappingsSet<T> allMappings = this.handlerMethods.keySet();addMatchesToCollection(allMappings, message, matches);}if (matches.isEmpty()) {handleNoMatch(this.handlerMethods.keySet(), lookupDestination, message);return;}Comparator<Match> comparator = new MatchComparator(getMappingComparator(message));matches.sort(comparator);if (logger.isTraceEnabled()) {logger.trace("Found " + matches.size() + " handler methods: " + matches);}Match bestMatch = matches.get(0);if (matches.size() > 1) {Match secondBestMatch = matches.get(1);if (comparator.compare(bestMatch, secondBestMatch) == 0) {Method m1 = bestMatch.handlerMethod.getMethod();Method m2 = secondBestMatch.handlerMethod.getMethod();throw new IllegalStateException("Ambiguous handler methods mapped for destination '" +lookupDestination + "': {" + m1 + ", " + m2 + "}");}}handleMatch(bestMatch.mapping, bestMatch.handlerMethod, lookupDestination, message);}
  • handleMessage 主要做一些消息的处理
  • handleMessageInternal就是关键点了。
    • 根据destination找到mappings,即我们注解中配置的url
    • 正常情况下,会找到一个匹配的url,这个url会对应一个method,调用下面的方法执行后续逻辑。handleMatch(bestMatch.mapping, bestMatch.handlerMethod, lookupDestination, message)
protected void handleMatch(T mapping, HandlerMethod handlerMethod, String lookupDestination, Message<?> message) {if (logger.isDebugEnabled()) {logger.debug("Invoking " + handlerMethod.getShortLogMessage());}handlerMethod = handlerMethod.createWithResolvedBean();InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod);if (this.handlerMethodLogger != null) {invocable.setLogger(this.handlerMethodLogger);}invocable.setMessageMethodArgumentResolvers(this.argumentResolvers);try {Object returnValue = invocable.invoke(message);MethodParameter returnType = handlerMethod.getReturnType();if (void.class == returnType.getParameterType()) {return;}if (returnValue != null && this.returnValueHandlers.isAsyncReturnValue(returnValue, returnType)) {ListenableFuture<?> future = this.returnValueHandlers.toListenableFuture(returnValue, returnType);if (future != null) {future.addCallback(new ReturnValueListenableFutureCallback(invocable, message));}}else {this.returnValueHandlers.handleReturnValue(returnValue, returnType, message);}}catch (Exception ex) {processHandlerMethodException(handlerMethod, ex, message);}catch (Throwable ex) {Exception handlingException =new MessageHandlingException(message, "Unexpected handler method invocation error", ex);processHandlerMethodException(handlerMethod, handlingException, message);}}

这里最重要的就是 invocable.invoke(message);。即调用反射来执行目标方法。这里代码之所以比较复杂,是处理入参和返回值。这里不是我们研究的重点。就不再分析了。

整个流程总结如下
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/9048.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

科林算法_3 图

一、图论基础 多对多的关系 定义&#xff1a;G(V,E) Vertex顶点 Edge边 顶点的集合V{v1,v2} 边的结合E{(v1,v2)} 无向图(1,2) 有向图<1,2> 依附&#xff1a;边(v1,v2)依附于顶点v1,v2 路径&#xff1a;&#xff08;v1,v2)(v2,v3) 无权路径最短&#xff1a;边最少…

程序员不会告诉老板的那些神器

目录 1. 持续集成工具&#xff1a;CruiseControl&#xff08;简称CC&#xff09; 2. 代码风格、质量检查工具&#xff1a;StyleCop 3.AI工具 3.1 AI助力编写开发日报 3.2 AI助力编写普适性代码 3.3 AI助力生成代码注释 3.4 AI助力重构代码去掉“坏味道” 3.5 AI助力…

【小白的大模型之路】基础篇:Transformer细节

基础篇&#xff1a;Transformer 引言模型基础架构原论文架构图EmbeddingPostional EncodingMulti-Head AttentionLayerNormEncoderDecoder其他 引言 此文作者本身对transformer有一些基础的了解,此处主要用于记录一些关于transformer模型的细节部分用于进一步理解其具体的实现机…

渗透之sql注入---宽字节注入

目录 宽字节注入原理&#xff1a; 实战&#xff1a; 源码分析&#xff1a; 开始注入&#xff1a; 找注入点&#xff1a; 注入数据库名&#xff1a; 注入表名&#xff1a; 注入列明&#xff1a; 注入具体值&#xff1a;http://sqli-labs:8084/less-32/?id-1%df%27unio…

luceda ipkiss教程 66:金属线的钝角转弯

案例分享&#xff1a;金属线的135度转弯&#xff1a; 所有代码如下&#xff1a; from si_fab import all as pdk import ipkiss3.all as i3 from ipkiss.geometry.shape_modifier import __ShapeModifierAutoOpenClosed__ from numpy import sqrtclass ShapeManhattanStub(__…

《ESP8266通信指南》11-Lua开发环境配置

往期 《ESP8266通信指南》10-MQTT通信&#xff08;Arduino开发&#xff09;-CSDN博客 《ESP8266通信指南》9-TCP通信&#xff08;Arudino开发&#xff09;-CSDN博客 《ESP8266通信指南》8-连接WIFI&#xff08;Arduino开发&#xff09;&#xff08;非常简单&#xff09;-CSD…

短信公司_供应群发短信公司

短信公司——供应群发短信公司 短信公司作为一种为企业提供群发短信服务的服务商&#xff0c;正逐渐受到市场的青睐。供应群发短信公司作为其中的一种类型&#xff0c;为各行各业的企业提供高效、便捷的短信推广渠道。本文将介绍短信公司的作用以及供应群发短信公司的特点和优势…

Django之创建Model以及后台管理

一&#xff0c;创建项目App python manage.py startapp App 二&#xff0c;在App.models.py中创建类&#xff0c;以下是示例 class UserModel(models.Model):uid models.AutoField(primary_keyTrue, auto_createdTrue)name models.CharField(max_length10, uniqueTrue, db…

ICode国际青少年编程竞赛- Python-2级训练场-坐标与列表练习

ICode国际青少年编程竞赛- Python-2级训练场-坐标与列表练习 1、 for i in range(6):Spaceship.step(Item[i].x - Spaceship.x)Dev.step(Item[i].y - Dev.y)Dev.step(Spaceship.y - Dev.y)2、 for i in range(5):Spaceship.step(Item[i].x - Spaceship.x)Flyer[i].step(Item[…

车载测试___面试题和答案归纳

车载面试题 一、实车还在设计开发阶段&#xff0c;大部分测试通过什么测试&#xff1f; 答案&#xff1a;通过台架和仿真来完成的 二、测试部分划分&#xff1f; 测试部门是分为自研&#xff0c;系统&#xff0c;验收&#xff0c;自研部门是开发阶段测试&#xff0c;系统部门…

重发被恶意举报的主食冻干测评,速看可能再被删!PR、希喂和SC真实对比PK!

要给猫咪提供高品质主食&#xff0c;主食冻干是不二之选。主食冻干不仅含肉量高、吸收消化率高&#xff0c;还有着丰富的、普通猫粮无法提供的各类营养素&#xff0c;满足猫咪微量元素的需求。可以说是营养与生骨肉喂养媲美&#xff0c;又能完美避开生骨肉细菌超标带来的一系列…

如何优雅的实现接口限流?

首先限流&#xff0c;其实解决方案有很多&#xff0c;比如通过nginx配置&#xff0c;通过gateway网关进行限流&#xff0c;比如Spring Cloud GateWay整合熔断器实现限流 但是以上都是全局的&#xff0c;如何灵活的针对某些接口进行不同级别的限流呢&#xff1f; 方案一&#…

超标量处理器设计:重排序缓存(ROB)

★超标量处理器的很多地方用到了重排序缓存&#xff0c;但是我对它不是很了解&#xff0c;所以我整理一下重排序缓存的知识点。 重排序缓存(ROB)在确保乱序执行的指令能够正确地完成和提交(Commit)&#xff0c;也可以用来寄存器重命名。 ROB是一个先进先出的表&#xff0c;每个…

Re_Lasso

from sklearn.linear_model import LassoCV, Lasso import pandas as pd from sklearn.model_selection import train_test_split from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score from sklearn.model_selection import GridSearchCV# 读取数据…

【备战软考(嵌入式系统设计师)】10 - 软件工程基础

这一部分的内容是概念比较多&#xff0c;不要理解&#xff0c;去感受。 涉及的知识点是嵌入式系统开发和维护的部分&#xff0c;也就是和管理相关的&#xff0c;而不是具体如何进行嵌入式系统开发的细节。 系统开发生命周期 按照顺序有下面几个阶段&#xff0c;我们主要要记…

12 华三的二层链路聚合

12 华三的二层链路聚合 配置思路 1. 配置二层静态聚合组 (1) 进入系统视图。 system-view (2) 创建二层聚合接口&#xff0c;并进入二层聚合接口视图。 interface bridge-aggregation interface-number [ lite ] 创建二层聚合接口后&#xff0c;系统将自动生成…

代码随想录算法训练营DAY46|C++动态规划Part8|139.单词拆分、多重背包理论基础、背包问题总结篇

文章目录 139.单词拆分思路CPP代码 多重背包理论基础处理输入把所有个数大于1的物品展开成1个开始迭代&#xff0c;计算dp数组代码优化 背包问题总结篇 139.单词拆分 力扣题目链接 文章讲解&#xff1a;139.单词拆分 视频讲解&#xff1a;你的背包如何装满&#xff1f;| LeetCo…

计算方法实验9:Romberg积分求解速度、位移

任务 输出质点的轨迹 ( x ( t ) , y ( t ) ) , t ∈ { 0.1 , 0.2 , 0.3 , . . . , 10 } (x(t), y(t)), t\in \{0.1, 0.2, 0.3, ..., 10\} (x(t),y(t)),t∈{0.1,0.2,0.3,...,10}&#xff0c;并在二维平面中画出该轨迹.请比较M分别取4, 8, 12, 16, 20 时&#xff0c;Romberg积分达…

蓝桥杯省三爆改省二,省一到底做错了什么?

到底怎么个事 这届蓝桥杯选的软件测试赛道&#xff0c;都说选择大于努力,软件测试一不卷二不难。省赛结束&#xff0c;自己就感觉稳啦&#xff0c;全部都稳啦。没想到一出结果&#xff0c;省三&#xff0c;g了。说落差&#xff0c;是真的有一点&#xff0c;就感觉和自己预期的…

汽车软件研发工具链丨怿星科技新产品重磅发布

“创新引领未来”聚焦汽车软件新基建&#xff0c;4月27日下午&#xff0c;怿星科技2024新产品发布会在北京圆满举行&#xff01;智能汽车领域的企业代表、知名大企业负责人、投资机构代表、研究机构代表齐聚现场&#xff0c;线上直播同步开启&#xff0c;共同见证怿星科技从单点…