当前位置: 首页 > news >正文

支持语音与视频即时通讯项目杂记(一)

    

第一部分解释服务端的实现。

(服务端结构)

   下面一个用于实现TCP服务器的代码,包括消息服务器(TcpMsgServer)和文件中转服务器(TcpFileServer)。

首先,TcpServer是TcpMsgServer和TcpFileServer的基类,它负责创建QTcpServer对象并监听端口。通过StartListen()函数可以启动监听,传入指定的端口号进行监听。CloseListen()函数用于关闭监听。

TcpMsgServer是消息服务器,继承自TcpServer类。它通过重写SltNewConnection()函数来处理新客户端连接的逻辑。当有新的客户端连接到服务器时,会创建一个ClientSocket对象来管理该客户端连接。在SltConnected()函数中,对连接进行验证后,将客户端对象添加到容器m_clients中,并建立与该客户端的信号与槽连接。在SltDisConnected()函数中,处理客户端下线的情况,从容器中移除对应的客户端对象,并断开相关的信号与槽连接。SltMsgToClient()函数用于消息转发控制,根据收到的消息类型、目标客户端ID和消息内容,找到对应的客户端对象,并调用其SltSendMessage()函数将消息发送给客户端。

TcpFileServer是文件中转服务器,同样继承自TcpServer类。它也重写了SltNewConnection()函数来处理新的客户端连接。在SltConnected()函数中,将连接上的客户端对象添加到容器m_clients中。SltDisConnected()函数处理客户端断连的情况,从容器中移除对应的客户端对象,并断开相关的信号与槽连接。SltClientDownloadFile()函数处理客户端请求下载文件的情况,根据收到的消息中的来源ID和文件名,在容器m_clients中找到对应的客户端对象,调用其StartTransferFile()函数开始文件传输过程。

在代码中,TcpMsgServer和TcpFileServer都采用了容器来管理连接的客户端对象,以便进行消息转发和文件传输等操作。

#include "tcpserver.h"
#include "clientsocket.h"
#include "myapp.h"
#include "databasemagr.h"#include <QHostAddress>/
/// 服务器类,是TcpMsgServer和TcpFileServer的基类
TcpServer::TcpServer(QObject *parent) :QObject(parent)
{m_tcpServer = new QTcpServer(this);connect(m_tcpServer, SIGNAL(newConnection()), this, SLOT(SltNewConnection()));
}TcpServer::~TcpServer()
{if (m_tcpServer->isListening()) m_tcpServer->close();
}///启动监听
bool TcpServer::StartListen(int port)
{if (m_tcpServer->isListening()) m_tcpServer->close();bool bOk = m_tcpServer->listen(QHostAddress::Any, port);return bOk;
}///关闭监听
void TcpServer::CloseListen()
{m_tcpServer->close();
}/
/// 消息服务器
TcpMsgServer::TcpMsgServer(QObject *parent) :TcpServer(parent)
{
}TcpMsgServer::~TcpMsgServer()
{qDebug() << "tcp server close";foreach (ClientSocket *client, m_clients) {m_clients.removeOne(client);client->Close();}
}/// 新客户端连接处理
void TcpMsgServer::SltNewConnection()
{ClientSocket *client = new ClientSocket(this, m_tcpServer->nextPendingConnection());connect(client, SIGNAL(signalConnected()), this, SLOT(SltConnected()));connect(client, SIGNAL(signalDisConnected()), this, SLOT(SltDisConnected()));
}///通过验证后,才可以加入容器进行管理
void TcpMsgServer::SltConnected()
{ClientSocket *client = (ClientSocket *)this->sender();if (NULL == client) return;connect(client, SIGNAL(signalMsgToClient(quint8,int,QJsonValue)),this, SLOT(SltMsgToClient(quint8,int,QJsonValue)));connect(client, SIGNAL(signalDownloadFile(QJsonValue)), this, SIGNAL(signalDownloadFile(QJsonValue)));m_clients.push_back(client);qDebug() << "TcpMsgServer::SltConnected. last m_nId=" + QString::number(m_clients[m_clients.size()-1]->GetUserId());
}///有客户端下线
void TcpMsgServer::SltDisConnected()
{//找到断连的socketClientSocket *client = (ClientSocket *)this->sender();if (NULL == client) return;//移除对应socketfor (int i = 0; i < m_clients.size(); i++) {if (client == m_clients.at(i)){m_clients.remove(i);return;}}disconnect(client, SIGNAL(signalConnected()), this, SLOT(SltConnected()));disconnect(client, SIGNAL(signalDisConnected()), this, SLOT(SltDisConnected()));disconnect(client, SIGNAL(signalMsgToClient(quint8,int,QJsonValue)),this, SLOT(SltMsgToClient(quint8,int,QJsonValue)));disconnect(client, SIGNAL(signalDownloadFile(QJsonValue)), this, SIGNAL(signalDownloadFile(QJsonValue)));
}///消息转发控制
void TcpMsgServer::SltMsgToClient(const quint8 &type, const int &id, const QJsonValue &json)
{// 查找要发送过去的idfor (int i = 0; i < m_clients.size(); i++) {if (id == m_clients.at(i)->GetUserId()){qDebug()<<"TcpMsgServer::SltMsgToClient. send to:"+QString::number(id);m_clients.at(i)->SltSendMessage(type, json);return;}}
}///传送文件到指定ID的客户端
void TcpMsgServer::SltTransFileToClient(const int &userId, const QJsonValue &json)
{// 查找要发送过去的idfor (int i = 0; i < m_clients.size(); i++) {if (userId == m_clients.at(i)->GetUserId()){m_clients.at(i)->SltSendMessage(SendFile, json);return;}}
}//
/// 文件中转服务器,客户端先把待转发的文件保存在服务器
/// 服务器接受完成后,通知其他客户端来下载
TcpFileServer::TcpFileServer(QObject *parent) :TcpServer(parent)
{
}TcpFileServer::~TcpFileServer()
{qDebug() << "tcp server close";foreach (ClientFileSocket *client, m_clients) {m_clients.removeOne(client);client->Close();}
}///客户端与文件服务器新建连接
void TcpFileServer::SltNewConnection()
{//新建槽函数与socketClientFileSocket *client = new ClientFileSocket(this, m_tcpServer->nextPendingConnection());connect(client, SIGNAL(signalConnected()), this, SLOT(SltConnected()));connect(client, SIGNAL(signalDisConnected()), this, SLOT(SltDisConnected()));
}/// socket管理
void TcpFileServer::SltConnected()
{//连接时将Client放入vector m_clientsClientFileSocket *client = (ClientFileSocket *)this->sender();if (NULL == client) return;m_clients.push_back(client);
}/// 客户端断连
void TcpFileServer::SltDisConnected()
{ClientFileSocket *client = (ClientFileSocket *)this->sender();if (NULL == client) return;for (int i = 0; i < m_clients.size(); i++) {if (client == m_clients.at(i)){m_clients.remove(i);return;}}disconnect(client, SIGNAL(signalConnected()), this, SLOT(SltConnected()));disconnect(client, SIGNAL(signalDisConnected()), this, SLOT(SltDisConnected()));
}/// 客户端请求下载文件
void TcpFileServer::SltClientDownloadFile(const QJsonValue &json)
{// 根据ID寻找连接的socketif (json.isObject()) {QJsonObject jsonObj = json.toObject();qint32 nId = jsonObj.value("from").toInt();//qint32 nWid = jsonObj.value("id").toInt();;//QString fileName = jsonObj.value("msg").toString();qDebug() << "get file" << jsonObj << m_clients.size();for (int i = 0; i < m_clients.size(); i++) {if (m_clients.at(i)->CheckUserId(nId, nWid)){m_clients.at(i)->StartTransferFile(fileName);return;}}}
}

       当服务端端通过accpt收到一个请求后,创建一个ClientSocket,处理客户端消息。
        下面是一个Qt中的客户端socket管理类,用于与服务端进行通信。其中包含两个类,一个是ClientSocket,用于处理普通消息,另一个是ClientFileSocket,用于处理文件传输。

在ClientSocket中,包含了一些信号和槽函数,用于处理连接、数据接收、关闭等操作。同时还有一些私有函数,用于解析不同类型的消息,并且把解析后的数据发送到前台界面进行展示。

在ClientFileSocket中,主要有两个功能:文件接收和文件发送。对于文件接收,分别记录了已经接收到的数据大小、文件名大小、要接收的文件等信息;对于文件发送,记录了文件大小、已经发送的数据大小、剩余数据大小、要发送的文件等信息。同时还有一些私有函数,用于初始化socket、处理接收到的数据、更新发送进度等操作。

总的来说,这个类是一个很重要的网络通信模块,可以实现与服务端的双向交互,包括文字、图片、文件等。

#ifndef CLIENTSOCKET_H
#define CLIENTSOCKET_H#include <QObject>
#include <QTcpSocket>
#include <QFile>
#include <QApplication>/// 服务端socket管理类
class ClientSocket : public QObject
{Q_OBJECT
public:explicit ClientSocket(QObject *parent = 0, QTcpSocket *tcpSocket = NULL);~ClientSocket();int GetUserId() const;void Close();
signals:void signalConnected();void signalDisConnected();void signalDownloadFile(const QJsonValue &json);void signalMsgToClient(const quint8 &type, const int &id, const QJsonValue &dataVal);
public slots:private:QTcpSocket *m_tcpSocket;int         m_nId;public slots:// 消息回发void SltSendMessage(const quint8 &type, const QJsonValue &json);private slots:void SltConnected();void SltDisconnected();void SltReadyRead();private:// 消息解析和抓转发处理void ParseLogin(const QJsonValue &dataVal);void ParseUserOnline(const QJsonValue &dataVal);void ParseLogout(const QJsonValue &dataVal);void ParseUpdateUserHead(const QJsonValue &dataVal);void ParseReister(const QJsonValue &dataVal);void ParseAddFriend(const QJsonValue &dataVal);void ParseAddGroup(const QJsonValue &dataVal);void ParseCreateGroup(const QJsonValue &dataVal);void ParseGetMyFriend(const QJsonValue &dataVal);void ParseGetMyGroups(const QJsonValue &dataVal);void ParseRefreshFriend(const QJsonValue &dataVal);void ParseRefreshGroups(const QJsonValue &dataVal);void ParseFriendMessages(const QByteArray &reply);void ParseGroupMessages(const QByteArray &reply);
};

ClientSocket::ClientSocket(QObject *parent, QTcpSocket *tcpSocket) :QObject(parent)
{qRegisterMetaType<QAbstractSocket::SocketError>("QAbstractSocket::SocketError");m_nId = -1;if (tcpSocket == NULL) m_tcpSocket = new QTcpSocket(this);m_tcpSocket = tcpSocket;connect(m_tcpSocket, SIGNAL(readyRead()), this, SLOT(SltReadyRead()));//处理客户端信息connect(m_tcpSocket, SIGNAL(connected()), this, SLOT(SltConnected()));//处理登录成功信号connect(m_tcpSocket, SIGNAL(disconnected()), this, SLOT(SltDisconnected()));//处理登出信号
}

处理客户端消息,根据消息类型进行不同的处理:

void ClientSocket::SltReadyRead()
{// 读取socket数据QByteArray reply = m_tcpSocket->readAll();QJsonParseError jsonError;// 转化为 JSON 文档QJsonDocument doucment = QJsonDocument::fromJson(reply, &jsonError);// 解析未发生错误if (!doucment.isNull() && (jsonError.error == QJsonParseError::NoError)) {// JSON 文档为对象if (doucment.isObject()) {// 转化为对象QJsonObject jsonObj = doucment.object();int nType = jsonObj.value("type").toInt();QJsonValue dataVal = jsonObj.value("data");switch (nType) {case Register:{ParseReister(dataVal);}break;case Login:{ParseLogin(dataVal);}break;case UserOnLine:{ParseUserOnline(dataVal);}break;case Logout:{ParseLogout(dataVal);Q_EMIT signalDisConnected();m_tcpSocket->abort();}break;case UpdateHeadPic:{ParseUpdateUserHead(dataVal);}break;case AddFriend:{ParseAddFriend(dataVal);}break;case AddGroup:{ParseAddGroup(dataVal);}break;case CreateGroup:{ParseCreateGroup(dataVal);}break;case GetMyFriends:{ParseGetMyFriend(dataVal);}break;case GetMyGroups:{ParseGetMyGroups(dataVal);}break;case RefreshFriends:{ParseRefreshFriend(dataVal);}break;case RefreshGroups:{ParseRefreshGroups(dataVal);}break;case SendMsg:case SendFile:case SendPicture:{ParseFriendMessages(reply);}break;case SendGroupMsg:{ParseGroupMessages(reply);}break;case SendFace:{ParseGroupMessages(reply);}break;case SendFileOk:{}break;case GetFile:{Q_EMIT signalDownloadFile(dataVal);}break;default:break;}}}
}

登录的处理:

void ClientSocket::ParseLogin(const QJsonValue &dataVal)
{// data 的 value 也是JSON对象if (dataVal.isObject()) {QJsonObject dataObj = dataVal.toObject();QString strName = dataObj.value("name").toString();QString strPwd = dataObj.value("passwd").toString();QJsonObject jsonObj = DataBaseMagr::Instance()->CheckUserLogin(strName, strPwd);m_nId = jsonObj.value("id").toInt();qDebug() << "login" << jsonObj;//验证成功才向server发送信号说明可以将socket加入容器管理if (m_nId > 0) Q_EMIT signalConnected();// 发送查询结果至客户端SltSendMessage(Login, jsonObj);;}
}

余略.....

相关文章:

支持语音与视频即时通讯项目杂记(一)

第一部分解释服务端的实现。 &#xff08;服务端结构&#xff09; 下面一个用于实现TCP服务器的代码&#xff0c;包括消息服务器&#xff08;TcpMsgServer&#xff09;和文件中转服务器&#xff08;TcpFileServer&#xff09;。 首先&#xff0c;TcpServer是TcpMsgServer和Tcp…...

文档:htm格式转txt

꧂ 两个地方都保存꧁ import os import codecs from bs4 import BeautifulSoupdef generate_output_filename(file_path, save_path):# 获取文件名&#xff08;不包含扩展名&#xff09;file_name os.path.splitext(os.path.basename(file_path))[0]# 构造保存路径和文件名ou…...

电子邮件地址注册过程详解

许多人可能对如何注册电子邮件地址感到困惑&#xff0c;本文将详细解析电子邮件地址的注册过程&#xff1a;确定邮箱厂商、创建邮箱账户、设置电子邮件地址。 1、确定要注册的邮箱厂商 首先我们需要确定要注册哪种类型的电子邮件服务。目前市场上有许多不同的电子邮件服务提供商…...

深度学习——卷积神经网络(CNN)基础二

深度学习——卷积神经网络&#xff08;CNN&#xff09;基础二 文章目录 前言三、填充和步幅3.1. 填充3.2. 步幅3.3. 小结 四、多输入多输出通道4.1. 多输入通道4.2. 多输出通道4.3. 11卷积层4.4. 小结 总结 前言 上文对卷积有了初步的认识&#xff0c;其实卷积操作就是通过卷积…...

R语言进度条:txtProgressBar功能使用方法

R语言进度条使用攻略 在数据处理、建模或其他计算密集型任务中&#xff0c;我们常常会执行一些可能需要很长时间的操作。 在这些情况下&#xff0c;展示一个进度条可以帮助我们了解当前任务的进度&#xff0c;以及大约还需要多长时间来完成&#xff0c;R语言提供了几种简单且灵…...

Maven实战-声明周期和插件

Maven实战-声明周期和插件 Maven 设计了插件机制&#xff0c;每个构建步骤都可以绑定一个或者多个插件行为&#xff0c;而且 Maven 为大多数构建步骤编写 并绑定了默认插件。例如&#xff0c;针对编译的插件有 maven-compiler-plugin&#xff0c;针对测试的插件有 maven-sure…...

ebpf的快速开发工具--libbpf-bootstrap

基于ubuntu22.04-深入浅出 eBPF 基于ebpf的性能工具-bpftrace 基于ebpf的性能工具-bpftrace脚本语法 基于ebpf的性能工具-bpftrace实战(内存泄漏) 什么是libbpf-bootstrap libbpf-bootstrap是一个开源项目&#xff0c;旨在帮助开发者快速启动和开发使用eBPF(Extended Berk…...

万界星空科技/生产制造执行MES系统/开源MES/免费MES

开源系统概述&#xff1a; 万界星空科技免费MES、开源MES、商业开源MES、市面上最好的开源MES、MES源代码、免费MES、免费智能制造系统、免费排产系统、免费排班系统、免费质检系统、免费生产计划系统、免费数字化大屏。 万界星空开源MES制造执行系统的Java开源版本。开源mes…...

螺纹快速接头在卫浴行业中的应用提高产量降低生产成本

螺纹快速接头在卫浴行业主要用于上下水测试和密封性测试&#xff0c;可以快速密封连接待测产品和水管。取代之前的工人手拧编织管六角螺母的方式&#xff0c;方便快捷&#xff0c;密封性好&#xff0c;产品测试更稳定。 卫浴行业产品必须具备很好的密封性&#xff0c;防止在实际…...

通达OA 2016网络智能办公系统 handle.php SQL注入漏洞

一、漏洞描述 北京通达信科科技有限公司通达OA2016网络智能办公系统 handle.php 存在sql注入漏洞&#xff0c;攻击者可利用此漏洞获取数据库管理员权限&#xff0c;查询数据、获取系统信息&#xff0c;威胁企业单位数据安全。 二、网络空间搜索引擎查询 fofa查询 app"T…...

parameter的各种用法以及localparam的用法

parameter的各种用法以及localparam的用法 一、这种写法放在v文件或者是用来调用其他的ram文件都是正确的。 一、这种写法放在v文件或者是用来调用其他的ram文件都是正确的。 module para_local();parameter a 10; // 第一种用法 parameter a 4d10; // 第二种用法 para…...

网络社区挖掘-图论部分的基本知识笔记

1 网络社区挖掘定义 网络社区挖掘是指利用数据挖掘技术和机器学习算法&#xff0c;分析社交网络、在线社区或互联网上的各种交互数据&#xff0c;以揭示其中隐藏的模式、关系和信息。这些社区可以是社交媒体平台、在线论坛、博客、微博等&#xff0c;人们在这些平台上进行交流…...

Vue Router - 路由的使用、两种切换方式、两种传参方式、嵌套方式

目录 一、Vue Router 1.1、下载 1.2、基本使用 a&#xff09;引入 vue-router.js&#xff08;注意&#xff1a;要在 Vue.js 之后引入&#xff09;. b&#xff09;创建好路由规则 c&#xff09;注册到 Vue 实例中 d&#xff09;展示路由组件 1.3、切换路由的两种方式 1.…...

mysql为什么会选错索引,以及优化器是如何选择索引的

一&#xff1a;概念 在 索引建立之后&#xff0c;一条语句可能会命中多个索引&#xff0c;这时&#xff0c;索引的选择&#xff0c;就会交由 优化器 来选择合适的索引。 优化器选择索引的目的&#xff0c;是找到一个最优的执行方案&#xff0c;并用最小的代价去执行语句。 二…...

vue基础知识十七:你知道vue中key的原理吗?说说你对它的理解

一、Key是什么 开始之前&#xff0c;我们先还原两个实际工作场景 1.当我们在使用v-for时&#xff0c;需要给单元加上key <ul><li v-for"item in items" :key"item.id">...</li> </ul>2.用new Date()生成的时间戳作为key&#x…...

攻防演练蓝队|Windows应急响应入侵排查

文章目录 日志分析web日志windows系统日志 文件排查进程排查新增、隐藏账号排查启动项/服务/计划任务排查工具 日志分析 web日志 dirpro扫描目录&#xff0c;sqlmap扫描dvwa Python dirpro -u http://192.168.52.129 -b sqlmap -u "http://192.168.52.129/dvwa/vulnera…...

uniapp 小程序实现图片宽度100%、高度自适应的效果

因为image组件默认是有宽度跟高度的&#xff0c;所以这个高度不怎么好写 通过load事件来控制图片的高度 话不多说&#xff0c;直接上代码&#xff0c; <image class"img" src"/static/image.png" :style"{ height: imgHeight px }"mode&q…...

05. NXP官方SDK使用实验

05. NXP官方SDK使用实验 官方SDK移植 官方SDK移植 新建cc.h文件 SDK包里面会用到很多数据类型&#xff0c;所以需要在该文件中定义一些常用的数据类型 #pragma once #define __I volatile #define __O volatile #define __IO volatiletypedef sig…...

Python- JSON使用初探

JSON 在JSON格式中&#xff0c;{} 和 [] 是两种主要的数据结构&#xff0c;分别表示对象&#xff08;或称为字典、哈希、map&#xff09;和数组&#xff08;或称为列表、序列&#xff09;。 {} - 对象 在JSON中&#xff0c;对象是一组"key": value对的集合。这些键必…...

vim的配置文件

用户级别配置文件 ~/.vimrc 修改用户级别的配置文件只会影响当前用户, 不会影响其他的用户. 例如: 在用户的家目录下的.vimrc文件中添加 set tabstop4 ----设置缩进4个空格 set nu ----设置行号 set shiftwidth4 —设置ggG缩进4个空格, 默认是缩进8个空格 系统级别配置文件 /e…...

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...

Prompt Tuning、P-Tuning、Prefix Tuning的区别

一、Prompt Tuning、P-Tuning、Prefix Tuning的区别 1. Prompt Tuning(提示调优) 核心思想:固定预训练模型参数,仅学习额外的连续提示向量(通常是嵌入层的一部分)。实现方式:在输入文本前添加可训练的连续向量(软提示),模型只更新这些提示参数。优势:参数量少(仅提…...

Java 语言特性(面试系列1)

一、面向对象编程 1. 封装&#xff08;Encapsulation&#xff09; 定义&#xff1a;将数据&#xff08;属性&#xff09;和操作数据的方法绑定在一起&#xff0c;通过访问控制符&#xff08;private、protected、public&#xff09;隐藏内部实现细节。示例&#xff1a; public …...

Qt Widget类解析与代码注释

#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui; }//解释这串代码&#xff0c;写上注释 当然可以&#xff01;这段代码是 Qt …...

MySQL中【正则表达式】用法

MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现&#xff08;两者等价&#xff09;&#xff0c;用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例&#xff1a; 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...

3-11单元格区域边界定位(End属性)学习笔记

返回一个Range 对象&#xff0c;只读。该对象代表包含源区域的区域上端下端左端右端的最后一个单元格。等同于按键 End 向上键(End(xlUp))、End向下键(End(xlDown))、End向左键(End(xlToLeft)End向右键(End(xlToRight)) 注意&#xff1a;它移动的位置必须是相连的有内容的单元格…...

CSS设置元素的宽度根据其内容自动调整

width: fit-content 是 CSS 中的一个属性值&#xff0c;用于设置元素的宽度根据其内容自动调整&#xff0c;确保宽度刚好容纳内容而不会超出。 效果对比 默认情况&#xff08;width: auto&#xff09;&#xff1a; 块级元素&#xff08;如 <div>&#xff09;会占满父容器…...

Spring是如何解决Bean的循环依赖:三级缓存机制

1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间‌互相持有对方引用‌,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...

CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)

漏洞概览 漏洞名称&#xff1a;Apache Flink REST API 任意文件读取漏洞CVE编号&#xff1a;CVE-2020-17519CVSS评分&#xff1a;7.5影响版本&#xff1a;Apache Flink 1.11.0、1.11.1、1.11.2修复版本&#xff1a;≥ 1.11.3 或 ≥ 1.12.0漏洞类型&#xff1a;路径遍历&#x…...

PAN/FPN

import torch import torch.nn as nn import torch.nn.functional as F import mathclass LowResQueryHighResKVAttention(nn.Module):"""方案 1: 低分辨率特征 (Query) 查询高分辨率特征 (Key, Value).输出分辨率与低分辨率输入相同。"""def __…...