【实战】SpringBoot整合Websocket、Redis实现Websocket集群负载均衡

文章目录

    • 前言
    • 技术积累
      • 什么是Websocket
      • 什么是Redis发布订阅
      • Redis发布订阅与消息队列的区别
    • 实战演示
      • SpringBoot整合Websoket
      • Websoket集群负载均衡
    • 实战测试
      • IDEA启动两台服务端
      • 配置nginx负载均衡
      • 浏览器访问模拟对话

前言

相信很多同学都用过websocket来实现服务端主动向客户端推送消息吧,基本上所有的管理类系统都会有这个功能。因为有websocket的存在,使得前后的主动交互变得容易和低成本。其实在JAVA领域用SpringBoot框架集成Websoket还是很简单的,今天我们重点不是集成而是通过Redis的发布订阅实现Websocket集群通信,当然有条件的也可以用MQ代替。

技术积累

什么是Websocket

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。

WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在这里插入图片描述

什么是Redis发布订阅

Redis 的发布订阅(Pub/Sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。Redis 客户端可以订阅任意数量的频道。当有新消息通过 PUBLISH 命令发送给频道时,这个消息会被发送给订阅它的所有客户端。

在这里插入图片描述

Redis发布订阅与消息队列的区别

Redis的发布订阅(Pub/Sub)和消息队列是两种不同的消息传递模式,它们的主要区别在于消息的处理方式和使用场景。

消息的处理方式:
在 Redis 的发布订阅模式中,消息是即时的,也就是说,当消息发布后,只有当前在线且订阅了该频道的客户端才能收到这个消息,消息不会被存储,一旦发布,当前没有在线的客户端将无法接收到这个消息。

在消息队列中,消息是持久化的,消息被发送到队列后,会一直在队列中等待被消费,即使没有在线的消费者,消息也不会丢失,消费者下次上线后可以继续从队列中获取到消息。

实战演示

本次演示采用demo形式,仅仅提供演示Websocket集群的实现方式,以及解决消息负载均衡的问题。演示案例重点偏后端实现Websoket集群通讯,不涉及前后端心跳检测,如果应用在生产环境前端需要增加心跳检测与重复创建。

本实战采用原生spring websocket,后续有时间再提供netty版本,以及产线版本。有条件的同学可以自己实现,原理都差不多。

SpringBoot整合Websoket

1、项目结构
在这里插入图片描述

2、springcloud 版本

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
    <java.version>8</java.version>
    <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

3、maven依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.68</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!-- 整合thymeleaf前端页面 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

4、配置文件

server:
  port: 8888
spring:
  profiles:
    active: dev
  mvc:
    pathmatch:
      # Springfox使用的路径匹配是基于AntPathMatcher的,而Spring Boot 2.6.X使用的是PathPatternMatcher
      matching-strategy: ant_path_matcher
  thymeleaf:
    mode: HTML
    encoding: UTF-8
    content-type: text/html
    cache: false
    prefix: classpath:/templates/

5、thymeleaf 页面
websocket.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>websocket通讯</title>
</head>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
    var socket;
    function openSocket() {
        if (typeof (WebSocket) == "undefined") {
            console.log("您的浏览器不支持WebSocket");
        } else {
            console.log("您的浏览器支持WebSocket");
            //实现化WebSocket对象,指定要连接的服务器地址与端口  建立连接
            //等同于socket = new WebSocket("ws://localhost:8888/xxxx/im/25");
            //var socketUrl="${request.contextPath}/im/"+$("#userId").val();
            var socketUrl = "ws://192.168.1.4:7777/ws/" + $("#userId").val();
            socketUrl = socketUrl.replace("https", "ws").replace("http", "ws");
            console.log(socketUrl);
            if (socket != null) {
                socket.close();
                socket = null;
            }
            socket = new WebSocket(socketUrl);
            //打开事件
            socket.onopen = function () {
                console.log("websocket已打开");
                //socket.send("这是来自客户端的消息" + location.href + new Date());
            };
            //获得消息事件
            socket.onmessage = function (msg) {
                console.log("接收消息为:"+msg.data);
            };
            //关闭事件
            socket.onclose = function () {
                console.log("websocket已关闭");
            };
            //发生了错误事件
            socket.onerror = function () {
                console.log("websocket发生了错误");
            }
        }
    }

    //心跳检测与重复验证自己实现

    function sendMessage() {
        if (typeof (WebSocket) == "undefined") {
            console.log("您的浏览器不支持WebSocket");
        } else {
            console.log("您的浏览器支持WebSocket");
            console.log('发送消息为:{"fromUserId":"' + $("#userId").val() + '","toUserId":"' + $("#toUserId").val() + '","contentText":"' + $("#contentText").val() + '"}');
            socket.send('{"fromUserId":"' + $("#userId").val() + '","toUserId":"' + $("#toUserId").val() + '","contentText":"' + $("#contentText").val() + '"}');
        }
    }

</script>
<body>
<p>【userId】:<div><input id="userId" name="userId" type="text" value="10"></div>
<p>【toUserId】:<div><input id="toUserId" name="toUserId" type="text" value="20"></div>
<p>【内容】:<div><input id="contentText" name="contentText" type="text" value="hello websocket"></div>
<p>【操作】:<div><button onclick="openSocket()">开启socket</button></div>
<p>【操作】:<div><button onclick="sendMessage()">发送消息</button></div>
</body>
</html>

6、消息实体
Message.java

import lombok.Data;

/**
 * Message
 * @author senfel
 * @version 1.0
 * @date 2024/5/17 14:39
 */
@Data
public class Message {
    /**
     * 消息编码
     */
    private String code;

    /**
     * 来自(保证唯一)
     */
    private String fromUserId;

    /**
     * 去自(保证唯一)
     */
    private String toUserId;

    /**
     * 内容
     */
    private String contentText;

}

7、Websocket配置类
WebSocketConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
 * WebSocketConfig
 * @author senfel
 * @version 1.0
 * @date 2024/5/16 16:51
 */
@Component
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        System.out.println("启动websocket支持");
        return new ServerEndpointExporter();
    }
}


Websocket 服务类
WebSocketServer.java
import com.example.ccedemo.config.SpringUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * WebSocketServer
 * @author senfel
 * @version 1.0
 * @date 2024/5/16 16:59
 */
@ConditionalOnClass(value = WebSocketConfig.class)
@Component
@ServerEndpoint("/ws/{deviceId}")
public class WebSocketServer {

    protected Logger logger = LoggerFactory.getLogger(this.getClass());

    /**与某个客户端的连接会话,需要通过它来给客户端发送数据*/
    private Session session;
    /**
     * 设备ID
     */
    private String deviceId;
    /**concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
    虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
    注:底下WebSocket是当前类名*/
    private static CopyOnWriteArraySet<WebSocketServer> webSockets =new CopyOnWriteArraySet<>();
    /**用来存在线连接用户信息*/
    private static ConcurrentHashMap<String,Session> sessionPool = new ConcurrentHashMap<String,Session>();

    /**
     * 链接成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value="deviceId")String deviceId) {
        try {
            if(StringUtils.isEmpty(deviceId)||deviceId.equals("undefined")){
                return;
            }
            this.session = session;
            this.deviceId = deviceId;
            webSockets.add(this);
            sessionPool.put(deviceId, session);
            logger.info("【websocket消息】有新的连接,总数为:"+webSockets.size());
            StringBuffer stringBuffer = new StringBuffer();
            sessionPool.forEach((key, value) -> {
                stringBuffer.append(key).append(";");
            });
            logger.info("当前服务器连接有客户端有:"+stringBuffer.toString());
        } catch (Exception e) {
        }
    }

    /**
     * 链接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        try {
            webSockets.remove(this);
            sessionPool.remove(this.deviceId);
            logger.info("【websocket消息】连接断开,总数为:"+webSockets.size());
            StringBuffer stringBuffer = new StringBuffer();
            sessionPool.forEach((key, value) -> {
                stringBuffer.append(key).append(";");
            });
            logger.info("当前服务器连接有客户端有:"+stringBuffer.toString());
        } catch (Exception e) {
        }
    }
    /**
     * 收到客户端消息后调用的方法
     * @param message
     */
    @OnMessage
    public void onMessage(String message) {
        logger.info("【websocket消息】收到客户端消息:"+message);
        SpringUtil.getBean(StringRedisTemplate.class).convertAndSend("webSocketMsgPush",message);
    }

    /** 发送错误时的处理
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        logger.error("用户错误,原因:"+error.getMessage());
        error.printStackTrace();
    }



    /**
     * 广播消息
     * @author senfel
     * @date 2024/5/17 17:10
     * @return void
     */
    public void sendAllMessage(String message) {
        logger.info("【websocket消息】广播消息:"+message);
        for(WebSocketServer webSocket : webSockets) {
            try {
                if(webSocket.session.isOpen()) {
                    webSocket.session.getAsyncRemote().sendText(message);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * 单点消息 单人
     * @param deviceId
     * @param message
     * @author senfel
     * @date 2024/5/17 17:10
     * @return void
     */
    public void sendOneMessage(String deviceId, String message) {
        Session session = sessionPool.get(deviceId);
        if (session != null&&session.isOpen()) {
            try {
                logger.info("【websocket消息】 单点消息:"+message);
                session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 单点消息
     * @param deviceId
     * @param object
     * @author senfel
     * @date 2024/5/17 17:10
     * @return void
     */
    public void sendOneObject(String deviceId, Object object) {
        Session session = sessionPool.get(deviceId);
        if (session != null&&session.isOpen()) {
            try {
                logger.info("【websocket消息】 单点消息(对象):"+object);
                session.getAsyncRemote().sendObject(object);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 单点消息(多人)
     * @param deviceIds
     * @param message
     * @author senfel
     * @date 2024/5/17 17:11
     * @return void
     */
    public void sendMoreMessage(String[] deviceIds, String message) {
        for(String deviceId:deviceIds) {
            Session session = sessionPool.get(deviceId);
            if (session != null&&session.isOpen()) {
                try {
                    logger.info("【websocket消息】 单点消息:"+message);
                    session.getAsyncRemote().sendText(message);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

8、controller提供接口

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

/**
 * TestController
 * @author senfel
 * @version 1.0
 * @date 2024/5/17 17:49
 */
@RestController
@RequestMapping("/api/websocket")
public class BaseController {

    @GetMapping("page")
    public ModelAndView page(Long userId){
        ModelAndView websocket = new ModelAndView("websocket");
        websocket.addObject("userId",userId);
        return websocket;
    }
}

Websoket集群负载均衡

大家都知道Websoket是一个长链接,在不断开的情况下服务端与客户端是可以自由通讯的,这是因为服务端缓存了会话。

如果我们后端采用集群部署,那么可能多个用户的缓存会话会分散在各个服务器上。在我们给指定用户推送消息时就有可能调用服务器上并没有这个用户的会话。

所以,我们引入Redis发布订阅,将消息进行转发到所有的服务端,只有有会话缓存的服务端才会成功推送消息。讲到这里就比较明显了吧,完美解决Websoket负载均衡的问题。

1、maven引入Redis

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、配置文件

spring:
  redis:
    host: 127.0.0.1
    port: 6379

3、Redis配置类
RedisConfig.java

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * RedisConfig 
 * @author senfel
 * @version 1.0
 * @date 2024/5/17 14:31
 */
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    @Primary
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {

        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSeial.setObjectMapper(om);
        template.setValueSerializer(jacksonSeial);
        template.setKeySerializer(stringRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);
        template.setHashValueSerializer(jacksonSeial);
        template.afterPropertiesSet();
        return template;
    }

    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                            MessageListenerAdapter topicAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        //订阅了主题 webSocketMsgPush
        container.addMessageListener(topicAdapter, new PatternTopic("webSocketMsgPush"));
        return container;
    }
    /**
     * 消息监听器适配器,绑定消息处理器
     *
     * @return
     */
    @Bean
    MessageListenerAdapter topicAdapter() {
        return new MessageListenerAdapter(new RedisListener());
    }
}

4、Redis订阅监听
RedisListener.java

import com.alibaba.fastjson.JSONObject;
import com.example.ccedemo.config.SpringUtil;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;

/**
 * RedisListener
 * @author senfel
 * @version 1.0
 * @date 2024/5/17 14:37
 */
public class RedisListener implements MessageListener {

    @Override
    public void onMessage(Message msg, byte[] bytes) {
        System.out.println(".监听到需要进行负载转发的消息:" + msg.toString());
        com.example.ccedemo.redissocket.Message message = JSONObject.parseObject(msg.toString(), com.example.ccedemo.redissocket.Message.class);
        SpringUtil.getBean(WebSocketServer.class).sendOneMessage(message.getToUserId(), message.getContentText());
    }
}

实战测试

我们本地启动两个服务,分别开启端口8888、9999,然后用nginx暴露7777端口做一个负载均衡。

IDEA启动两台服务端

在这里插入图片描述

在这里插入图片描述

配置nginx负载均衡

#服务器url变量定义
upstream api_service1 {
    server 192.168.1.4:8888;
	server 192.168.1.4:9999;
}



#nginx配置websocket
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}
server {
    listen  7777;
        large_client_header_buffers 4 16k;
        client_max_body_size 300m;
        client_body_buffer_size 128k;
        proxy_connect_timeout 600;
        proxy_read_timeout 600;
        proxy_send_timeout 600;
        proxy_buffer_size 64k;
        proxy_buffers   4 32k;
        proxy_busy_buffers_size 64k;
        proxy_temp_file_write_size 64k;
        proxy_http_version 1.1;
		
    root  /demo/page/dist;
    index  index.html;
    
    
    #api
    location /api/ {
        proxy_pass  http://api_service1/api/;
        proxy_set_header Host $http_host;
    }
    
  

    #nginx配置websocket
    location /ws/ { 
        proxy_http_version 1.1; 
        proxy_pass  http://api_service1/ws/;
        proxy_redirect off;
        proxy_set_header Host $host;  
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 3600s;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
    

    #解决页面刷新404
    location / {
        try_files $uri $uri/ @req;
        index index.html;
    }

    location @req {
        rewrite ^.*$ /index.html last;
    }
}

浏览器访问模拟对话

1、浏览器开启多个无痕界面
http://192.168.1.4:7777/api/websocket/page
模拟对话:
多个界面的用户ID互补
在这里插入图片描述

2、分别开启soket,由于nginx轮询策略会分别注册在两个服务端上
在这里插入图片描述在这里插入图片描述

3、客户端相互发送消息验证
在这里插入图片描述

由以上图片可知,我们两个客户端相互对话能够接收到对方推送的消息。那么,由此也可以证明我们后端Websocke集群使用Redis发布订阅的方式搭建成功。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Web前端一套全部清晰 ⑨ day5 CSS.4 标准流、浮动、Flex布局

我走我的路&#xff0c;有人拦也走&#xff0c;没人陪也走 —— 24.5.24 一、标准流 标准流也叫文档流&#xff0c;指的是标签在页面中默认的排布规则&#xff0c;例如:块元素独占一行&#xff0c;行内元素可以一行显示多个。 二、浮动 作用: 让块级元素水平排列。 属性名:floa…

瑞米派Ubuntu系统移植指南-米尔RemiPi

1.概述 Linux系统平台上有许多开源的系统构建框架&#xff0c;这些框架方便了开发者进行嵌入式系统的构建和定制化开发&#xff0c;目前比较常见的有Buildroot, Yocto, OpenEmbedded等等。 同时更多的传统的桌面系统也加入到嵌入式环境体系中&#xff0c;如Ubuntu&#xff0c…

启动docker报错:Failed to listen on Docker Socket for the API.

说明&#xff1a; 1、安装部署docker完成后&#xff0c;启动docker报错&#xff1a;Failed to listen on Docker Socket for the API&#xff0c;如下图所示&#xff1a; 2、将SocketGroupdocker更改成&#xff1a;SocketGrouproot即可 一、解决方法&#xff1a; 1、执行命令…

用智能插件(Fitten Code: Faster and Better AI Assistant)修改好了可以持久保存的vue3留言板

天际 第一修改是选项式&#xff1a; <!-- 模板结构 --> <template><div><textarea placeholder"请输入备注内容" v-model"newItem"></textarea><button click"addItem">添加</button><hr><…

Redis 主从复制、哨兵与集群

一、Redis 主从复制 1. 主从复制的介绍 主从复制&#xff0c;是指将一台Redis服务器的数据&#xff0c;复制到其他的Redis服务器。前者称为主节点(Master)&#xff0c;后者称为从节点(Slave)&#xff1b;数据的复制是单向的&#xff0c;只能由主节点到从节点。 默认情况下&a…

【MySQL精通之路】InnoDB(6)-磁盘结构(2)-索引

主博客&#xff1a; 【MySQL精通之路】InnoDB(6)-磁盘上的InnoDB结构-CSDN博客 上一篇&#xff1a; 下一篇&#xff1a; 【MySQL精通之路】磁盘上的InnoDB结构-表空间-CSDN博客 目录 1.聚集索引和二级索引 1.1 Innodb 如何建立聚集索引 1.2 聚集索引如何加快查询速度 1…

大语言模型的工程技巧(一)——GPU计算

相关说明 这篇文章的大部分内容参考自我的新书《解构大语言模型&#xff1a;从线性回归到通用人工智能》&#xff0c;欢迎有兴趣的读者多多支持。 本文涉及到的代码链接如下&#xff1a;regression2chatgpt/ch07_autograd/gpu.ipynb 本文将讨论如何利用PyTorch实现GPU计算。本…

第十一节 SpringBoot Starter 面试题

一、面试题 很多同学的简历都写着熟悉 SpringBoot&#xff0c; 而 Starter 的实现原理被当作的考题的的情况越来越多。 来源牛客网关于 starter 的一些面试题 情景一、路虎一面 情景二、蔚来 情景三、同花顺 Starter 频频出现&#xff0c;因此在面试准备时&#xff0c;这道题…

Qt_电脑wifi相关操作

项目描述: 在做项目时用到了获取wifi的操作。在网上查找了好久资料,这里做一些总结。 这里有显示当前电脑wifi连接状态,列出wifi列表,连接断开wifi等函数。欢迎大家留言添加文章内容。 使用范围: windows电脑(中文的环境) 使用技术:windows的cmd命令。和对字符串的解析…

概念解析 | 3D Referring Expression Comprehension (3D-REC):让计算机“听懂“人类的3D语言指令

注1:本文系"概念解析"系列之一,致力于简洁清晰地解释、辨析复杂而专业的概念。本次辨析的概念是:3D Referring Expression Comprehension (3D-REC)。 概念解析 | 3D Referring Expression Comprehension (3D-REC):让计算机"听懂"人类的3D语言指令 PDF]…

FIFO-Diffusion,一个无需额外训练即可生成长视频的框架。通过确保每个帧引用足够多的先前帧来生成高质量、一致的长视频。

简单来讲&#xff0c;FIFO-Diffusion先通过一些模型如VideoCraft2、zeroscope、Opem-Sora Plan等与FIFO-Diffusion的组合生成短视频&#xff0c;然后取结尾的帧&#xff08;也可以取多帧&#xff09;&#xff0c;再用这一帧的图片生成另一段短视频&#xff0c;然后拼接起来。FI…

工大智信智能听诊器:开启个人健康管理的全新模式

工大智信智能听诊器&#xff1a;开启个人健康管理的全新模式 在快节奏的现代生活中&#xff0c;健康管理已成为人们关注的焦点。工大智信智能听诊器&#xff0c;作为一款创新的医疗设备&#xff0c;不仅提供高级数据管理功能&#xff0c;而且成为了个人健康管理的得力助手。 这…

6款网站登录页(附带源码)

6款网站登录页 效果图及部分源码123456 领取源码下期更新预报 效果图及部分源码 1 部分源码 <style>* {margin: 0;padding: 0;}html {height: 100%;}body {height: 100%;}.container {height: 100%;background-image: linear-gradient(to right, #fbc2eb, #a6c1ee);}.l…

Application Development using Large Language Models笔记

诸神缄默不语-个人CSDN博文目录 这是2023年NeurIPS Andrew Ng和Isa Fulford做的tutorial&#xff0c;关于如何用LLM来开发新产品的技术和思路&#xff1a;NeurIPS Tutorial Application Development using Large Language Models 文章目录 1. LLM基础2. 提示工程技巧3. 微调4.…

图片、视频画质增强变清晰工具分享(免费)

生活中可能会修一下模糊图片那么这就有一款用来修图片的管理工具&#xff0c;也有可能会修一下模糊的视频&#xff0c;在吾爱上有大佬开发了这么一款工具&#xff0c;免费的&#xff0c;不需要开任何VIP&#xff0c;我试了一下&#xff0c;好用&#xff0c;分享出来&#xff0c…

linux 上除了shell、python脚本以外,还有什么脚本语言用得比较多?

在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「 Linux的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01;说到在 Linux下的编程&#xf…

TypeScript-泛型

泛型(Generics) 指在定义接口&#xff0c;函数等类型的时候&#xff0c;不预先指定具体的类型&#xff0c;而在使用的时候再指定类型的一种特性&#xff0c;使用泛型可以复用类型并且让类型更加灵活 泛型接口-interface 语法&#xff1a;在 interface 接口类型的名称后面使用…

如何使用Unity XR Interaction Toolkit

使用环境&#xff1a; Unity2021.3.21f XR Interaction Toolkit v3.0.0 各类函数可用的&#xff1a; 简单项目配置&#xff1a; 第一步&#xff0c;导包&#xff08;samples可以不用导这么多&#xff0c;两个就够了&#xff09;&#xff1a; 第二步&#xff0c;构建场景&a…

基于STM32实现智能空气质量监测系统

目录 文章主题环境准备智能空气质量监测系统基础代码示例&#xff1a;实现智能空气质量监测系统 配置传感器并读取数据数据处理与显示数据存储与传输应用场景&#xff1a;室内环境监测与空气质量控制问题解决方案与优化收尾与总结 1. 文章主题 文章主题 本教程将详细介绍如何…

Xline社区会议Call Up|在 CURP 算法中实现联合共识的安全性

为了更全面地向大家介绍Xline的进展&#xff0c;同时促进Xline社区的发展&#xff0c;我们将于2024年5月31日北京时间11:00 p.m.召开Xline社区会议。 欢迎您届时登陆zoom观看直播&#xff0c;或点击“阅读原文”链接加入会议&#xff1a; 会议号: 832 1086 6737 密码: 41125…