踩坑系列 Spring websocket并发发送消息异常

news2024/11/25 16:24:49

文章目录

    • 示例代码
      • WebSocketConfig配置代码
      • 握手拦截器代码
      • 业务处理器代码
    • 问题复现
    • 原因分析
    • 解决方案
      • 方案一 加锁同步发送
      • 方案二 使用ConcurrentWebSocketSessionDecorator
      • 方案三 自研事件驱动队列(借鉴 Tomcat)
    • 总结

今天刚刚经历了一个坑,非常新鲜,我立刻决定记录下来。首先,让我们先看一下我们项目中使用的 Spring WebSocket 示例代码。

示例代码

在我们的项目中,我们使用了 Spring WebSocket 来实现服务器与客户端之间的实时通信。下面是一个简化的示例代码:

WebSocketConfig配置代码

@Configuration
@EnableWebSocket // 启动Websocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/myHandler/**")
            // 添加拦截器,可以获取连接的param和 header 用作认证鉴权
            .addInterceptors(new LakerSessionHandshakeInterceptor())
            // 设置运行跨域
            .setAllowedOrigins("*");
    }
       
    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        // 设置默认会话空闲超时 以毫秒为单位 非正值意味着无限超时,默认值 0 ,默认没10s检查一次空闲就关闭
        container.setMaxSessionIdleTimeout(10 * 1000L);
        // 设置异步发送消息的默认超时时间 以毫秒为单位 非正值意味着无限超时 ,默认值-1,还没看到作用
        container.setAsyncSendTimeout(10 * 1000L);
        // 设置文本消息的默认最大缓冲区大小 以字符为单位,默认值 8 * 1024
        container.setMaxTextMessageBufferSize(8 * 1024);
        // 设置二进制消息的默认最大缓冲区大小 以字节为单位,默认值 8 * 1024
        container.setMaxBinaryMessageBufferSize(8 * 1024);
        return container;
    }

    @Bean
    public WebSocketHandler myHandler() {
        return new MyHandler();
    }

}

握手拦截器代码

public class LakerSessionHandshakeInterceptor extends HttpSessionHandshakeInterceptor {
    // 拦截器返回false,则不会进行websocket协议的转换
    // 可以获取请求参数做认证鉴权
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
        // 将所有查询参数复制到 WebSocketSession 属性映射
        Enumeration<String> parameterNames = servletRequest.getParameterNames();
        while (parameterNames.hasMoreElements()) {
            String parameterName = parameterNames.nextElement();
            // 放入的值可以从WebSocketHandler里面的WebSocketSession拿出来
            attributes.put(parameterName, servletRequest.getParameter(parameterName));
        }
        if (servletRequest.getHeader(HttpHeaders.AUTHORIZATION) != null) {
            setErrorResponse(response, HttpStatus.UNAUTHORIZED);
            return false;
        }
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {

    }
    private void setErrorResponse(ServerHttpResponse response, HttpStatus status, String errorMessage) {
        response.setStatusCode(status);
        if (errorMessage != null) {
            try {
                objectMapper.writeValue(response.getBody(), new ErrorDetail(status.value(), errorMessage));
            } catch (IOException ioe) {
            }
        }
    }
}

业务处理器代码

public class MyHandler extends AbstractWebSocketHandler {
     private final Map<String, WebSocketSession> webSocketSessionMap = new ConcurrentHashMap<>();
 
    //成功连接时
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        super.afterConnectionEstablished(session);
        // 设置最大报文大小,如果报文过大则会报错的,可以将限制调大。
        // 会覆盖config中的配置。
        session.setBinaryMessageSizeLimit(8 * 1024);
        session.setTextMessageSizeLimit(8 * 1024);
        webSocketSessionMap.put(session.getId(), session);
    }

    //处理textmessage
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        super.handleTextMessage(session, message);
        // 有消息就广播下
        for (Map.Entry<String, WebSocketSession> entry : webSocketSessionMap.entrySet()) {
            String s = entry.getKey();
            WebSocketSession webSocketSession = entry.getValue();
            if (webSocketSession.isOpen()) {
            	webSocketSession.sendMessage(new TextMessage(s + ":" + message.getPayload()));
            	log.info("send to {} msg:{}", s, message.getPayload());
            }
        }
    }
    
    
    //报错时
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        super.handleTransportError(session, exception);
          log.warn("handleTransportError:: sessionId: {} {}", session.getId(), exception.getMessage(), exception);
        if (session.isOpen()) {
            try {
                session.close();
            } catch (Exception ex) {
                // ignore
            }
        }
    }
    
   //连接关闭时
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        super.afterConnectionClosed(session, status);
        WebSocketSession session =  webSocketSessionMap.remove(session.getId());
        if (session.isOpen()) {
            try {
                session.close();
            } catch (Exception ex) {
                // ignore
            }
        }
    }
    
    //处理binarymessage
    @Override
    protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
        super.handleBinaryMessage(session, message);
    }

    //处理pong
    @Override
    protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
        super.handlePongMessage(session, message);
    }

    //是否支持报文拆包
    // 决定是否接受半包,因为websocket协议比较底层,好像Tcp协议一样,如果发送大消息可能会拆成多个小报文。如果不希望处理不完整的报文,希望底层帮忙聚合成完整消息将此方法返回false,这样底层会等待完整报文到达聚合后才回调。
    @Override
    public boolean supportsPartialMessages() {
        return super.supportsPartialMessages();
    }
}

问题复现

在我们的测试环境中,我们发现当多个线程同时尝试通过 WebSocket 会话发送消息时,会抛出异常。

现在我们用JMeter模拟100个用户发消息。

当执行一会儿后会发现服务端出现如下异常日志:

java.lang.IllegalStateException: 远程 endpoint 处于 [TEXT_PARTIAL_WRITING] 状态,是被调用方法的无效状态
	at org.apache.tomcat.websocket.WsRemoteEndpointImplBase$StateMachine.checkState(WsRemoteEndpointImplBase.java:1274)
	at org.apache.tomcat.websocket.WsRemoteEndpointImplBase$StateMachine.textPartialStart(WsRemoteEndpointImplBase.java:1231)
	at org.apache.tomcat.websocket.WsRemoteEndpointImplBase.sendPartialString(WsRemoteEndpointImplBase.java:226)
	at org.apache.tomcat.websocket.WsRemoteEndpointBasic.sendText(WsRemoteEndpointBasic.java:49)
	at org.springframework.web.socket.adapter.standard.StandardWebSocketSession.sendTextMessage(StandardWebSocketSession.java:215)
	at org.springframework.web.socket.adapter.AbstractWebSocketSession.sendMessage(AbstractWebSocketSession.java:108)

原因分析

经过分析,发现异常的根本原因是在并发发送消息时,WebSocket 会话的状态发生了异常。

具体来说,当一个线程正在发送文本消息时,另一个线程也尝试发送消息,就会导致状态不一致,从而触发异常。

WebSocketSession.sendMessage不是线程安全的,内部有个状态机来管理防止并发导致问题以fail-fast方式快速告诉使用者。

那么问题代码就在这里了

WebSocketSession是不支持并发发送消息的,我们使用者要保证其线程安全,这是我一开始没预期到的

解决方案

方案一 加锁同步发送

为了解决并发发送消息导致的异常,我们可以在发送消息的代码块上加锁,确保同一时刻只有一个线程能够执行发送操作。

下面是使用加锁机制的示例代码:

改起来非常简单,只需要对webSocketSession实例加个锁即可。

缺点

当并发度较高时,越后面排队等待锁的人被block的越久。

大致模型图

方案二 使用ConcurrentWebSocketSessionDecorator

另一种解决并发发送消息的方法是使用 ConcurrentWebSocketSessionDecorator。这是 Spring WebSocket 提供的一个装饰器类,用于增强底层的 WebSocketSession 的线程安全性。它通过并发安全的方式包装原始的 WebSocketSession 对象,确保在多线程环境下安全地访问和修改会话属性,以及进行消息发送操作。

ConcurrentWebSocketSessionDecorator 的工作原理是利用线程安全的数据结构和同步机制,确保对会话执行的操作的原子性和一致性。当需要发送消息时,装饰器会获取锁或使用并发数据结构来协调多个线程之间的访问。这样可以防止对会话状态的并发修改,避免潜在的竞态条件。

下面是使用 ConcurrentWebSocketSessionDecorator 的示例代码:

    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        log.info("{} Connection established!", session.getId());
        // webSocketSessionMap.put(session.getId(), session);
        // 把线程安全的session代理装饰类放到容器里
        webSocketSessionMap.put(session.getId(), new ConcurrentWebSocketSessionDecorator(session, 10 * 1000, 64000));
    }

ConcurrentWebSocketSessionDecorator原理

ConcurrentWebSocketSessionDecorator整体代码大概200行还是比较容易看懂的。

// 包装一个WebSocketSession以保证一次只有一个线程可以发送消息。
// 如果发送速度慢,后续尝试从其他线程发送更多消息将无法获取刷新锁,消息将被缓冲。届时,将检查指定的缓冲区大小限制和发送时间限制,如果超过限制,会话将关闭。
public class ConcurrentWebSocketSessionDecorator extends WebSocketSessionDecorator {
	private static final Log logger = LogFactory.getLog(ConcurrentWebSocketSessionDecorator.class);

	private final int sendTimeLimit;

	private final int bufferSizeLimit;

	private final OverflowStrategy overflowStrategy;

	@Nullable
	private Consumer<WebSocketMessage<?>> preSendCallback;

	private final Queue<WebSocketMessage<?>> buffer = new LinkedBlockingQueue<>();

	private final AtomicInteger bufferSize = new AtomicInteger();

	private volatile long sendStartTime;

	private volatile boolean limitExceeded;

	private volatile boolean closeInProgress;

	private final Lock flushLock = new ReentrantLock();

	private final Lock closeLock = new ReentrantLock();

	public ConcurrentWebSocketSessionDecorator(WebSocketSession delegate, int sendTimeLimit, int bufferSizeLimit) {
		this(delegate, sendTimeLimit, bufferSizeLimit, OverflowStrategy.TERMINATE);
	}
    // delegate 需要代理的session
    // sendTimeLimit 表示发送**单个消息**的最大时间
    // bufferSizeLimit 表示发送消息的队列最大字节数,不是消息的数量而是消息的总大小
    // overflowStrategy 表示超过限制时的策略有2个
                 // - 断开连接(默认选项)
                 // - 丢弃最老的数据,直到大小满足
	public ConcurrentWebSocketSessionDecorator(
			WebSocketSession delegate, int sendTimeLimit, int bufferSizeLimit, OverflowStrategy overflowStrategy) {

		super(delegate);
		this.sendTimeLimit = sendTimeLimit;
		this.bufferSizeLimit = bufferSizeLimit;
		this.overflowStrategy = overflowStrategy;
	}


    // 返回自当前发送开始以来的时间(毫秒),如果当前没有发送正在进行则返回 0。
    // 即花费的耗时
	public long getTimeSinceSendStarted() {
		long start = this.sendStartTime;
		return (start > 0 ? (System.currentTimeMillis() - start) : 0);
	}

	// 设置在将消息添加到发送缓冲区后调用的回调
	public void setMessageCallback(Consumer<WebSocketMessage<?>> callback) {
		this.preSendCallback = callback;
	}


	@Override
	public void sendMessage(WebSocketMessage<?> message) throws IOException {
        //检查超限了就不发了
		if (shouldNotSend()) {
			return;
		}
		// 消息放到buffer队列
		this.buffer.add(message);
        // 增加bufferSize用于后面判断是不是超限了
		this.bufferSize.addAndGet(message.getPayloadLength());
		// 发送缓冲区后调用的回调
		if (this.preSendCallback != null) {
			this.preSendCallback.accept(message);
		}

		do {
            // 尝试获取锁,发送消息,只有一个线程负责发送所有消息
			if (!tryFlushMessageBuffer()) {
				// 没获取锁的线程,对当前buffer和时间检查,
                // 检查不过就抛异常 然后框架自己会抓取异常关闭当前的连接
				checkSessionLimits();
				break;
			}
		}
		while (!this.buffer.isEmpty() && !shouldNotSend());
	}
	// 超限了 不能发送了
	private boolean shouldNotSend() {
		return (this.limitExceeded || this.closeInProgress);
	}
	// 尝试获取锁 并发送所有缓存的消息
	private boolean tryFlushMessageBuffer() throws IOException {
		if (this.flushLock.tryLock()) {
			try {
                 // 循环发送消息
				while (true) {
                    // 一次拉一个消息
					WebSocketMessage<?> message = this.buffer.poll();
                    // 没消息了 或者 超限了
					if (message == null || shouldNotSend()) {
                        // 退出 完活
						break;
					}
                    // 释放bufferSize
					this.bufferSize.addAndGet(-message.getPayloadLength());
                    // 用于判断单个消息是否发送超时的
					this.sendStartTime = System.currentTimeMillis();
                    // 发送消息
					getDelegate().sendMessage(message);
                    // 重置开始时间
					this.sendStartTime = 0;
				}
			}
			finally {
				this.sendStartTime = 0;
				this.flushLock.unlock();
			}
			return true;
		}
		return false;
	}
   //检查是否超时,是否超过buffer限制
	private void checkSessionLimits() {
        // 应该发送 且 获取到关闭锁
		if (!shouldNotSend() && this.closeLock.tryLock()) {
			try {
                 //检测是否发送超时
				if (getTimeSinceSendStarted() > getSendTimeLimit()) {
					String format = "Send time %d (ms) for session '%s' exceeded the allowed limit %d";
					String reason = String.format(format, getTimeSinceSendStarted(), getId(), getSendTimeLimit());
					limitExceeded(reason);
				}
                 //检测buffer大小,根据策略要么抛异常关闭连接,要么丢弃数据
				else if (getBufferSize() > getBufferSizeLimit()) {
					switch (this.overflowStrategy) {
                        // 关闭连接,抛出异常框架就会关闭连接    
						case TERMINATE:
							String format = "Buffer size %d bytes for session '%s' exceeds the allowed limit %d";
							String reason = String.format(format, getBufferSize(), getId(), getBufferSizeLimit());
							limitExceeded(reason);
							break;
                        // 丢弃老数据    
						case DROP:
							int i = 0;
							while (getBufferSize() > getBufferSizeLimit()) {
								WebSocketMessage<?> message = this.buffer.poll();
								if (message == null) {
									break;
								}
								this.bufferSize.addAndGet(-message.getPayloadLength());
								i++;
							}
							if (logger.isDebugEnabled()) {
								logger.debug("Dropped " + i + " messages, buffer size: " + getBufferSize());
							}
							break;
						default:
							// Should never happen..
							throw new IllegalStateException("Unexpected OverflowStrategy: " + this.overflowStrategy);
					}
				}
			}
			finally {
				this.closeLock.unlock();
			}
		}
	}

	private void limitExceeded(String reason) {
		this.limitExceeded = true;
		throw new SessionLimitExceededException(reason, CloseStatus.SESSION_NOT_RELIABLE);
	}

	@Override
	public void close(CloseStatus status) throws IOException {
		this.closeLock.lock();
		try {
			if (this.closeInProgress) {
				return;
			}
			if (!CloseStatus.SESSION_NOT_RELIABLE.equals(status)) {
				try {
					checkSessionLimits();
				}
				catch (SessionLimitExceededException ex) {
					// Ignore
				}
				if (this.limitExceeded) {
					if (logger.isDebugEnabled()) {
						logger.debug("Changing close status " + status + " to SESSION_NOT_RELIABLE.");
					}
					status = CloseStatus.SESSION_NOT_RELIABLE;
				}
			}
			this.closeInProgress = true;
			super.close(status);
		}
		finally {
			this.closeLock.unlock();
		}
	}

	public enum OverflowStrategy {
		TERMINATE,
		DROP
	}

}

大致模型图

方案三 自研事件驱动队列(借鉴 Tomcat)

除了使用加锁和 ConcurrentWebSocketSessionDecorator,我们还可以借鉴 Tomcat 的事件驱动队列机制来解决并发发送消息的问题。具体的实现需要一些复杂的逻辑和代码,涉及到消息队列、线程池和事件处理机制,因此在这里我不展开讨论。如果你对这个方案感兴趣,可以参考 Tomcat 的源代码,了解更多关于事件驱动队列的实现细节。

这个参考之前的tomcat设计即可。

img

总结

在本篇博文中,我们讨论了在使用 Spring WebSocket 进行并发发送消息时可能遇到的异常情况。我们深入分析了异常的原因,并提供了三种解决方案:加锁同步发送、使用 ConcurrentWebSocketSessionDecorator 和自研事件驱动队列(借鉴 Tomcat)。每种方案都有其适用的场景和注意事项,你可以根据自己的需求选择合适的方法来解决并发发送消息的异常问题。

希望本文对你有所帮助,让你在使用 Spring WebSocket 时能够避免类似的坑。如果你对本文有任何疑问或意见,欢迎在评论区留言,我们将尽力为你解答。谢谢阅读!

参考链接:

  • Spring Framework Documentation: WebSocket
  • Tomcat Documentation: Event-driven Architectures

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

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

相关文章

云原生之使用Docker部署wordpress网站

云原生之使用Docker部署wordpress网站 一、wordpress介绍二、检查本地docker环境1.检查docker状态2.检查docker版本 三、下载wordpress镜像四、创建数据库1.创建数据目录2.创建mysql数据库容器3.查看mysql容器状态4.远程客户端测试连接数据库 五、部署wordpress1.创建wordpress…

java开发——程序性能的优化方法

java开发——程序性能的优化方法 1、算法逻辑优化2、redis缓存优化3、异步编排4、MQ削峰填谷5、预加载6、sql调优7、jvm调优8、集群搭建 后端开发必须学习的优化思想&#xff01; 1、算法逻辑优化 (1) 什么算法优化&#xff1f; 算法优化的思想就是&#xff0c;对于同一个问题…

ll 内容详解

linux的数据存储是以block&#xff08;块&#xff09;为单位的 &#xff1a; 1个block 4 KB 4096 字节 1KB 1024 字节[rootCTF-RHCSA-2 ~]# ll -sh total 76K &#xff08;列表中 所有文本文件 总共占用磁盘空间的KB大小 &#xff09;&#xff08;root用户家目录中…

cortex A7核按键中断实验

cortex A7核按键中断实验 一、分析电路图 实验目的&#xff1a;完成板子三个按键操作 1.1 电路图IO口 KEY1------>PF9 KEY2------>PF7 KEY3------>PF8 1.2 工作原理 KEY1 ------> PF9 ------> 按键触发方式&#xff1a;下降沿触发 KEY2 ------> PF7 …

ElasticSearch安装与介绍

目录 ElasticSearch安装与介绍Elastic Stack简介ElasticsearchLogstashKibanaBeats ElasticSearch快速入门简介下载单机版安装启动ElasticSearch 错误分析错误情况1错误情况2错误情况3 ElasticSearch-Head可视化工具通过Docker方式安装通过Chrome插件安装 ElasticSearch中的基本…

docker部署prometheus+grafana+alertmanager+dingtalk实现钉钉告警

目录 docker安装准备工作镜像拉取容器启动启动node-exporter启动prometheus启动grafana启动webhook-prometheus-dingtalk启动alertmanager所有容器启动成功如下 将prometheus和alertmanager进行关联在prometheus目录下新建一个rules.yml文件的告警规则修改prometheus.yml文件&a…

NLP学习笔记十二-skip-gram模型求解

NLP学习笔记十一-skip-gram模型求解 上一篇文章&#xff0c;我们见到了skip-gram模型的原理&#xff0c;这里我们在陈述一下skip-gram模型其实是基于分布相似性原理来设计的&#xff0c;在skip-gram模型中&#xff0c;他认为一个词的内涵可以由他的上下文文本信息来概括&#…

AcWing 837. 连通块中点的数量

题目如下&#xff1a; 给定一个包含 n个点&#xff08;编号为 1∼ n&#xff09;的无向图&#xff0c;初始时图中没有边。 现在要进行 m 个操作&#xff0c;操作共有三种&#xff1a; C a b&#xff0c;在点 a和点 b之间连一条边&#xff0c;a 和 b 可能相等&#xff1b;Q1 …

【iMessage苹果日历推真机群控】使用虚拟化平台创建一个 macOS 虚拟机

PC 虚拟机上部署群控推送并模拟苹果 iMessage 推送消息是比较复杂的任务。由于苹果的 iMessage 服务是基于苹果设备和操作系统的&#xff0c;模拟 iMessage 推送需要考虑苹果的生态系统和安全机制。 以下是一种可能的方法&#xff0c;但请注意这是一个高级设置&#xff0c;需要…

chatgpt赋能python:Python编程中的放大代码技巧

Python 编程中的放大代码技巧 Python 是一门广泛应用于各个领域的编程语言。由于它易学易用、可移植性好、开发效率高等特点&#xff0c;使其在人工智能、大数据分析、网站开发等领域被广泛应用。在实际编程中&#xff0c;随着代码量的增加&#xff0c;需要更好地组织和管理代…

BFS 广度优先搜索

广度优先搜索BFS&#xff08;Breadth First Search&#xff09;也称为宽度优先搜索&#xff0c;它是一种先生成的结点先扩展的策略&#xff0c;类似于树的层次遍历。 在广度优先搜索算法中&#xff0c;解答树上结点的扩展是按它们在树中的层次进行的。首先生成第一层结点&#…

TS系列之keyof详解,示例

文章目录 前言一、keyof是什么总结 前言 如果你用过TS的工具类型&#xff0c;Partial、Required、Pick、Record。那么你可能看过他们内部实现都有共同点就是keyof关键字。即使没有见过&#xff0c;那么下面就一起来了解一下&#xff0c;keyof关键字的详细作用吧。 一、keyof是…

Filebeat详细介绍,下载和启动,日志读取和模块设置等

目录 Filebeat介绍为什么要用Filebeat&#xff1f;架构下载启动读取文件自定义字段输出到ElasticSearch Filebeat工作原理harvesterprospectorinput启动命令参数说明 部署Nginx读取Nginx中的配置文件Modulenginx module 配置配置filebeat测试错误1错误2 Filebeat 介绍 Filebe…

Java开发中的常见问题和解决方法:如何解决常见的性能和bug问题

章节一&#xff1a;引言 在Java开发中&#xff0c;我们经常会面临各种各样的问题&#xff0c;包括性能问题和Bug。这些问题可能会导致应用程序的运行变慢、不稳定甚至崩溃。本文将介绍一些常见的Java开发问题&#xff0c;并提供解决这些问题的方法和技巧&#xff0c;帮助开发人…

ElasticSearch集群8.0版本搭建、故障转移

目录 ElasticSearch集群集群节点搭建集群分片和副本 故障转移将data节点停止将master节点停止 分布式文档路由文档的写操作 搜索文档全文搜索搜索&#xff08;query&#xff09;取回 fetch ElasticSearch集群 集群节点 ELasticsearch的集群是由多个节点组成的&#xff0c;通过…

SSM学习记录9:SpringBoot整合SSM(注解方式)

SSM学习记录9&#xff1a;SpringBoot整合SSM&#xff08;注解方式&#xff09; 1.首先创建新项目&#xff0c;选择Spring Initializr&#xff0c;type为Maven 2.接着依赖选择Spring Web 3.无需繁琐配置&#xff0c;即可运行编写的controller类 启动SpringBootDemoApplication↓…

利用人工智能模型学习Python爬虫

爬虫是一种按照一定的规则&#xff0c;自动地抓取万维网信息的程序或者脚本。 网络爬虫(又称为网页蜘蛛&#xff0c;网络机器人)是其中一种类型。 爬虫可以自动化浏览网络中的信息&#xff0c;当然浏览信息的时候需要按照我们制定的规则进行&#xff0c;这些规则我们称之为网络…

UE4/5样条线学习(一):基础的样条线使用

目录 效果展示&#xff1a; 制作&#xff1a; 组件 逻辑 效果展示&#xff1a; 注&#xff1a;按住alt拉轴可以拉出多一个点 制作&#xff1a; 第一步我们创建一个蓝图&#xff0c;命名为BP_Sline&#xff1a; 组件 之后我们开始找组件&#xff0c;输入bill&#xff0c;我…

使用 docker 创建 mongodb 副本集, 和调整副本集优先级

mongod 本地创建副本集 mongod --port 27017 --dbpath /srv/mongodb/db0 --replSet rs0 --bind_ip localhost,<hostname(s)|ip address(es)> –dbpath 指向数据存放地址 –replSet 后面为 副本集的名。 rs.initiate() 启动新的副本集 rs.conf() 查看副本集的配置 rs.stat…

chatgpt赋能python:Python实现多关键词搜索PDF文件

Python实现多关键词搜索PDF文件 概述 在今天的数字化社会中&#xff0c;很多信息都以数字化的形式存储在PDF文件中。这让我们在搜索特定信息时面临很多挑战&#xff0c;特别是当我们需要同时搜索多个PDF文件并集中检索这些文件时。 在这篇文章中&#xff0c;我们将介绍如何使…