项目9-网页聊天室8(消息的发送和接收之websocket)

这是整个项目最最核心的部分.

但是这个部分的编写,需要依赖"基础设施"

包括不限于前面已经实现的 主界面,用户管理,会话管理, 好友管理, 消息管理 等等....

发送消息,和接收消息,需要"实时传输

张三 发了一条消息,李四 这边立即就能接收到,

这样的机制, 基于 HTTP 实现,有点困难~~

服务器主动发消息给李四???

按照之前的讨论,张三和李四,不能直接通信(NAT)必须是张三发消息给服务器,服务器转发给 李四.(服务器有 外网 IP,张三李四都能访问到)
张三发给服务器,张三是客户端, 聊天程序是服务器客户端主动发消息给服务器,很正常.(本来客户端就是主动发起请求的一方)
服务器把消息转发给李四,李四也是客户端,聊天程序是服务器服务器要主动发响应给客户端了???这个事情是不太寻常的!!!!

以往写 HTTP 系列的程序,都是第一种, 客户端发起请求了,服务器才返回响应.

客户端不发请求,服务器就不返回响应~~
第二种情况, HTTP 程序中还没遇到过~~ 这个情况称为"服务器推送数据给客户端

对于 HTTP 协议来说,第一种情况是主要的使用方式,第二种消息推送机制,其实在 HTTP 中不太容易实现
当然也可以使用 HTTP 模拟实现消息推送的效果~~
轮询 

轮询方式的问题
1.消耗更多的系统资源.
接收方,等待过程中,需要频繁的给服务器发起请求,这些请求里,大部分都是"空转的
2.获取到消息不够及时.
需要等到下个请求周期才能拿到数据
如果提高轮询的频率,此时获取消息就及时了,但是系统资源消耗就更多如果降低轮询的频率,此时获取消息就慢了,但是系统消耗资源减少点。

此处引入了一个更好的方案,来解决上述"消息推送问题"-Websocket

1.websocket

1.1 基础

WebSocket 协议完整解析 - 知乎 (zhihu.com)

websocket rfc 6455

RFC 6455: The WebSocket Protocol (rfc-editor.org)

 1.2 握手过程

当客户端想要使用 WebSocket 协议与服务端进行通信时, 首先需要确定服务端是否支持 WebSocket 协议, 因此 WebSocket 协议的第一步是进行握手, WebSocket 握手采用 HTTP Upgrade 机制, 客户端可以发送如下所示的结构发起握手 (请注意 WebSocket 握手只允许使用 HTTP GET 方法):

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

在 HTTP Header 中设置 Upgrade 字段, 其字段值为 websocket, 并在 Connection 字段指示 Upgrade, 服务端若支持 WebSocket 协议, 并同意握手, 可以返回如下所示的结构:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Version: 13

1.3 常见错误说法

websocket是基于HTTP实现的(该说法是错误的)

websoket和HTTP是并列的关系

如何基于 websocket 编写 代码.
在 Java 中有两种形式来使用 websocket
1.直接使用 tomcat 提供的原生 websocket api
2.使用 spring 给咱们提供的 websocket api
基于 Spring 的 websocket api, 先写个简单的 hello world

1.4 WebSocket的使用 

万字详解,带你彻底掌握 WebSocket 用法(至尊典藏版)写的不错_websocket使用-CSDN博客

1.服务器代码

2.客户端部分

// 给这个 websocket 注册上一些回调函数.websocket.onopen = function() {// 连接建立完成后, 就会自动执行到.console.log("websocket 连接成功!");}websocket.onclose = function() {// 连接断开后, 自动执行到.console.log("websocket 连接断开!");}websocket.onerror = function() {// 连接异常时, 自动执行到console.log("websocket 连接异常!");}websocket.onmessage = function(e) {// 收到消息时, 自动执行到console.log("websocket 收到消息! " + e.data);}

websocket 的客户端和服务器都是分成这四个阶段来进行处理,
1.连接建立成功之后
2.收到消息后

3.连接关闭后
4.连接异常后

/
// 操作 websocket
/// 创建 websocket 实例
// let websocket = new WebSocket("ws://127.0.0.1:8080/WebSocketMessage");
let websocket = new WebSocket("ws://" + location.host + "/WebSoketMessage");
// let websocket = new WebSocket("ws://" + location.host + "/message/getMessage");
websocket.onopen = function() {console.log("websocket 连接成功!");
}websocket.onmessage = function(e) {console.log("websocket 收到消息! " + e.data);// 此时收到的 e.data 是个 json 字符串, 需要转成 js 对象let resp = JSON.parse(e.data);if (resp.type == 'message') {// 处理消息响应handleMessage(resp);} else {// resp 的 type 出错!console.log("resp.type 不符合要求!");}
}websocket.onclose = function() {console.log("websocket 连接关闭!");
}websocket.onerror = function() {console.log("websocket 连接异常!");
}

2.会话的区分

1.Servlet/Spring 自带的 HttpSession在登录的时候用到了

2.MessageSession 咱们自己聊天程序中创建的"会话",业务上的会话

3.WebSocketSession websocket 里面使用的会话
会话这个词,其实是"广义"概念

3.前后端交互

张三发请求,李四获得响应,张三也要有响应

【tips】

4.客户端实现发送消息操作/展示接收的消息

/
// 实现消息发送/接收逻辑
/function initSendButton() {// 1. 获取到发送按钮 和 消息输入框let sendButton = document.querySelector('.right .ctrl button');let messageInput = document.querySelector('.right .message-input');// 2. 给发送按钮注册一个点击事件sendButton.onclick = function() {// a) 先针对输入框的内容做个简单判定. 比如输入框内容为空, 则啥都不干if (!messageInput.value) {// value 的值是 null 或者 '' 都会触发这个条件return;}// b) 获取当前选中的 li 标签的 sessionId//页面刚刚加载时,selected未被绑定//同样也需要进行判断let selectedLi = document.querySelector('#session-list .selected');if (selectedLi == null) {// 当前没有 li 标签被选中. return;}let sessionId = selectedLi.getAttribute('message-session-id');// c) 构造 json 数据let req = {type: 'message',sessionId: sessionId,content: messageInput.value};req = JSON.stringify(req);console.log("[websocket] send: " + req);// d) 通过 websocket 发送消息websocket.send(req);// e) 发送完成之后, 清空之前的输入框messageInput.value = '';}
}initSendButton();/
// 操作 websocket
/// 创建 websocket 实例
// let websocket = new WebSocket("ws://127.0.0.1:8080/WebSocketMessage");
let websocket = new WebSocket("ws://" + location.host + "/WebSoketMessage");
// let websocket = new WebSocket("ws://" + location.host + "/message/getMessage");
websocket.onopen = function() {console.log("websocket 连接成功!");
}websocket.onmessage = function(e) {console.log("websocket 收到消息! " + e.data);// 此时收到的 e.data 是个 json 字符串, 需要转成 js 对象let resp = JSON.parse(e.data);if (resp.type == 'message') {// 处理消息响应handleMessage(resp);} else {// resp 的 type 出错!console.log("resp.type 不符合要求!");}
}websocket.onclose = function() {console.log("websocket 连接关闭!");
}websocket.onerror = function() {console.log("websocket 连接异常!");
}function handleMessage(resp) {// 把客户端收到的消息, 给展示出来. // 展示到对应的会话预览区域, 以及右侧消息列表中. // 1. 根据响应中的 sessionId 获取到当前会话对应的 li 标签. //    如果 li 标签不存在, 则创建一个新的let curSessionLi = findSessionLi(resp.sessionId);if (curSessionLi == null) {// 就需要创建出一个新的 li 标签, 表示新会话. curSessionLi = document.createElement('li');curSessionLi.setAttribute('message-session-id', resp.sessionId);// 此处 p 标签内部应该放消息的预览内容. 一会后面统一完成, 这里先置空curSessionLi.innerHTML = '<h3>' + resp.fromName + '</h3>'+ '<p></p>';// 给这个 li 标签也加上点击事件的处理curSessionLi.onclick = function() {clickSession(curSessionLi);}}// 2. 把新的消息, 显示到会话的预览区域 (li 标签里的 p 标签中)//    如果消息太长, 就需要进行截断. let p = curSessionLi.querySelector('p');p.innerHTML = resp.content;if (p.innerHTML.length > 10) {p.innerHTML = p.innerHTML.substring(0, 10) + '...';}// 3. 把收到消息的会话, 给放到会话列表最上面. let sessionListUL = document.querySelector('#session-list');sessionListUL.insertBefore(curSessionLi, sessionListUL.children[0]);// 4. 如果当前收到消息的会话处于被选中状态, 则把当前的消息给放到右侧消息列表中. //    新增消息的同时, 注意调整滚动条的位置, 保证新消息虽然在底部, 但是能够被用户直接看到. if (curSessionLi.className == 'selected') {// 把消息列表添加一个新消息. let messageShowDiv = document.querySelector('.right .message-show');addMessage(messageShowDiv, resp);scrollBottom(messageShowDiv);}// 其他操作, 还可以在会话窗口上给个提示 (红色的数字, 有几条消息未读), 还可以播放个提示音.  // 这些操作都是纯前端的. 实现也不难, 不是咱们的重点工作. 暂时不做了. 
}

5.服务器实现接收/转发消息

为了能够维护刚才所讨论的键值对映射关系,就需要知道当前 websocket 连接是哪个 userld 进行的.
在请求中虽然没有,但是在 HtpSession 中是有的!!

最初在用户登录的时候,给 HtpSession 里存了当前的 user 对象.

//通过注册这个特定的Httpsession 拦藏器,就可以把用户给Http5ession中添加的 Attribute 键值对
//往我们的 WebSocketsession 里也添加一份

package com.example.demo.component;import com.example.demo.config.Result;
import com.example.demo.constant.Constant;
import com.example.demo.mapper.MessageMapper;
import com.example.demo.mapper.MessageSessionMapper;
import com.example.demo.model.*;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.handler.TextWebSocketHandler;import java.io.IOException;
import java.util.List;@Component
@EnableWebSocket
public class WebSocketApi extends TextWebSocketHandler {@Autowiredprivate OnlineUserManager onlineUserManager;@Autowiredprivate MessageMapper messageMapper;@Autowiredprivate MessageSessionMapper messageSessionMapper;//内置的private ObjectMapper objectMapper=new ObjectMapper();@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {System.out.println("TestApi连接成功");User user=(User) session.getAttributes().get(Constant.USERINFO_SESSION_KEY);if(user==null){return;}//把键值对存起来onlineUserManager.online(user.getUserId(), session);/*if(result.getStatus()==Constant.HAVE_LOGIN){return result;}*/}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {System.out.println("TestApi收到消息"+message.toString());//1.获取当前用户的信息,进行消息的转发、User user=(User) session.getAttributes().get(Constant.USERINFO_SESSION_KEY);if(user==null){System.out.println("未登录用户,无法进行消息转发");return;}//2.针对请求进行解析,把json格式的字符串,转成一个Java对象MessageRequest messageRequest=objectMapper.readValue(message.getPayload(),MessageRequest.class);if(messageRequest.getType().equals("message")){//进行消息转发transferMessage(user,messageRequest);}else {System.out.println("信息有误,无法进行消息转发"+message.getPayload());}}//进行消息的转发private void transferMessage(User user, MessageRequest messageRequest) throws IOException {// 1. 先构造一个待转发的响应对象. MessageResponse/*private Integer fromId;private String fromName;private Integer sessionId;private String content;*/MessageResponse response=new MessageResponse();response.setFromId(user.getUserId());response.setFromName(user.getUsername());response.setSessionId(messageRequest.getSessionId());response.setContent(messageRequest.getContent());// 把这个 java 对象转成 json 格式字符串String respJson=objectMapper.writeValueAsString(response);System.out.println("[transferJson]"+respJson);// 2. 根据请求中的 sessionId, 获取到这个 MessageSession 里都有哪些用户. 通过查询数据库就能够知道了.List<Friend> friends=messageSessionMapper.getFriendBySessionId(messageRequest.getSessionId(), user.getUserId());// 此处注意!!! 上述数据库查询, 会把当前发消息的用户给排除掉. 而最终转发的时候, 则需要也把发送消息的人也发一次.// 把当前用户也添加到上述 List 里面Friend myself=new Friend();myself.setFriendId(user.getUserId());myself.setFriendName(user.getUsername());friends.add(myself);// 3. 循环遍历上述的这个列表, 给列表中的每个用户都发一份响应消息//    注意: 这里除了给查询到的好友们发, 也要给自己也发一个. 方便实现在自己的客户端上显示自己发送的消息.//    注意: 一个会话中, 可能有多个用户(群聊). 虽然客户端是没有支持群聊的(前端写起来相对麻烦), 后端无论是 API 还是 数据库//          都是支持群聊的. 此处的转发逻辑也一样让它支持群聊.for(Friend friend:friends){// 知道了每个用户的 userId, 进一步的查询刚才准备好的 OnlineUserManager, 就知道了对应的 WebSocketSession// 从而进行发送消息WebSocketSession webSocketSession=onlineUserManager.getSession(friend.getFriendId());if(webSocketSession==null){//如果该用户未在线,则不发送continue;}webSocketSession.sendMessage(new TextMessage(respJson));}// 4. 转发的消息, 还需要放到数据库里. 后续用户如果下线之后, 重新上线, 还可以通过历史消息的方式拿到之前的消息.//    需要往 message 表中写入一条记录.Message message=new Message();message.setSessionId(messageRequest.getSessionId());message.setContent(messageRequest.getContent());message.setFromId(user.getUserId());//像自增主键, 还有时间这样的属性, 都可以让 SQL 在数据库中生成messageMapper.add(message);}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {System.out.println("TestApi出现异常");User user=(User) session.getAttributes().get(Constant.USERINFO_SESSION_KEY);if(user==null){return;}onlineUserManager.offline(user.getUserId(), session);}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {System.out.println("TestApi正常关闭");User user=(User) session.getAttributes().get(Constant.USERINFO_SESSION_KEY);if(user==null){return;}onlineUserManager.offline(user.getUserId(), session);}
}
@Component
public class OnlineUserManager {//为了防止多线程所出现的错误,选择ConcurrentHashMapprivate ConcurrentHashMap<Integer, WebSocketSession> sessions=new ConcurrentHashMap<>();//1.用户上线,给这个哈希表中插入键值对public void online(Integer userId,WebSocketSession webSocketSession){if(sessions.get(userId)!=null){//此时说明用户已经在线了,就登录失败,不会记录这个映射关系.//不记录这个映射关系,后续就收不到任何消息(毕竟,咱们此处是通过映射关系来实现消息转发的)System.out.println(userId+"已登录,请不要重复登陆");return;}sessions.put(userId,webSocketSession);System.out.println(userId+"上线");}//2.用户下线,针对此哈希表删除元素public void offline(Integer userId,WebSocketSession webSocketSession){WebSocketSession exwebSocketSession=sessions.get(userId);if(exwebSocketSession==webSocketSession){//两个session为同一个才是真正的下线操作,否则什么也不干sessions.remove(userId);System.out.println(userId+"下线");}}// 3) 根据 userId 获取到 WebSocketSessionpublic WebSocketSession getSession(int userId) {return sessions.get(userId);}
}

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

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

相关文章

【游戏引擎】Unity脚本基础 开启游戏开发之旅

持续更新。。。。。。。。。。。。。。。 【游戏引擎】Unity脚本基础 Unity脚本基础C#语言简介C#基础 Unity脚本基础创建和附加脚本MonoBehaviour生命周期生命周期方法 示例脚本 Unity特有的API常用Unity API 实践示例&#xff1a;制作一个简单的移动脚本步骤1&#xff1a;创建…

口碑比较好的相亲交友平台有哪些?正规靠谱的相亲软件排行榜测评

在网络时代&#xff0c;越来越多的人热衷于使用相亲交友软件来寻找生命中的另一半。这些软件确实为许多用户提供了真实可靠的交友平台。然而&#xff0c;市面上的相亲软件种类繁多&#xff0c;质量良莠不齐&#xff0c;让人难以选择。今天&#xff0c;我将介绍几款我使用过且认…

并发编程:ScheduledThreadPoolExecutor你真的了解吗?

前言 首先看到标题&#xff0c;我们其实很多人都知道&#xff0c;但是呢 在实际项目中我们面对很多延迟任务实现方案有很多选择&#xff0c;甚至直接在网上百度反正都能实现就行&#xff0c;但是忽略了很多细节&#xff0c;导致生产上的事故&#xff0c;都是因为没有真正了解到…

Git时光机、Git标签、Git分支、GitHub协作

Git时光机&#xff08;切换版本&#xff09; 1.查看提交历史 HEAD指针指向这次分支的最后一次提交 版本信息一行显示【git log --prettyoneline】 2.引用日志【git reflog】 &#xff08;只在自己的工作区中存在&#xff09; 非常重要&#xff1a;当HEAD指针进行切换之后&…

重学java 43.多线程 多等待多唤醒案例

Fear never builds the future,but hope does. —— 24.5.25 多等待多唤醒问题 在多条线程同时消费同时等待时&#xff0c;会出现问题 BaoZiPu package S77ThreadMoreWait;/*count和flag可以定义成包装类&#xff0c;但要记得给count和flag手动赋值不然对于本案例来说&#xff…

python低阶基础100题(上册)

** python低阶基础100题&#xff08;上册&#xff09; ** 1. 请打印出字符串 Hello World print("Hello World")2. 请打印出字符串 爸爸妈妈&#xff0c;你们辛苦啦 print("爸爸妈妈&#xff0c;你们辛苦啦")3. 请打印出字符串 人生苦短&#xff0c;我…

SVN创建分支,分支合并,切换分支。通俗易懂

1、首先在svnbucket.com远程仓库上创建项目&#xff0c;这里我创建了个测试demo&#xff1a; 2、先把svn仓库的项目检出到自己的文件夹&#xff0c;我这里是demo001文件夹&#xff0c;此时并没有创建truck, branches, tags这三个目录&#xff1a; 3、 在demo001文件夹里新建tru…

vue实战 ---- 社交媒体---黑马头条项目

vue基础 1.介绍 为什么会有Vuex ? ​ Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态&#xff0c;并以相应的规则保证状态以一种可预测的方式发生变化。 vuex是采用集中式管理组件依赖的共享数据的一个工具&#xff0c;可以解…

spring boot打的包直接运行

Spring Boot 提供了一个插件 spring-boot-maven-plugin 把程序打包成一个可执行的jar包&#xff0c;直接执行java -jar xxx.jar即可以启动程序 1、引用 spring-boot-maven-plugin插件 <build><plugins><plugin><groupId>org.springframework.boot<…

Hive安装教程

前置条件:hadoop&mysql docker容器安装mysql-CSDN博客 以下的/opt/bigdata目录根据自己实际情况更改 1.上传hive包并解压 tar -zxvf apache-hive-3.1.3-bin.tar.gz -C /opt/bigdata/ 2.修改路径 mv /opt/bigdata/apache-hive-3.1.3-bin/ hive cd /opt/bigdata/hive/…

全网最全网络基础思维导图合集(38张)

计算机网络基础知识点多且杂&#xff0c;想要系统地学习&#xff0c;思维导图肯定是必不可少的。 今天整理了38张思维导图&#xff0c;帮助你轻松理清思路&#xff0c;快速掌握关键内容。建议你收藏起来慢慢看&#xff0c;在看过之后最好能重新动手画一画&#xff0c;让计算机…

【数据结构】线性表习题 |顺序表 |链表 |栈和队列

&#x1f4d6;专栏文章&#xff1a;数据结构学习笔记 &#x1faaa;作者主页&#xff1a;格乐斯 前言 线性表习题 |顺序表 |链表 |栈和队列 顺序表和链表 1、 选B 1002(5-1)108* 第i个元素地址X&#xff0c;元素长度Len&#xff0c;第j个元素地址Y 公式&#xff1a;YXL…

Windows 7 SP1 安装VMtools -- 安装失败的解决方法

VMware安装Win7 SP1可以参考这篇文章&#xff1a;https://blog.csdn.net/2301_77225571/article/details/139121179?spm1001.2014.3001.5501 1.下载补丁 https://www.catalog.update.microsoft.com/search.aspx?qkb4474419 2.本机远控Win7 【Win】【R】&#xff0c;输入cmd…

第二十届文博会沙井艺立方分会场启幕!大咖齐打卡!

2024年5月24日-27日&#xff0c;第二十届中国&#xff08;深圳&#xff09;国际文化产业博览交易会沙井艺立方分会场活动将在艺立方非遗&#xff08;文旅&#xff09;产业园盛大举办。 本届文博会艺立方分会场活动办展特色鲜明&#xff0c;亮彩纷呈&#xff0c;将以“种下梧桐树…

aws eks理解和使用podidentity为pod授权

参考链接 https://www.amazonaws.cn/new/2024/amazon-eks-introduces-eks-pod-identity/https://aws.amazon.com/cn/blogs/aws/amazon-eks-pod-identity-simplifies-iam-permissions-for-applications-on-amazon-eks-clusters/ 先决条件 集群版本需要符合要求&#xff0c;如果…

【揭秘!在线ChatGPT神器,体验入口在此!】

&#x1f680;【揭秘&#xff01;在线ChatGPT神器&#xff0c;体验入口在此&#xff01;】&#x1f680; 前言 嘿&#xff0c;大家好&#xff01;今天我要和大家分享一些关于如何使用免费的ChatGPT的技巧。ChatGPT是一项令人兴奋的人工智能技术&#xff0c;它可以成为我们的好…

aws eks集成wasm运行时并启动pod

参考资料 WebAssembly 在云原生中的实践指南&#xff0c;https://cloud.tencent.com/developer/article/2324065 作为一种通用字节码技术&#xff0c;wasm的初衷是在浏览器中的程序实现原生应用性能。高级语言将wasm作为目标语言进行编译并运行在wasm解释器中。和nodejs类似的…

文件自动同步备份-FreeFileSync工具解决硬盘损坏、误操作覆盖导致数据丢失

文件自动同步备份-FreeFileSync工具解决硬盘损坏、误操作覆盖导致数据丢失 文章目录 文件自动同步备份-FreeFileSync工具解决硬盘损坏、误操作覆盖导致数据丢失前言一、FreeFileSync二、使用方法1.用外部存储卡或盘作为异地备份目标盘2.设置同步策略3.设置为windows的自动计划 …

将电脑D盘部分空间划分给C盘的方法

本文介绍在Windows电脑中&#xff0c;将D盘的部分空间分给C盘的方法。 最近&#xff0c;发现电脑中C盘的空间剩余不多了&#xff1b;而D盘由于当初分盘时划分的空间过多&#xff0c;导致其剩余空间很大且大概率以后都不会用上D盘中这些多余的空间了。因此&#xff0c;希望将D盘…

ubuntu20.04 安装系统后-开机黑屏-nvidia显卡驱动没问题_thinkpad-intel-13700H

文章目录 硬件现象原因&解决 硬件 thinkpad p1 gen6笔记本&#xff0c; intel 13代cpu 13700H,nvidia rtx 2000 Ada laptop gpu 13700H应该是有集显的&#xff0c;但可能没装集显驱动or由于Bios设置的缘故&#xff0c;我的win任务管理器只能看到一个gpu(gpu0)&#xff1…