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

【QT常用技术讲解】国产Linux桌面系统+window系统通过窗口句柄对窗口进行操作

前言

        本人的国产化项目涉及在国产Linux系统(宿主机)与window系统(虚拟机)的应用窗口交互功能:国产Linux系统与window生成一对一匹配的虚拟应用窗口,当点击虚拟应用窗口时,window端需要从任务栏中激活并显示到桌面最顶端。本篇将分别讲解国产桌面系统(统信UOS和麒麟kylin系统)和window10上通过窗口句柄实现对窗口的操作。

国产Linux桌面系统(宿主机)

1、创建窗口应用

QT中创建一个QWidget项目,在初始化函数中,启动窗口的任务栏点击事件、设置按钮和尺寸等属性。

    ui->setupUi(this);
    // 启用窗口的任务栏点击事件
    setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint);

    // 设置窗口标志,移除最大化、最小化和关闭按钮
    this->setWindowFlags(Qt::Window | Qt::FramelessWindowHint);
    setWindowOpacity(0);  // 设置窗口为完全透明
    resize(1, 1);  // 设置窗口为一个最小的尺寸

2、发送(打开指定应用程序命令)信息到虚拟机端

获取到当前程序的进程ID,以及要打开的window应用绝对路径,一起发送给虚拟机window端的QT编写的win服务器进程。

#include "networkclient.h"
void sendpack(const QString& Ip,netPackQ packet){
    // 创建一个客户端连接
    NetworkClient client(Ip, PORT);
    // 发送数据
    client.sendData(packet);
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    pid_t pid = QCoreApplication::applicationPid();
    .....
}

发送端的NetworkClient类如下:

//"networkclient.h"头文件
#ifndef NETWORKCLIENT_H
#define NETWORKCLIENT_H

#include <QObject>
#include <QTcpSocket>
#include <QDataStream>
#include <QByteArray>
#include <QString>
#include <iostream>
#include <QTimer>


//请求包结构
typedef struct  netPackQ
{
    char opCode;
    char keyword[254];
    netPackQ(){
        memset(opCode,0,1);
        memset(keyword,0,254);
    }
} netPackQ;

class NetworkClient : public QObject
{
    Q_OBJECT
public:
    explicit NetworkClient(QObject *parent = nullptr);
    NetworkClient(const QString &host, int port);
    ~NetworkClient();
    void sendData(const netPackQ &data) ;
    bool checkPortOpen(const QString &host, quint16 port, int timeout = 1);
    bool m_blink_status;
private:
    QTcpSocket *socket;
};

#endif // NETWORKCLIENT_H
//networkclient.cpp文件
#include "networkclient.h"

NetworkClient::NetworkClient(const QString &host, int port)
{
    m_blink_status=false;
    socket = new QTcpSocket(this);

    // 连接到服务器
    socket->connectToHost(host, port);

    if (!socket->waitForConnected(2000)) {
        std::cerr << "连接服务器失败!" << std::endl;
        return;
    }else m_blink_status=true;

    std::cout << "已连接到服务器!" << std::endl;
}

NetworkClient::~NetworkClient() {
    if (socket->isOpen()) {
        socket->close();
    }
}

void NetworkClient::sendData(const netPackQ &data) {
    if (socket->state() == QAbstractSocket::ConnectedState) {
        // 创建数据流
        QByteArray byteArray;
        QDataStream out(&byteArray, QIODevice::WriteOnly);
        out.setVersion(QDataStream::Qt_5_12);  // 设置版本,避免Qt版本变化导致问题

        // 写入数据结构
        out.writeRawData(data.opCode, sizeof(data.opCode));
        out.writeRawData(data.keyword, sizeof(data.keyword));

        // 发送数据
        socket->write(byteArray);
        socket->flush(); // 确保数据立即发送

        std::cout << "数据已发送!" << std::endl;
    }
}

qint64 getcurtime(){
    // 获取当前时间戳(秒级)
    std::time_t timestamp = std::time(nullptr);
    return timestamp;
}

bool NetworkClient::checkPortOpen(const QString &host, quint16 port,  int timeout) {
    qint64 bgtime=getcurtime();
    QTcpSocket socket;
    QTimer timer;

    // 设置超时机制
    timer.setSingleShot(true);

    // 设置连接超时为1秒
    QObject::connect(&timer, &QTimer::timeout, [&]() {
        qDebug() << "Connection attempt to" << host << "on port" << port << "timed out!";
        socket.abort();  // 取消当前连接尝试
    });

    // 尝试连接目标主机和端口
    socket.connectToHost(host, port);

    // 启动计时器来控制超时
    timer.start(timeout);

    // 等待连接状态变化
    if (socket.waitForConnected(timeout)) {
        // 连接成功
        qDebug() << "Successfully connected to" << host << "on port" << port;
        socket.disconnectFromHost();
        return true;  // 表示端口可用
    } else {
        // 连接失败或超时
        return false;  // 表示端口不可用
    }

    qint64 endtime=getcurtime();
    std::cout << " checksqlonline use time:" << endtime-bgtime << std::endl;
    return false;
}

需要在.pro文件中加入network

QT       += network

3、点击宿主机任务栏上的窗口应用图标时,发送(指定应用激活并置顶到桌面)信息到虚拟机端

需要重写事件函数event(),增加发送事件函数sendevent();

    // 重写事件处理函数
    bool event(QEvent *event) override{
    // 判断是否是窗口激活事件
    if (event->type() == QEvent::WindowActivate) {
        //发送窗口激活命令到window系统端
        sendevent();
        return true;  // 事件已处理
    }

    // 调用基类的事件处理
    return QWidget::event(event);
}

void sendevent(){
    // 创建一个客户端连接
    NetworkClient client(m_Ip, PORT); //

    if (client.m_blink_status==false){
        std::cout << client.m_blink_status << std::endl;
        QMessageBox::information(this, "提示框", "无法链接对端进程管理器,程序将退出");
        this->close();
    }
    else {
        // 创建一个数据包实例并填充数据
        netPackQ packet;
        packet.opCode=1;//发送应用发布窗口信息
        sprintf(packet.keyword, "%d",m_Pid);  // 设置关键字
        // 发送数据
        client.sendData(packet);
    }
}

深入的优化需求场景一

当虚拟机客户端缩小到宿主机(国产系统)的任务栏时,用户进行以上的激活操作是无法让虚拟机窗口显示在宿主机桌面的,以下介绍三种“国产系统(统信UOS、麒麟kylin桌面系统)让缩小到任务栏的第三方应用窗口,激活并显示到桌面”的解决方案(只有最后一个有效)。

方案一:QT的Xlib库
#include <QtCore/qtextstream.h> 
#include <QApplication>
#include <QtWidgets/QWidget>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/extensions/shape.h>
#include <iostream>
#include <QDebug>

void activateWindow(Window windowId) {
    std::cout << __LINE__ << std::endl;
    // 打开 X11 显示连接
    Display *display = XOpenDisplay(nullptr);
    if (!display) {
        qDebug() << "Cannot open X11 display!";
        std::cout << __LINE__ << std::endl;
        return;
    }

    // 获取窗口信息
    XWindowAttributes winAttributes;
    if (!XGetWindowAttributes(display, windowId, &winAttributes)) {
        qDebug() << "Cannot get window attributes!";
        std::cout << __LINE__ << std::endl;
        XCloseDisplay(display);
        return;
    }
    // 将窗口显示到桌面(确保它不被最小化)
    XMapWindow(display, windowId);  // 将窗口显示
    // 将窗口置顶
    XRaiseWindow(display, windowId);

    // 激活窗口(通过设置焦点)
    XSetInputFocus(display, windowId, RevertToParent, CurrentTime);

    // 确保修改生效
    XFlush(display);
    XCloseDisplay(display);
    std::cout << __LINE__ << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc < 2) {//外部传入windowId方式
        std::cout << "Usage: " << argv[0] << " <windowId>" << std::endl;
        return -1;
    }

    // 从命令行参数获取窗口 ID,并将其从字符串转换为 Window 类型
    QByteArray windowIdStr = argv[1];  // 获取参数,假设它是一个十六进制字符串
    Window windowId = strtoul(windowIdStr.constData(), nullptr, 16);  // 将字符串转换为 Window ID(十六进制)
    //Window windowId = 0x07a00006;//可以用这条进行测试

    // 激活该窗口并将其置顶
    activateWindow(windowId);
    return 0;
}

需要在.pro文件中加入库

LIBS += -lX11

以上程序需要的传入的参数windowId是通过命令行wmctrl -x -l获取的第一列信息(后文专项讲解)。经过测试,以上功能可以让缩小到任务栏的窗口应用有响应(变黄色并且闪烁),但未显示到桌面,不符合项目需求(后面有符合需求的解决方案)。

方案二:QT的<Qwindow>
#include <QApplication>
#include<QWindow>
void showwindow(Window windowId){
    QWindow *window = QWindow::fromWinId(windowId);
    if(window){
        window->show();
        window->requestActivate();
        //QGuiApplication::focusWindow();
        window->raise();
        std::cout << __LINE__ << std::endl;
    }else{
        std::cout << __LINE__ << std::endl;
    }
}


int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    if (argc < 2) {
        std::cout << "Usage: " << argv[0] << " <windowId>" << std::endl;
        return -1;
    }

    // 从命令行参数获取窗口 ID,并将其从字符串转换为 Window 类型
    QByteArray windowIdStr = argv[1];  // 获取参数,假设它是一个十六进制字符串
    Window windowId = strtoul(windowIdStr.constData(), nullptr, 16);  // 将字符串转换为 Window ID(十六进制)
    // 假设我们从 wmctrl 获取到的窗口句柄是 0x06400002
    //Window windowId = 0x07a00006;

    // 激活该窗口并将其置顶
    showwindow(windowId);
    return a.exec();
}

以上程序需要的传入的参数windowId是通过命令行wmctrl -x -l获取的第一列信息(后文专项讲解)。经过测试,以上功能可以让缩小到任务栏的窗口应用有响应(变黄色并且闪烁),但未显示到桌面,不符合项目需求(后面有符合需求的解决方案)。

方案三:系统工具wmctl

第一列(例如:0x0c000006,标识为CLASSID)是当前的窗口句柄,第三列(peony-qt-desktop.桌面,标识为CLASS)是窗口类,第四列(xxxx 桌面,标识为win_name)是应用窗口的名称。

系统级工具wmctrl,通过命令行:

wmctrl -x -r <CLASS> -b remove,hidden && wmctrl  -x -a <CLASS>

可以让激活任务栏中的指定类型的窗口激活并显示到桌面,当时有一个缺点,一个类型有多个窗口打开时,只能激活其中一个(窗口ID最小的),比如多个文件管理器缩小到任务栏,此命令只能激活一个显示到桌面,如果某个应用是唯一的,可以使用此命令完成交互效果(比如我需要激活vbox客户端,在我的项目中它是唯一显示到桌面的)。

如果需要根据窗口句柄进行激活,可以达到精准的激活,以下是命令行:

wmctrl -i -r <CLASSID> -b remove,hidden && wmctrl -i -a <CLASSID>

深入的应用场景二

        如果用户右键点击宿主机(国产系统)任务栏上虚拟应用窗口的右键“退出”功能时,通常系统会直接把此窗口杀死,此时同步发送关闭应用命令给到虚拟机进行关闭,这样能达到初级的事件同步闭环。

        深入的场景时,如果发送关闭事件给到虚拟机(window系统),虚拟机的窗口关不掉呢,比如正在打开的记事本有编辑的内容未保存时,会弹窗提示是否关闭,如果选择“不关闭”,此时虚拟机中的应用窗口就会“遗留”。解决这个问题的方法,就需要在宿主机(工程系统)重载窗口的关闭事件closeEvent()来先阻止窗口关闭,等待虚拟机(window)正在的关闭窗口之后,再同步发送真是关闭窗口的信息回来之后,再关闭/杀掉虚拟机窗口。

// 重写 closeEvent 事件---接收在任务栏上右键点击“关闭所有”的菜单功能事件---20250218
    void closeEvent(QCloseEvent *event) override {
    std::cout << "====截获关闭窗口命令,等待window返回关闭指令====" << std::endl;
    event->ignore();  // 拒绝关闭窗口
    sendkill();
}

voidsendkill(){
    // 创建一个客户端连接
    NetworkClient client(m_Ip, PORT);
    if (client.m_blink_status==false){
        std::cout << client.m_blink_status << std::endl;
        QMessageBox::information(this, "提示框", "无法链接对端进程管理器,程序将退出");
        this->close();
    }
    else {
        // 创建一个数据包实例并填充数据
        netPackQ packet;
        packet.opCode = 2;  // 关闭窗口
        sprintf(packet.keyword, "%d",m_Pid);  // 设置关键字
        // 发送数据
        client.sendData(packet);
    }
}

另外,“等待虚拟机(window)正在的关闭窗口之后,再同步发送真是关闭窗口的信息回来之后,再关闭/杀掉虚拟机窗口”这个功能,我另外写了一个TCP服务,接收虚拟机端主动的关闭的窗口进程,然后同步杀死宿主机端对应的进程ID(这一块没有技术难点,这里就不展示了,参照下文window端的服务器部分来写即可)。

window系统(虚拟机)

1、创建TCP服务

创建tcp服务进行侦听

void Mytcpserver::runserver(){
    // 创建 TCP 服务器
    server = new QTcpServer(this);

    // 连接新连接信号到槽函数
    connect(server, &QTcpServer::newConnection, this, &Mytcpserver::onNewConnection);

    // 绑定到 10001 端口
    if (!server->listen(QHostAddress::Any, 10001)) {
        qDebug() << "Server could not start!";
    } else {
        qDebug() << "Server started on port 10001.";
    }
}

2、接收打开应用程序命令,通过后台命令行启动进程

void Mytcpserver::onNewConnection() {
    QTcpSocket *socket = server->nextPendingConnection();
    // 获取对端的IP地址
    QHostAddress clientAddress = socket->peerAddress();
    //qDebug() << "New connection from:" << clientAddress.toString();
    m_peerIp=clientAddress.toString();
    // 连接读取数据信号
    connect(socket, &QTcpSocket::readyRead, [socket,this]() {
        netPackQ packet;
        qint64 bytesReceived = socket->read(reinterpret_cast<char*>(&packet), sizeof(netPackQ));
        if (bytesReceived == sizeof(netPackQ)) {
            // 处理接收到的数据
            qDebug() << "Received date";
            if(packet.opCode== 1){//执行后台命令,打开指定应用。比如:C:\Windows\notepad.exe
                qDebug() << "执行指定程序(PID|PATH). keyword:" << packet.keyword;
                bool bfind=false;
                QStringList recvlist=QString(packet.keyword).split("|");
                if(recvlist.size()>1){
                    QString pid=recvlist[0];
                    QString execpath=recvlist[1];
                    //1.运行程序
                    cmdrun act;
                    act.Run(execpath);
                    //2.获取execpath打开的窗口句柄,并进行绑定
                    //因为执行程序之后,获取窗口句柄存在延时,需要加入轮询机制
                    for(int num=0;num<5;num++){//给停止4秒的轮询机会
                        qDebug() << "num:" << num;
                        //QThread::sleep会导致QTimer定时器休眠,这里要主动调用getwinHwnd()
                        QMap<HWND,QString> hwmdmap=getwinHwnd();//更新窗口句柄清单
                        for (auto &key : hwmdmap.keys()) {
                            HWND hwnd = key;
                            QString hw_execpath = hwmdmap[key];
                            //qDebug() << "hw_execpath:" << hw_execpath << "==>execpath:" << execpath;
                            //if(hw_execpath==execpath && m_ignorehwndlist.count(hwnd)==0){//判断执行程序路径是否一样
                            if(checkpath(hw_execpath,execpath) && m_ignorehwndlist.count(hwnd)==0){//判断执行程序路径是否一样
                                //判断窗口是否已经绑定过,多一个执行程序是有可能开多个窗口的
                                if(m_hwndpidmap.count(hwnd)==0){//如果没有绑定过就进行绑定操作
                                    stHwnd sthd;
                                    sthd.hwnd=hwnd;
                                    sthd.pid=pid;
                                    //sthd.path=execpath;
                                    sthd.path=hw_execpath;
                                    m_hwndpidmap[hwnd]=sthd;

                                    quintptr valueAsUInt = reinterpret_cast<quintptr>(hwnd);
                                    QString msg = QString("执行新程序 hwnd:%1==>pid:%2 path:%3").arg(valueAsUInt).arg(pid).arg(execpath);
                                    qDebug() << msg;
                                    writeLog(msg);
                                    bfind=true;
                                    break;
                                }
                            }
                        }
                        if(bfind) break;
                        QThread::sleep(1);
                    }
                }
                if(bfind)
                    socket->write("sucess\n"); // 回复客户端--绑定成功
                else
                    socket->write("fail\n"); // 回复客户端----绑定失败
            }
        } else {
            qDebug() << "Received incomplete data";
        }
    });

    // 连接断开信号
    connect(socket, &QTcpSocket::disconnected, socket, &QTcpSocket::deleteLater);
}

3、让指定应用窗口激活并指定显示到桌面

            else if(packet.opCode==2){//让指定的应用窗口置顶---接收宿主机的命令
                qDebug() << "执行窗口前置事件(PID). keyword:" << packet.keyword;
                bool bfind=false;
                QString pid=packet.keyword;
                for (auto &key : m_hwndpidmap.keys()){
                    HWND hwnd = key;
                    //QString hw_pid = m_hwndpidmap[hwnd];
                    stHwnd sthd = m_hwndpidmap[hwnd];
                    QString hw_pid = sthd.pid;
                    if(hw_pid==pid){
                        restoreNotepad(hwnd);
                        bfind=true;
                    }
                }
                if(bfind)
                    socket->write("sucess\n"); // 回复客户端--找到pid
                else
                    socket->write("fail\n"); // 回复客户端----未找到pid
            }
// 从任务栏恢复 Notepad 窗口
void Mytcpserver::restoreNotepad(HWND hwnd) {
   //qDebug() << __FUNCTION__ << __LINE__;
   qDebug() << __FUNCTION__ << "触发置顶事件操作";
   ShowWindow(hwnd, SW_RESTORE);  // 恢复窗口
   SetForegroundWindow(hwnd);      // 将窗口置于最前--只能从任务栏中激活,如果没有缩小到任务栏,无法展现在最前面
   SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);// 将窗口窗口置顶
   QThread::sleep(0.5);//有些应用需要延时才能达到置顶+取消置顶的效果,比如资源管理器
   SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);//取消窗口置顶
}

4、监测应用窗口关闭,同步发送信息给宿主机

        else if(packet.opCode == 9){//让指定的应用窗口关闭--接收宿主机的命令
                qDebug() << "关闭指定程序(PID). keyword:" << packet.keyword;
                bool bfind=false;
                QString pid=packet.keyword;
                for (auto &key : m_hwndpidmap.keys()){
                    HWND hwnd = key;
                    //QString hw_pid = m_hwndpidmap[hwnd];
                    stHwnd sthd = m_hwndpidmap[hwnd];
                    QString hw_pid = sthd.pid;
                    if(hw_pid==pid){
                        closeWindowByHwnd(hwnd);//通过窗口句柄关闭窗口时,如果需要保存,是无法直接关闭窗口的
                        bfind=true;
                        quintptr valueAsUInt = reinterpret_cast<quintptr>(hwnd);
                        QString msg = QString("关闭指定程序(PID) hwnd:%1==>pid:%2").arg(valueAsUInt).arg(pid);
                        writeLog(msg);
                    }
                }
                if(bfind)
                    socket->write("sucess\n"); // 回复客户端--找到pid
                else
                    socket->write("fail\n"); // 回复客户端----未找到pid
            }
void Mytcpserver::checkkillproc(){
    QMap<HWND,QString> hwmdmap=getwinHwnd();
    for (auto &key : m_hwndpidmap.keys()) {
        stHwnd sthd = m_hwndpidmap[key];
        QString pid = sthd.pid;
        QString execpath = sthd.path;
        //句柄不存在,说明窗口被关闭了
        if(hwmdmap.count(key)==0){
            bool bret=false;
            bool bpro=false;
            for (auto &hkey : hwmdmap.keys()) {
                HWND hwnd = hkey;
                QString hw_execpath = hwmdmap[hwnd];

                //窗口关闭之后,如果还能匹配当当前开启的窗口的执行路径一样,并且不在过滤清单以及之前的记录中时,对句柄进行迁移(有些应用打开多个窗口)
                if(hw_execpath==execpath && m_ignorehwndlist.count(hwnd)==0 && m_hwndpidmap.count(hwnd)==0){//判断执行程序路径是否一样
                    m_hwndpidmap.remove(key);
                    sthd.hwnd=hwnd;
                    m_hwndpidmap[hwnd]=sthd;
                    bret=true;
                    quintptr old_valueAsUInt = reinterpret_cast<quintptr>(key);
                    quintptr valueAsUInt = reinterpret_cast<quintptr>(hwnd);
                    QString msg = QString("句柄迁移old hwnd:%1==>new hwnd:%2").arg(old_valueAsUInt).arg(valueAsUInt);
                    qDebug() << msg;
                    writeLog(msg);
                    break;
                }
            }
            if(bret==false && bpro==false){
                sendkillpid(pid);
                m_hwndpidmap.remove(key);
                quintptr valueAsUInt = reinterpret_cast<quintptr>(key);
                QString msg = QString("sendkillpid hwnd:%1==>pid:%2").arg(valueAsUInt).arg(pid);
                writeLog(msg);
            }
        }
    }
    //处理被忽略的窗口
    for (auto it = m_ignorehwndlist.begin(); it != m_ignorehwndlist.end(); ) {
        if(hwmdmap.count(*it)==0){
            qDebug() << "忽略名单中的" << *it << "已经被释放";
            it = m_ignorehwndlist.erase(it); // erase返回下一个有效的迭代器
        } else {
            ++it;
        }
    }
}

设置定时器,及过滤已经打开的窗口

Mytcpserver::Mytcpserver(QObject *parent) : QObject(parent) {
    m_bfirst_status=true;
    getwinHwnd();//只取一次已经存在的窗口名单,这是被过滤的名单
    m_bfirst_status=false;
    qDebug() << "已经存在的窗口句柄:";
    QString msg="已经存在的窗口句柄:\n";
    for (const HWND &value : m_ignorehwndlist) {
            //std::cout << value << std::endl;
        qDebug() << "m_ignorehwndlist hwnd:" << value;
        // 转换 HWND 为 quintptr
        quintptr valueAsUInt = reinterpret_cast<quintptr>(value);
        stHwnd sthd=m_ignorehwndmap[value];
        msg += QString("%1 => %2 => %3\n").arg(valueAsUInt).arg(sthd.name).arg(sthd.path);
    }
    writeLog(msg);
    m_settinghwnd=0;
    addtimer = new QTimer(this);
    addtimer->setInterval(1000);
    connect(addtimer,&QTimer::timeout,this,&Mytcpserver::checkkillproc);
    addtimer->start();
    //m_peerIp="192.168.10.92";
}

结尾

        本篇主要是根据项目需求,提供关键的解决思路及方案,不方便提供全部代码,希望能帮到有相关需求场景的读者。


http://www.kler.cn/a/552868.html

相关文章:

  • Jtti.cc:CentOS下PyTorch运行出错怎么办
  • Java集合之ArrayList(含源码解析 超详细)
  • 测试。。。
  • 在高流量下保持WordPress网站的稳定和高效运行
  • C++中为什么有了tuple还需要pair?
  • DeepSeek和ChatGPT的全面对比
  • No.38 蓝队 | 网络安全学习笔记:等级保护与法律法规
  • 华为昇腾服务器部署DeepSeek模型实战
  • 第十七天 WebView组件实战
  • javaSE学习笔记23-线程(thread)-总结
  • YOLOv11-ultralytics-8.3.67部分代码阅读笔记-dataset.py
  • Note25021902_TIA Portal V18 WinCC BCA Ed 需要.NET 3.5 SP1
  • 给出方法步骤 挑战解决 用加密和访问控制保护数据隐私。 调架构、参数与用 GPU 加速优化模型性能。 全面测试解决兼容性问题。
  • 游戏引擎学习第112天
  • 创建三个节点
  • 分布式架构与XXL-JOB
  • 【SpringMVC】Controller的多种方式接收请求参数
  • FastGPT及大模型API(Docker)私有化部署指南
  • JavaAPI(字符串 正则表达式)
  • Linksys WRT54G路由器溢出漏洞分析–运行环境修复