SpringBoot整合Netty整合WebSocket-带参认证

news2024/11/27 20:36:19

文章目录

    • 一. VectorNettyApplication启动类配置
    • 二.WebSocketServerBoot初始化服务端Netty
    • 三. WebsocketServerChannelInitializer初始化服务端Netty读写处理器
    • 四.initParamHandler处理器-去参websocket识别
    • 五.MessageHandler核心业务处理类-采用工厂策略模式
      • 5.1 策略上下文
    • 六.统一响应
    • 七.统一输出处理器


一. VectorNettyApplication启动类配置

初始化SpringBoot线程同时初始化Netty线程

/**
 * @description: 通知启动类
 * @Title: VectorNotification
 * @Package com.vector.notification
 * @Author YuanJie
 * @Date 2023/3/2 12:57
 */
@EnableDiscoveryClient // 开启服务注册与发现
@SpringBootApplication(scanBasePackages = {"com.vector"},exclude = {DataSourceAutoConfiguration.class}) // 开启组件扫描和自动配置
public class VectorNettyApplication implements CommandLineRunner {

    @Value("${netty.host}")
    private String host;
    @Value("${netty.port}")
    private Integer port;
    @Resource
    private WebSocketServerBoot webSocketServerBoot;


    public static void main(String[] args) {
        SpringApplication.run(VectorNettyApplication.class, args);
    }


    // springboot启动后执行netty服务端启动
    @Override
    public void run(String... args) throws Exception {
        ChannelFuture channelFuture = webSocketServerBoot.bind(host, port);
        // 优雅关闭, jvm关闭时将netty服务端关闭
        Runtime.getRuntime().addShutdownHook(new Thread(() -> webSocketServerBoot.destroy()));
        // 阻塞 直到channel关闭
        channelFuture.channel().closeFuture().syncUninterruptibly();
    }
}

二.WebSocketServerBoot初始化服务端Netty

主要进行netty的基本配置

/**
 * @author YuanJie
 * @projectName vector-server
 * @package com.vector.netty.accept
 * @className com.vector.netty.accept.ServerBootstrap
 * @copyright Copyright 2020 vector, Inc All rights reserved.
 * @date 2023/6/9 18:34
 */
@Component
@Slf4j
public class WebSocketServerBoot {
    private final EventLoopGroup parentGroup = new NioEventLoopGroup();
    private final EventLoopGroup childGroup = new NioEventLoopGroup(2);
    private Channel channel;

    @Resource
    private WebsocketServerChannelInitializer websocketServerChannelInitializer;

    /**
     * 初始化服务端
     * sync():等待Future直到其完成,如果这个Future失败,则抛出失败原因;
     * syncUninterruptibly():不会被中断的sync();
     */
    public ChannelFuture bind(String host, Integer port) {
        ChannelFuture channelFuture = null;
        try {
            channelFuture = new ServerBootstrap()
                    .group(parentGroup, childGroup) // 指定线程模型 一个用于接收客户端连接,一个用于处理客户端读写操作
                    .channel(NioServerSocketChannel.class) // 指定服务端的IO模型
                    .option(ChannelOption.SO_BACKLOG, 1024) // 设置TCP缓冲区
                    .childOption(ChannelOption.SO_KEEPALIVE, true) // 保持连接 tcp底层心跳机制
                    .childHandler(websocketServerChannelInitializer) // 指定处理新连接数据的读写处理逻辑
                    .bind(host, port)
                    .addListener(new GenericFutureListener<Future<? super Void>>() {
                        @Override
                        public void operationComplete(Future<? super Void> future) throws Exception {
                            if (future.isSuccess()) {
                                log.info("服务端启动成功,监听端口:{}", port);
                            } else {
                                log.error("服务端启动失败,监听端口:{}", port);
                                bind(host, port + 1);
                            }
                        }
                    })
                    .syncUninterruptibly();// 绑定端口
            channel = channelFuture.channel(); // 获取channel
        } finally {
            if (null == channelFuture) {
                channel.close();
                parentGroup.shutdownGracefully();
                childGroup.shutdownGracefully();
            }
        }
        return channelFuture;
    }


    /**
     * 销毁
     */
    public void destroy() {
        if (null == channel) return;
        channel.close();
        parentGroup.shutdownGracefully();
        childGroup.shutdownGracefully();
    }

    /**
     * 获取通道
     *
     * @return
     */
    public Channel getChannel() {
        return channel;
    }
}

三. WebsocketServerChannelInitializer初始化服务端Netty读写处理器

主要规划netty的读写处理器

/**
 * @author YuanJie
 * @projectName vector-server
 * @package com.vector.netty.config
 * @className com.vector.netty.server.ServerChannelInitializer
 * @copyright Copyright 2020 vector, Inc All rights reserved.
 * @date 2023/6/9 19:13
 */
@Component
public class WebsocketServerChannelInitializer extends ChannelInitializer<SocketChannel> {
    // @Sharable
    private final LoggingHandler loggingHandler = new LoggingHandler(LogLevel.INFO);
    public final static String WEBSOCKET_PATH = "/ws";
    @Resource
    private InitParamHandler initParamHandler;
    @Resource
    private MessageHandler messageHandler;
    @Resource
    private OutBoundHandler outBoundHandler;
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        // 日志打印
        pipeline.addLast(loggingHandler);
        // http报文解析器 线程不安全不能被共享
        pipeline.addLast(new HttpServerCodec());
//        // 添加对大数据流的支持
        pipeline.addLast(new ChunkedWriteHandler());
//        // 消息聚合器 8192 8M
        pipeline.addLast(new HttpObjectAggregator(1 << 13));
        // 进行设置心跳检测
        pipeline.addLast(new IdleStateHandler(60, 30, 60 * 30, TimeUnit.SECONDS));
        // ================= 上述是用于支持http协议的 ==============
        //websocket 服务器处理的协议,用于给指定的客户端进行连接访问的路由地址
        // 处理uri参数 WebSocketServerProtocolHandler不允许带参数 顺序不可调换
        pipeline.addLast(initParamHandler);
        pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH,null, true, 1<<16,true,true,5000));
        pipeline.addLast(messageHandler);
        // 自定义出栈处理器
        pipeline.addLast(outBoundHandler);
    }
}

四.initParamHandler处理器-去参websocket识别

主要为了去参,WebSocketServerProtocolHandler不允许带参数,同时初始化一些信道用户数据

/**
 * URL参数处理程序,这时候连接还是个http请求,没有升级成webSocket协议,此处SimpleChannelInboundHandler泛型使用FullHttpRequest
 *
 * @author YuanJie
 * @date 2023/5/7 15:07
 */
@Slf4j
@ChannelHandler.Sharable
@Component
public class InitParamHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    /**
     * 存储已经登录用户的channel对象
     */
    public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 存储用户id和用户的channelId绑定
     */
    public static final Map<Long, ChannelId> userMap = new ConcurrentHashMap<>();
    /**
     * 用于存储群聊房间号和群聊成员的channel信息
     */
    public static final Map<Long, ChannelGroup> groupMap = new ConcurrentHashMap<>();
    @DubboReference
    private MemberRemote memberRemote;


    /**
     * 此处进行url参数提取,重定向URL,访问webSocket的url不支持带参数的,带参数会抛异常,这里先提取参数,将参数放入通道中传递下去,重新设置一个不带参数的url
     *
     * @param ctx     the {@link ChannelHandlerContext} which this {@link SimpleChannelInboundHandler}
     *                belongs to
     * @param request the message to handle
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        if (!this.acceptInboundMessage(request)) {
            ctx.fireChannelRead(request.retain());
        }
        String uri = request.uri();
        log.info("NettyWebSocketParamHandler.channelRead0 --> : 格式化URL... {}", uri);
        Map<CharSequence, CharSequence> queryMap = UrlBuilder.ofHttp(uri).getQuery().getQueryMap();
        //将参数放入通道中传递下去
        String senderId = "senderId";
        if (StringUtils.isBlank(queryMap.get(senderId))) {
            log.info("NettyWebSocketParamHandler.channelRead0 --> : 参数缺失 senderId");
            ctx.close();
        }
        // 验证token
//        verifyToken(ctx,senderId);
        // 初始化数据
//        initData(ctx, Long.valueOf(queryMap.get(senderId).toString()));
        // 获取?之前的路径
        request.setUri(WebsocketServerChannelInitializer.WEBSOCKET_PATH);
        ctx.fireChannelRead(request.retain());
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        //添加到channelGroup通道组
        channelGroup.add(ctx.channel());
        ctx.channel().id();
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        // 移除channelGroup 通道组
        channelGroup.remove(ctx.channel());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        log.error("NettyWebSocketParamHandler.exceptionCaught --> cause: ", cause);
        ctx.close();
    }

    private void verifyToken(ChannelHandlerContext ctx, Long senderId) {
        String userKey = CacheConstants.LOGIN_TOKEN_KEY + senderId;
        RedissonCache redissonCache = SpringContextUtil.getBean(RedissonCache.class);
        Boolean hasKey = redissonCache.hasKey(userKey);
        if (!hasKey) {
            log.info("NettyWebSocketParamHandler.channelRead0 --> : 用户未登录... {}", senderId);
            ctx.close();
        }
        // token续期
        redissonCache.expire(userKey, SystemConstants.TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS);
    }

    /**
     * 加入聊天室
     *
     * @param ctx
     * @param senderId
     * @throws ExecutionException
     * @throws InterruptedException
     */
    private void joinGroup(ChannelHandlerContext ctx, Long senderId) {
        R r = null;
        try {
            CompletableFuture<R> result = memberRemote.getGroupListById(senderId);
            r = result.get(3, TimeUnit.SECONDS);
        } catch (Exception e) {
            log.error("messageHandler.joinGroup查询群聊列表失败 ===> {}", e.getMessage());
            ctx.channel().write(WSMessageDTO.error("查询群聊列表失败"));
            return;
        }
        if (r == null || r.getCode() != 200) {
            log.error("查询群聊列表失败 ====> {}", r.getMsg());
            ctx.channel().write(WSMessageDTO.error("查询群聊列表失败"));
            return;
        }
        //查询成功
        //获取群聊列表
        String json = JacksonInstance.toJson(r.getData());
        List<Long> groupIds = JacksonInstance.toObjectList(json, new TypeReference<List<Long>>() {
        });
        ChannelGroup group;
        for (Long groupId : groupIds) {
            group = groupMap.get(groupId);
            if (group == null) {
                //如果群聊信道不存在,则创建一个群聊信道
                group = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
                groupMap.put(groupId, group);
            }
            //将当前用户加入到群聊信道中
            group.add(ctx.channel());
        }
    }

    /**
     * 加入聊天信道
     */
    private void joinChat(ChannelHandlerContext ctx, Long senderId) {
        //将当前用户的channelId放入map中
        userMap.put(senderId, ctx.channel().id());
    }

    private void initData(ChannelHandlerContext ctx, Long senderId) {
        joinChat(ctx, senderId);
        joinGroup(ctx, senderId);
    }
}

五.MessageHandler核心业务处理类-采用工厂策略模式

使得业务和通信协议无关,无感知。具体业务可以增加策略

/**
 * @author YuanJie
 * @projectName vector-server
 * @package com.vector.netty.handler
 * @className com.vector.netty.handler.MessageTypeHandler
 * @copyright Copyright 2020 vector, Inc All rights reserved.
 * @date 2023/6/15 16:23
 */
@Slf4j
@Component
@ChannelHandler.Sharable
public class MessageHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    @Resource
    private MessageStrategyContext messageStrategyContext;


    @Override
    public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) {
        // 获取客户端发送的数据
        WSMessageDTO wsMessageDTO = JacksonInstance.toObject(msg.text(), new TypeReference<WSMessageDTO>() {
        });
        wsMessageDTO.setMessageId(SnowFlakeUtil.getNextId());
        log.info("客户端收到服务器数据:{}", wsMessageDTO.getMessage());
        verifyParams(ctx, wsMessageDTO);
        // 根据消息类型获取对应的处理器 核心处理方法
        messageStrategyContext.messageType(ctx, wsMessageDTO);
    }

    private void verifyParams(ChannelHandlerContext ctx, WSMessageDTO wsMessageDTO) {
        StringBuilder sb = new StringBuilder();
        if (wsMessageDTO.getSenderId() == null) {
            sb.append("senderId不能为空");
        }
        if (!EnumBusiness.containsBusiness(wsMessageDTO.getBusinessType())) {
            sb.append("businessType不能为空");
        }
        if (!EnumMessage.containsMessage(wsMessageDTO.getMessageType())) {
            sb.append("messageType不能为空");
        }
        if (wsMessageDTO.getMessage() == null) {
            sb.append("message不能为空");
        }
        if (sb.length() > 0) {
            log.error("参数校验失败:{}", sb.toString());
            ctx.channel().write(WSMessageDTO.error("参数校验失败:" + sb.toString()));
            ctx.close();
        }
    }


    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }
}

5.1 策略上下文

具体工厂策略可以详看我的策略模式文章
枚举维护前端参数和bean对象名

/**
 * @author YuanJie
 * @projectName vector-server
 * @package com.vector.netty.enums
 * @className com.vector.netty.enums.BusinessEnums
 * @copyright Copyright 2020 vector, Inc All rights reserved.
 * @date 2023/6/14 16:13
 */
public enum EnumBusiness {
    /**
     * 单聊
     */
    chatMessage("chat", ChatMessageStrategy.class.getSimpleName()),
    /**
     * 群聊
     */
    groupMessage("group", GroupMessageStrategy.class.getSimpleName()),
    /**
     * 在线人数
     */
    onlineCount("onlineCount", OnlineCountStrategy.class.getSimpleName()),
    TEST("test",TestStrategy.class.getSimpleName());


    private final String businessType;
    private final String beanName;

    EnumBusiness(String businessType, String beanName) {
        this.businessType = businessType;
        this.beanName = StringUtils.isNotEmpty(beanName)?beanName.toLowerCase():null;
    }

    /**
     * 根据code获取对应的枚举对象
     */
    public static EnumBusiness getEnum(String businessType) {
        EnumBusiness[] values = EnumBusiness.values(); // 获取枚举列表
        if (null != businessType && values.length > 0) {
            for (EnumBusiness value : values) {
                if (value.businessType.equals(businessType)) {
                    return value;  // 返回枚举对象
                }
            }
        }
        return null;
    }

    /**
     * 该code在枚举列表code属性是否存在
     */
    public static boolean containsBusiness(String businessType) {
        EnumBusiness anEnum = getEnum(businessType); // 获取枚举对象
        return anEnum != null;
    }

    /**
     * 判断code与枚举中的code是否相同
     */
    public static boolean equals(String businessType, EnumBusiness calendarSourceEnum) {
        return calendarSourceEnum.businessType.equals(businessType);
    }


    public String getBusinessType() {
        return businessType;
    }

    public String getBeanName() {
        return beanName;
    }
}

策略根据bean名获取实例对象

/**
 * @author YuanJie
 * @projectName vector-server
 * @package com.vector.netty.service
 * @className com.vector.netty.service.MessageContext
 * @copyright Copyright 2020 vector, Inc All rights reserved.
 * @date 2023/6/14 17:02
 */
@Component
@Slf4j
public class MessageStrategyContext {

    /** 策略实例集合 */
    private final ConcurrentHashMap<String, MessageStrategy> strategyConcurrentHashMap =
            new ConcurrentHashMap<>(20);
    /**
     * 注入策略实例
     * 如果使用的是构造器注入,可能会有多个参数注入进来。
     *
     * 如果使用的是field反射注入
     *
     * 如果使用的是setter方法注入,那么你将不能将属性设置为final。
     *
     * @param strategyMap
     *         注意注入类型要是Map基础类型
     *         注入接口,spring会自动注入他的所有被spring托管的实现类
     */
    @Autowired
    public MessageStrategyContext(Map<String, MessageStrategy> strategyMap) {
        //清空集合数据
        this.strategyConcurrentHashMap.clear();
        if (!CollectionUtils.isEmpty(strategyMap)) {
            strategyMap.forEach((beanName, messageStrategy) -> {
                if (StringUtils.isEmpty(beanName) || messageStrategy == null) {
                    return;
                }
                this.strategyConcurrentHashMap.put(beanName.toLowerCase(), messageStrategy);
            });
        }
    }
    /**
     * 选择业务方式
     * 单聊,群聊,统计在线人数...
     *
     * @param msg 信息
     */
    public void messageType(ChannelHandlerContext ctx, WSMessageDTO msg){
        EnumBusiness enumerateInstances = EnumBusiness.getEnum(msg.getBusinessType());
        if (CollectionUtils.isEmpty(strategyConcurrentHashMap)) {
            log.info("策略实例集合初始化失败,请检查是否正确注入!");
        }
        MessageStrategy messageStrategy = strategyConcurrentHashMap.get(enumerateInstances.getBeanName());
        messageStrategy.messageType(ctx, msg);
    }

}

六.统一响应

注意使用该统一响应对象,所有入栈处理器必须使用即调用方必须是SimpleChannelInboundHandler,详细原因在下文 七.统一输出处理器中

/**
 * @author YuanJie
 * @projectName vector-server
 * @package com.vector.netty.entity
 * @className com.vector.netty.entity.SocketMessage
 * @copyright Copyright 2020 vector, Inc All rights reserved.
 * @date 2023/6/14 19:35
 */
@Data
public class WSMessageDTO {
    /**
     * 消息发送者
     */
    private Long senderId;
    /**
     * 消息接收者/群聊id
     */
    private Long chatId;
    /**
     * 消息类型 0文本 1图片 2文件 3视频 4语音 5位置 6名片 7链接 8系统消息
     * @see com.vector.netty.enums.EnumMessage
     */
    private byte messageType;
    /**
     * 业务类型 chat单聊 group群聊 onlineCount在线人数
     * @see com.vector.netty.enums.EnumBusiness
     */
    private String businessType;

    /**
     * 记录每条消息的id
     */
    private Long messageId;
    /**
     * 消息内容
     */
    private String message;

    /**
     * 消息发送时间
     */
    private LocalDateTime sendTime;
    /**
     * 消息接收时间
     */
    private LocalDateTime receiveTime;

    /**
     * 最后一条消息内容
     */
    private String lastMessage;

    /**
     * 消息状态 0失败 1成功
     */
    private byte code;


    /**
     * 封装统一返回格式
     * @return
     */
    public static TextWebSocketFrame ok(){
        WSMessageDTO data = new WSMessageDTO();
        data.setCode((byte) 1);
        return new TextWebSocketFrame(JacksonInstance.toJson(data)).retain();
    }

    public static TextWebSocketFrame ok(WSMessageDTO data){
        data.setCode((byte) 1);
        return new TextWebSocketFrame(JacksonInstance.toJson(data)).retain();
    }

    public static TextWebSocketFrame error(String message){
        WSMessageDTO data = new WSMessageDTO();
        data.setCode((byte) 0);
        data.setMessage(message);
        return new TextWebSocketFrame(JacksonInstance.toJson(data)).retain();
    }
}

七.统一输出处理器

  • 若调用WSMessageDTO方法,必须注意内存泄露
  • 即调用方必须是SimpleChannelInboundHandler<>
  • 严禁使用ChannelInboundHandlerAdapter, 否则将造成严重内存泄露
  • 相应地,必须使用此处的写出@param msg ,释放@param msg 引用
/**
 * @author YuanJie
 * @projectName vector-server
 * @package com.vector.netty.handler
 * @className com.vector.netty.handler.OutBoundHandler
 * @copyright Copyright 2020 vector, Inc All rights reserved.
 * @date 2023/7/24 22:38
 */
@Slf4j
@Component
@ChannelHandler.Sharable
public class OutBoundHandler extends ChannelOutboundHandlerAdapter {
    /**
     * 若调用WSMessageDTO方法,必须注意内存泄露
     * 即调用方必须是SimpleChannelInboundHandler<>
     * 严禁使用ChannelInboundHandlerAdapter, 否则将造成严重内存泄露
     * 相应地,必须使用此处的写出@param msg ,释放@param msg 引用
     * @param ctx               the {@link ChannelHandlerContext} for which the write operation is made
     * @param msg               the message to write
     * @param promise           the {@link ChannelPromise} to notify once the operation completes
     * @throws Exception
     */
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        if (msg instanceof FullHttpMessage){
            log.info("webSocket协议升级成功");
            // 出栈必须得这样写,不能自定义通信消息,可能把websocket反馈的消息覆盖了。  也不能在最后处理器调ctx.fireChannelRead()
            ctx.writeAndFlush(msg,promise);
            return;
        } else if (msg instanceof TextWebSocketFrame) {
            log.info("我要给客户端发送消息了。。。。");
            ctx.writeAndFlush(msg, promise);
            return;
        }
        log.error("OutBoundHandler.write: 消息类型错误");
        ctx.writeAndFlush(WSMessageDTO.error("服务器内部错误: OutBoundHandler.write()"),promise);
        ctx.close();
    }
}

在这里插入图片描述
在这里插入图片描述

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

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

相关文章

【C语言】联合和枚举

个人主页点这里~ 联合和枚举 一、联合体1、联合体类型的声明2、联合体成员的特点3、与结构体对比4、计算联合体大小 二、枚举1、枚举的声明2、枚举的优点3、枚举类型的使用 一、联合体 1、联合体类型的声明 联合体的定义与结构体相似&#xff0c;但是联合体往往会节省更多的空…

分布式光伏电力监控系统解决方案

安科瑞薛瑶瑶18701709087 分布式光伏现状 自发自用、余电上网模式 完全自发自用&#xff08;防逆流&#xff09;模式 全额上网模式 0.4kV并网系统 此图为车间新建屋顶分布式光伏发电项目&#xff0c;建设装机容量为1103.46kWp。组件采用单晶硅功率515Wp及550Wp组件&#xff…

文献学习-25-综合学习和适应性教学:用于病理性胶质瘤分级的多模态知识蒸馏

Comprehensive learning and adaptive teaching: Distilling multi-modal knowledge for pathological glioma grading Authors: Xiaohan Xing , Meilu Zhu , Zhen Chen , Yixuan Yuan Source: Medical Image Analysis 91 (2024) 102990 Key words: 知识蒸馏、模态缺失、胶质瘤…

js实现简单的添加移除水印效果

一、实现场景 在做某些内部管理系统时&#xff0c;需求可能要求增加水印&#xff0c;水印内容通常可能是logo或者用户名手机号。实现效果如图&#xff1a; 以下步骤可实现简单添加或者移除水印&#xff0c;并且可以自定义样式、旋转角度等。 二、实现方式 1、先新建一个js…

期权定价模型有哪些?

常见的期权定价模型有BSM模型、二叉树模型以及蒙特卡洛定价模型。 BS模型 在一系列的假设条件下&#xff0c;该模型将期权的价值表示成为标的资产价格、行权价格、无风险利率、期权剩余期限和标的资产波动率的函数。即一旦定量的给出上述5个影响因子的数值&#xff0c;就可以…

鸿蒙OS开发实例:【ArkTS类库多线程CPU密集型任务TaskPool】

CPU密集型任务是指需要占用系统资源处理大量计算能力的任务&#xff0c;需要长时间运行&#xff0c;这段时间会阻塞线程其它事件的处理&#xff0c;不适宜放在主线程进行。例如图像处理、视频编码、数据分析等。 基于多线程并发机制处理CPU密集型任务可以提高CPU利用率&#x…

【PFA树脂交换柱】实验室高纯PFA材质过滤柱耐受电子级氢氟酸含氟树脂层析柱

PFA离子交换柱&#xff0c;也叫PFA层析柱、PFA过滤柱等&#xff0c;其原理是利用吸附剂对不同化合物有不同吸附作用和不同化合物在溶剂中的不同溶解度&#xff0c;用适应溶剂使混合物在填有吸附剂的柱内通过&#xff0c;使复杂的混合物达到分离和提纯的目的。 柱体为透明PFA材…

Linux:运营商在网络中扮演的角色

文章目录 ip目前的问题ip目前的几种解决方案私有ipVS公有ip运营商再谈ip划分运营商的角度看ip 本篇总结的是运营商在网络中扮演的角色 ip目前的问题 在目前看来&#xff0c;ip最大的问题是ip号不够用了&#xff0c;那这个问题如何解决呢&#xff1f; 在之前的内容中有子网掩…

【GlobalMapper精品教程】073:像素到点(Pixels-to-Points)从无人机图像轻松生成点云

文章目录 一、工具介绍二、生成点云三、生成正射四、生成3D模型五、注意事项一、工具介绍 Global Mapper v19引入的新的像素到点工具使用摄影测量原理,从重叠图像生成高密度点云、正射影像及三维模型。它使LiDAR模块成为已经功能很强大的的必备Global Mapper扩展功能。 打开…

BabyAGI源码解读(1)-主体配置部分

1. BabyAGI概览 babyAGI是一个AI驱动的任务管理系统&#xff0c;其中babyagi.py脚本中定义了整个的流程&#xff0c;使用OpenAI的NLP能力根据目标创建新的任务&#xff0c;使用Chroma和Weaviate作为上下文存储和获取任务结果。 babyAGI实际上是一个死循环&#xff0c;它执行以…

每日一题(力扣136):只出现一次的数字

利用哈希&#xff1a;时间复杂度O(n)&#xff0c;空间复杂度O(n) class Solution { public:int singleNumber(vector<int>& nums) {if (nums.size() 1){return nums[0];}unordered_map<int, int> map;int len nums.size();for (int i 0; i < len; i){if…

计算机视觉的技术领域

计算机视觉是一门研究如何使计算机能够“看”和理解图像和视频的科学。它结合了图像处理、模式识别、机器学习、人工智能等多个领域的技术。以下是计算机视觉中的一些关键技术领域。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1. …

梨花带雨网页音乐播放器二开优化修复美化版全开源版本源码

源码简介 最新梨花带雨网页音乐播放器二开优化修复美化版全开源版本源码下载 梨花带雨播放器基于thinkphp6开发的XPlayerHTML5网页播放器前台控制面板,支持多音乐平台音乐解析。二开内容&#xff1a;修复播放器接口问题&#xff0c;把接口本地化&#xff0c;但是集成外链播放器…

【C++】 vector <string> 的超详细解析!!!

目录 一、前言 二、 vector <string> 的个人理解和总结 三、vector <string> 的初始化 四、vector <string> 的输入 \ 输出 五、vector <string> 中的注意事项 六、常考面试题 七、共勉 一、前言 在【C】中&#xff0c;vector 是一个动态数组…

加域报错:无法完成此功能

在尝试将计算机加入Windows域时&#xff0c;如果收到“无法完成此功能”的提示&#xff0c;这可能由多种原因引起&#xff0c;以下是一些常见的问题及其解决方法&#xff1a; 网络连接问题&#xff1a;确保当前计算机与域控制器之间的网络连接是正常的。可以尝试使用ping命令测…

Linux 环境下 Redis基础配置及开机自启

Linux 环境下 Redis基础配置及开机自启 linux环境安装redis<redis-6.0.5.tar.gz> 1-redis基本安装配置 解压 获取到tar包后&#xff0c;解压到相关目录&#xff0c;一般是将redis目录放在usr/local/redis目录下&#xff0c;可以使用-C指定到解压下目录 tar -zvxf re…

介绍一下Java的数据类型

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a;每天一个知识点 ✨特色专栏&#xff1a…

【JavaEE初阶系列】——synchronized原理及优化(偏向锁,轻量级锁,自旋锁,锁消除,锁粗化)

目录 &#x1f6a9;synchronized锁特性详细解说 &#x1f6a9;加锁工作过程(锁升级) &#x1f388;偏向锁 &#x1f388;轻量级锁(自适应的自旋锁) &#x1f388; 重量级锁 &#x1f6a9;其他的优化操作 &#x1f388;锁消除 &#x1f388;锁粗化 &#x1f388;相关面…

【AcWing】蓝桥杯集训每日一题Day10|递归|暴力|数学归纳法|1360.有序分数(C++)

1360.有序分数 1360. 有序分数 - AcWing题库难度&#xff1a;简单时/空限制&#xff1a;1s / 64MB总通过数&#xff1a;4128总尝试数&#xff1a;6630来源&#xff1a;usaco training 2.1算法标签枚举排序最大公约数递归Stern-Brocot Tree 题目内容 给定一个整数 N&#xff0…

Linux利用Jenkins部署SpringBoot项目保姆级教程

在当今快速发展的软件开发领域&#xff0c;持续集成和持续部署&#xff08;CI/CD&#xff09;已经成为提升开发效率、缩短产品上市时间的关键实践。Linux系统以其稳定性和开源友好性&#xff0c;成为众多开发者和企业的首选平台。而Spring Boot&#xff0c;作为一个轻量级的Jav…