问题引入

消息推送的方式

我们要实现,服务器把消息推送到客户端,可以轮训,长轮训

还有sse

WebSocket理论

WebSocket 的由来与核心价值

  • 诞生背景:解决 HTTP 协议在实时通信中的固有缺陷(单向请求-响应模式)

  • 核心驱动力

    • 替代低效轮询(Polling)和长轮询(Comet)

    • 满足实时应用需求(聊天、金融行情、游戏等)

  • 核心优势

    • 全双工通信:客户端/服务器可同时发送数据

    • 低延迟:从 HTTP 的数百 ms 降至 10-50ms

    • 高效传输:头部开销仅 2-14 字节(vs HTTP 的数百字节)

  • 标准化:2011 年 RFC 6455 成为正式标准

WebSocket 协议核心组成

组成部分作用必要性
握手阶段通过 HTTP 协议升级协商(101 Switching Protocols)切换到 WebSocket 协议兼容现有网络基础设施(代理、防火墙)
数据帧传输应用数据(文本/二进制)封装数据,支持分片传输大文件
控制帧管理连接状态(Ping/Pong 保活、Close 关闭)维持连接健康,避免资源泄漏
掩码机制客户端发送数据时进行 XOR 掩码加密防止恶意代理缓存污染(安全关键)
Opcode标识帧类型(文本/二进制/控制帧)正确解析消息内容
Payload Length动态长度标识(7/16/64位)支持从短消息到 GB 级大文件传输

Spring Boot 深度集成方案

基础架构

核心组件详解

  1. Client(客户端)

    • 作用:发起连接、订阅频道、收发消息

    • 为什么需要:作为通信的终端用户界面

    • 解决问题

      • 提供用户交互入口

      • 实现跨平台通信(Web/App/桌面)

    • 技术实现

      const socket = new WebSocket("ws://yourdomain/ws-endpoint");
      socket.onmessage = (event) => {console.log("收到消息:", event.data);
      };

  2. Endpoint(连接端点)

    • 作用:处理握手请求,建立持久连接

    • 为什么需要:作为WebSocket连接的入口网关

    • 解决问题

      • 协议升级(HTTP→WebSocket)

      • 连接生命周期管理

      • 跨域处理(CORS)

    • Spring Boot实现

      @Configuration
      @EnableWebSocketMessageBroker
      public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/ws-endpoint").setAllowedOrigins("*").withSockJS(); // 浏览器兼容方案}
      }

  3. WebSocket Connection(连接管道)

    • 作用维护全双工通信通道

    • 为什么需要:突破HTTP的无状态限制

    • 解决问题

      • 避免频繁握手(单次握手持久连接)

      • 支持双向实时通信

      • 降低延迟(从HTTP的300ms+降至30ms内)

  4. Message Broker(消息代理)

    • 作用:消息路由、分发、存储

    • 为什么需要:解耦生产者和消费者

    • 解决问题

      • 海量连接下的消息分发

      • 分布式系统扩展

      • 消息持久化与重试

    • 配置示例

      @Override
      public void configureMessageBroker(MessageBrokerRegistry registry) {// 使用外部消息中间件registry.enableStompBrokerRelay("/topic", "/queue").setRelayHost("rabbitmq-host").setRelayPort(61613);
      }

  5. 频道系统(路由核心)

    频道类型前缀作用解决的问题消息流向
    广播频道/topic公共消息广播1:N 消息分发 (如聊天室公告)发布者 → 所有订阅者
    私有队列/queue点对点通信1:1 精准投递 (如订单通知)发布者 → 特定订阅者
    用户频道/user用户级隔离多设备同步 (如微信网页/App同时在线)发布者 → 用户所有会话
  6. @MessageMapping Controller(业务处理器)

    • 作用:处理业务逻辑,生成响应

    • 为什么需要:分离通信协议与业务逻辑

    • 解决问题

      • 业务逻辑集中管理

      • 消息验证与转换

      • 数据库/服务集成

    • 示例

      @MessageMapping("/trade")
      @SendTo("/topic/stock-updates")
      public StockUpdate handleTrade(Order order) {// 1. 验证订单// 2. 执行交易// 3. 生成市场数据更新return tradingService.execute(order);
      }

架构演进价值

  1. 协议层优化

    • 替代方案对比:

      方案延迟开销双向通信频道支持
      HTTP轮询300ms+
      WebSocket基础50ms✔️
      WS+STOMP30ms✔️✔️
  2. 工程化价值

  3. 业务场景适配

    • 广播场景:/topic/news(新闻推送)

    • 私有场景:/queue/user-123/notifications(个人通知)

    • 混合场景:/topic/room-{id} + /user/queue/private(在线教育平台)

总结:为什么需要此架构

  1. 连接管理 通过Endpoint统一处理握手/断开,解决连接生命周期管理混乱问题

  2. 消息路由 频道系统实现发布-订阅模式,解决海量消息精准投递问题

  3. 业务解耦 控制器隔离业务逻辑与通信协议,解决代码维护困难问题

  4. 水平扩展 消息代理支持集群部署,解决单点性能瓶颈问题

  5. 安全管控 频道级权限控制,解决敏感数据泄露风险

终极价值:此架构在协议层实现高效实时通信,在架构层通过频道机制解决复杂业务场景的消息路由问题,在工程层通过Spring Boot实现企业级标准化,是构建现代实时应用的基石。

原理流程

在我的E盘的WebSocket文件夹

消息执行流程(Flow)概览

建立连接(connect,连接)

Client(客户端)发起到 /ws-endpoint 的 WebSocket 握手(handshake,握手),Endpoint(端点)完成升级后建立 WebSocket Connection(WebSocket 连接)。

订阅频道(subscribe,订阅)

Client 通过 STOMP 向 broker 发送 SUBSCRIBE Frame(订阅帧),表示“我要订阅 /topic/greetings”。

发送消息到 Controller(SEND Frame)

Client 发送 SEND Frame(发送帧),destination(目的地)为 /app/hello

Broker 根据 setApplicationDestinationPrefixes("/app"),将消息路由(route,路由)给匹配 @MessageMapping("/hello") 的方法

Controller(控制器)处理

GreetingController.handleHello(...) 被调用(invoke,调用),执行业务逻辑,返回 Greeting 对象。

Broker(代理)转发

因为方法上有 @SendTo("/topic/greetings"),返回值被封装成 MESSAGE Frame(消息帧)发送给 Broker(消息代理)。

Broker 将该消息分发(dispatch,分发)给所有订阅(subscription,订阅)了 /topic/greetings 的客户端 session。

Client(客户端)接收(receive,接收)

Client 在订阅回调(callback,回调)中拿到服务器推送(push,推送)的消息并渲染到页面。

这就是完整的一次流程。

API

客户端

websocket对象创建

let ws = new WebSocket(URL);

URL说明

格式:协议://ip地址:端口/访问路径 协议:协议名称为 ws

websocket对象相关事件

事件事件处理程序描述
openws.onopen连接建立时触发
messagews.onmessage客户端接收到服务器发送的数据时触发
closews.onclose连接关闭时触发

websocket对象提供的方法

方法名称描述
send()通过websocket对象调用该方法发送数据给服务端

简单示例

<script>
let ws = new WebSocket("ws://localhost/chat");
ws.onopen = function() {
};ws.onmessage = function(evt) {// 通过 evt.data 可以获取服务器发送的数据
};ws.onclose = function() {
};
</script>

服务端

Tomcat的7.0.5版本开始支持WebSocket,并且实现了Java WebSocket规范。

Java WebSocket应用由一系列的Endpoint组成。Endpoint是一个java对象,代表WebSocket链接的一端,对于服务端,我们可以视为处理具体WebSocket消息的接口。

我们可以通过两种方式定义Endpoint:

  • 第一种是编程式,即继承类javax.websocket.Endpoint并实现其方法。

  • 第二种是注解式,即定义一个POJO,并添加@ServerEndpoint相关注解。

Endpoint实例在WebSocket握手时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。在Endpoint接口中明确了与其生命周期相关的方法,规范实现者确保生命周期的各个阶段调用实例的相关方法。生命周期方法如下:

方法描述注解
onOpen()当开启一个新的会话时调用,该方法是客户端与服务端握手成功后调用的方法@OnOpen
onClose()当会话关闭时调用@OnClose
onError()当连接过程异常时调用@OnError

服务端如何接收客户端发送的数据呢?

  • 编程式 通过添加 MessageHandler 消息处理器来接收消息

  • 注解式 在定义 Endpoint 时,通过 @OnMessage 注解指定接收消息的方法

服务端如何推送数据给客户端呢?

发送消息则由 RemoteEndpoint 完成,其实例由 Session 维护

发送消息有 2 种方式发送消息

  • 通过 session.getBasicRemote 获取同步消息发送的实例,然后调用其 sendXxx() 方法发送消息

  • 通过 session.getAsyncRemote 获取异步消息发送实例,然后调用其 sendXxx() 方法发送消息

@ServerEndpoint("/chat")
@Component
public class ChatEndpoint {@OnOpen// 连接建立时被调用public void onOpen(Session session, EndpointConfig config) {}@OnMessage// 接收到客户端发送的数据时被调用public void onMessage(String message) {}@OnClose// 连接关闭时被调用public void onClose(Session session) {}
}

WebSocket 消息分发的三种常见模式

session.getAsyncRemote()(getBasicRemote).sendXxx() 方法本身并不直接区分这些模式,而是通过 目标地址(如 Session、Broadcast)应用层逻辑 来实现不同的消息分发方式。

WebSocket 消息分发的三种常见模式

1. 单播(Unicast)
  • 点对点发送:消息直接发送给某个特定的客户端(Session)。

  • 实现方式:通过目标客户端的 session.getAsyncRemote().sendText()

  • 示例:

    // 向特定客户端发送消息
    targetSession.getAsyncRemote().sendText("Private message");

2. 广播(Broadcast)
  • 一对多发送:消息发送给所有连接的客户端(或特定分组)。

  • 实现方式:遍历所有 Session 或使用 @ServerEndpoint 的全局集合。

  • 示例:

    // 广播给所有客户端
    for (Session session : allSessions) {session.getAsyncRemote().sendText("Broadcast message");
    }
  • 注意Java WebSocket API 本身不提供原生广播方法,需自行维护 Session 集合。

3. 组播(Multicast)
  • 分组发送:消息发送给订阅了特定主题(Topic)或频道的客户端。

  • 实现方式:通过应用层维护分组映射(如 Map<String, Set<Session>>)。

  • 示例:

    // 向订阅了 "news" 频道的客户端发送消息
    for (Session session : channelSubscribers.get("news")) {session.getAsyncRemote().sendText("News update");
    }

总结
模式目标范围实现关键适用场景
单播单个 Session直接调用目标 Session私聊、定向通知
广播所有 Session遍历全局 Session 集合公告、全局状态更新
组播分组 Session维护分组映射(Topic → Sessions)频道订阅、房间聊天

WebSocket 的灵活性在于:sendXxx() 是工具,分发模式由开发者通过 Session 代码管理逻辑实现

在线聊天室实现

具体代码在learnWebSocket里面

流程分析

package com.learnwebsocket.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;/*** @version v1.0* @ClassName: WebsocketConfig*/
@Configuration
public class WebsocketConfig {/*** 创建一个ServerEndpointExporter对象,这个对象会自动注册使用了@ServerEndpoint注解的类* @return*/@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}

后端

ServerEndpointExporter

首先,由于websocket不直接归于spring管理,属于spring的扩展模块,所以为了把websocket的实例也注册到spring里面,我们需要一个spring和websocket的连接桥梁。也就是ServerEndpointExporter。这个类负责加载websocket的端点。他同时可以被spring直接管理。

package com.learnwebsocket.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;/*** @version v1.0* @ClassName: WebsocketConfig*/
@Configuration
public class WebsocketConfig {/*** 创建一个ServerEndpointExporter对象,这个对象会自动注册使用了@ServerEndpoint注解的类* @return*/@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}

端点Endpoint

然后。我们需要自己创建一个端点,供ServerEndpointExporter发现管理。

这里面我们需要实现三个方法,这个上面有讲。

这里面还有广播和单播的实现代码,仔细看看。

还有的就是,由于Endpoint不直接属于spring,若要给Endpoint去配置一些东西,我们需要手动创建一个类,实现java给我们的接口,来去配置之后给spring管理

package com.learnwebsocket.ws.pojo;import com.alibaba.fastjson.JSON;import com.learnwebsocket.config.GetHttpSessionConfig;
import com.learnwebsocket.utils.MessageUtils;
import org.springframework.stereotype.Component;import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;/*** @version v1.0* @ClassName: ChatEndpoint* @Description: 端点* @Author: 黑马程序员*/
@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfig.class)
@Component
public class ChatEndpoint {// 用来保存所有的用户private static final Map<String,Session> onlineUsers = new ConcurrentHashMap<>();//当前用户对应的session对象private HttpSession httpSession;/*** 建立websocket连接后,被调用* @param session*/@OnOpenpublic void onOpen(Session session, EndpointConfig config) {//1,将session进行保存this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());String user = (String) this.httpSession.getAttribute("user");onlineUsers.put(user,session);//2,广播消息。需要将登陆的所有的用户推送给所有的用户String message = MessageUtils.getMessage(true,null,getFriends());broadcastAllUsers(message);}public Set getFriends() {Set<String> set = onlineUsers.keySet();return set;}// 广播所有用户private void broadcastAllUsers(String message) {try {//遍历map集合Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet();for (Map.Entry<String, Session> entry : entries) {//获取到所有用户对应的session对象Session session = entry.getValue();//发送消息session.getBasicRemote().sendText(message);}} catch (Exception e) {//记录日志}}/*** 浏览器发送消息到服务端,该方法被调用** 张三  -->  李四* @param message*/@OnMessagepublic void onMessage(String message) {try {//将消息推送给指定的用户Message msg = JSON.parseObject(message, Message.class);//获取 消息接收方的用户名String toName = msg.getToName();String mess = msg.getMessage();//获取消息接收方用户对象的session对象Session session = onlineUsers.get(toName);String user = (String) this.httpSession.getAttribute("user");String msg1 = MessageUtils.getMessage(false, user, mess);session.getBasicRemote().sendText(msg1);} catch (Exception e) {//记录日志}}/*** 断开 websocket 连接时被调用* @param session*/@OnClosepublic void onClose(Session session) {//1,从onlineUsers中剔除当前用户的session对象String user = (String) this.httpSession.getAttribute("user");onlineUsers.remove(user);//2,通知其他所有的用户,当前用户下线了String message = MessageUtils.getMessage(true,null,getFriends());broadcastAllUsers(message);}
}

配置类

上面的httpSession来自配置类的,因为登陆后我们把用户的名字存到了httpSession。但是websocket无法直接获取httpSession,所以要把它存到websocket配置文件里面。再获取。

package com.learnwebsocket.config;import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;/*** @version v1.0* @ClassName: GetHttpSessionConfig*/
public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {@Overridepublic void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request,HandshakeResponse response) {//获取HttpSession对象HttpSession httpSession = (HttpSession) request.getHttpSession();//将httpSession对象保存起来sec.getUserProperties().put(HttpSession.class.getName(),httpSession);}
}

前端

先登陆之后,然后向后端的端点请求websocket的连接,之后绑定三个方法。

await axios.get("user/getUsername").then(res => {this.username = res.data;});//创建webSocket对象ws = new WebSocket("ws://localhost:8080/chat");//给ws绑定事件ws.onopen = this.onopen;//接收到服务端推送的消息后触发ws.onmessage = this.onMessage;ws.onclose = this.onClose;

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

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

相关文章

用Python从零开始实现神经网络

反向传播算法用于经典的前馈人工神经网络。 它仍然是训练大型深度学习网络的技术。 在这个教程中&#xff0c;你将学习如何用Python从头开始实现神经网络的反向传播算法。 完成本教程后&#xff0c;您将了解&#xff1a; 如何将输入前向传播以计算输出。如何反向传播错误和…

算法148. 排序链表

题目&#xff1a;给你链表的头结点 head &#xff0c;请将其按 升序 排列并返回 排序后的链表 。示例 1&#xff1a;输入&#xff1a;head [4,2,1,3] 输出&#xff1a;[1,2,3,4] 示例 2&#xff1a;输入&#xff1a;head [-1,5,3,4,0] 输出&#xff1a;[-1,0,3,4,5] 示例 3&a…

在腾讯云CodeBuddy上实现一个AI聊天助手

在腾讯云CodeBuddy上实现一个AI聊天助手项目 在当今数字化时代&#xff0c;AI聊天助手已经成为一种非常流行的应用&#xff0c;广泛应用于客户服务、智能助手等领域。今天&#xff0c;我们将通过腾讯云CodeBuddy平台&#xff0c;实现一个基于Spring Boot和OpenAI API的AI聊天助…

JavaScript Array.prototype.flatMap ():数组 “扁平化 + 映射” 的高效组合拳

在 JavaScript 数组处理中&#xff0c;我们经常需要先对每个元素进行转换&#xff08;映射&#xff09;&#xff0c;再将结果 “铺平”&#xff08;扁平化&#xff09;。比如将数组中的每个字符串按空格拆分&#xff0c;然后合并成一个新数组。传统做法是先用map()转换&#xf…

区块链与元宇宙:数字资产的守护者

1 区块链支撑元宇宙数字资产的底层逻辑1.1 不可篡改性构建信任基石区块链的不可篡改性为元宇宙数字资产提供了坚实的信任基础。其核心在于分布式账本技术&#xff0c;当一笔数字资产交易发生时&#xff0c;会被打包成区块并广播至网络中的所有节点。每个节点都会对这笔交易进行…

Linux软件编程:进程和线程(进程)

进程一、基本概念进程&#xff1a;是程序动态执行过程&#xff0c;包括创建、调度、消亡程序&#xff1a;存放在外存的一段数据的集合二、进程创建&#xff08;一&#xff09;进程空间分布每个进程运行起来后&#xff0c;操作系统开辟0-4G的虚拟空间进程空间&#xff1a;用户空…

Mybatis学习笔记(五)

分页插件与性能优化 分页插件配置 简要描述&#xff1a;MybatisPlus分页插件是基于物理分页实现的高性能分页解决方案&#xff0c;支持多种数据库的分页语法&#xff0c;能够自动识别数据库类型并生成对应的分页SQL。 核心概念&#xff1a; 物理分页&#xff1a;直接在SQL层面进…

企业可商用的conda:「Miniforge」+「conda-forge」

文章目录一、彻底卸载现有 Anaconda/Miniconda二、安装 Miniforge&#xff08;推荐&#xff09;macOS/Linux检查Windows检查三、将通道固定为 conda-forge&#xff08;严格优先&#xff09;四、验证是否仍引用 Anaconda 源五、常见问题&#xff08;FAQ&#xff09;六、参考命令…

Flutter ExpansionPanel组件(可收缩的列表)

可以展开或者收缩的面板组件&#xff0c;收缩面板组件效果由ExpansionPanelList组件和ExpansionPanel组件共同完成。 ExpansionPanelList属性说明属性说明children子元素expansionCallback设置回调事件ExpansionPanel属性说明headerBuilder收缩的标题body内容isExpanded设置内容…

C/C++ 进阶:深入解析 GCC:从源码到可执行程序的魔法四步曲

引言距离上一篇博客更新已经过去了大概一两周的时间&#xff0c;而对于 Linux 系统的基本指令以及 Shell 编程的学习其实基本讲解完毕&#xff0c;Linux基础一块的知识就将告一段落了&#xff0c;如果有细节性的知识&#xff0c;我也会及时分享给各位&#xff0c;作为一名正在攀…

云服务器运行持续强化学习COOM框架的问题

1 环境要求 下载地址&#xff1a;https://github.com/TTomilin/COOM tensorflow 2.11以上 python 3.9以上 tensorflow2.12.0&#xff0c;需要安装tensorflow-probability0.19 2 修改代码 COOM/wrappers/reward.py 将 from gym import RewardWrapper修改为 from gymnasium impor…

MyBatis Interceptor 深度解析与应用实践

MyBatis Interceptor 深度解析与应用实践 一、MyBatis Interceptor概述 1.1 什么是MyBatis Interceptor MyBatis Interceptor&#xff0c;也称为MyBatis 插件&#xff0c;是 MyBatis 提供的一种扩展机制&#xff0c;用于在 MyBatis 执行 SQL 的过程中插入自定义逻辑。它类似…

【自动化测试】Web自动化测试 Selenium

&#x1f525;个人主页&#xff1a; 中草药 &#x1f525;专栏&#xff1a;【Java】登神长阶 史诗般的Java成神之路 测试分类 了解各种各样的测试方法分类&#xff0c;不是为了墨守成规按照既定方法区测试&#xff0c;而是已了解思维为核心&#xff0c;并了解一些专业名词 根…

2025 电赛 C 题完整通关攻略:从单目标定到 2 cm 测距精度的全流程实战

摘要 2025 年全国大学生电子设计竞赛 C 题要求“仅用一颗固定摄像头”在 5 s 内完成 100 cm~200 cm 距离、误差 ≤2 cm 的单目测距&#xff0c;并实时显示功耗。本文整合国一选手方案、CSDN 高分博文、B 站实测视频及官方说明&#xff0c;给出从硬件选型→离线标定→在线算法→…

Day 10: Mini-GPT完整手写实战 - 从组件组装到文本生成的端到端实现

Day 10-2: Mini-GPT完整手写实战 - 从组件组装到文本生成的端到端实现 📚 今日学习目标 掌握GPT架构组装:将Transformer组件组装成完整的生成模型 理解生成式预训练:掌握自回归语言建模的核心机制 端到端代码实现:从数据预处理到模型训练的完整流程 文本生成实战:训练Mi…

深入解析Prompt缓存机制:原理、优化与实践经验

深入解析Prompt缓存机制&#xff1a;原理、优化与实践经验 概述 在大型语言模型应用中&#xff0c;API请求的延迟和成本始终是开发者关注的核心问题。Prompt缓存&#xff08;Prompt Caching&#xff09;技术通过智能地复用重复内容&#xff0c;有效减少了API响应时间和运行成本…

CV 医学影像分类、分割、目标检测,之【3D肝脏分割】项目拆解

CV 医学影像分类、分割、目标检测&#xff0c;之【3D肝脏分割】项目拆解第1行&#xff1a;from posixpath import join第2行&#xff1a;from torch.utils.data import DataLoader第3行&#xff1a;import os第4行&#xff1a;import sys第5行&#xff1a;import random第6行&a…

Mybatis学习笔记(七)

Spring Boot集成 简要描述&#xff1a;MyBatis-Plus与Spring Boot的深度集成&#xff0c;提供了自动配置、启动器等特性&#xff0c;大大简化了配置和使用。 核心概念&#xff1a; 自动配置&#xff1a;基于条件的自动配置机制启动器&#xff1a;简化依赖管理的starter配置属性…

机器人伴侣的智能升级:Deepoc具身智能模型如何重塑成人伴侣体验

引言&#xff1a;机器人伴侣市场的技术变革需求随着人工智能技术的飞速发展和人们情感需求的多元化&#xff0c;机器人成人伴侣市场正在经历前所未有的增长。传统机器人伴侣已经能够满足基础的交互需求&#xff0c;但在智能化、情感化和个性化方面仍存在明显不足。这正是深算纪…

metabase基础使用技巧 (dashboard, filter)

这是metabase系列分享文章的第2部分。本文将介绍metabase的基础概念和使用介绍 question question是metabase中提供的通过UI化操作就能实现简单的 快捷 直接的BI查询。 点击右侧的New -> Question即可创建Question&#xff0c;可以理解为一个格式化的查询&#xff1a; 这里…