Qt实现简易的多线程TCP服务器(附源码)

news2024/11/14 5:21:57

目录

一.UI界面的设计

二.服务器的启动

三.实现自定义的TcpServer类

1.在widget中声明自定义TcpServer类的成员变量

2.在TcpServer的构造函数中对于我们声明的m_widget进行初始化,m_widget我们用于后续的显示消息等,说白了就是主界面的更新显示等

四.实现自定义的TcpSocket类

1.TcpSocket.h   先忽略掉信号与槽函数,关注构造函数与qintptr类型的 m_sockDesc

五.实现自定义线程类

1.主要关注run函数,其中run函数是继承QThread中的虚函数,需要我们进行重写

2.实现某个客户端断开连接时通过信号与槽让主界面改变

3.实现有新的客户端连接时主界面更新

六.服务器收到多客户端消息进行显示的流程实现

七.服务器发送消息给某个客户端流程

八.服务器发送信息后,要在主页面信息消息更新显示的流程

注意:

效果演示:

源码下载地址:


在初学Qt 中Tcp服务器与客户端的时候,发现每次服务器只能和最后一个连接的客户端进行通信,因为没有用到多线程以及TcpServer中虚函数incomingConnection(),当新的客户端连接的时候,会自动调用incomingConnection函数,在里面产生新的线程来处理通信。

以下来讲讲这个简易的多线程Tcp服务器的实现

一.UI界面的设计

其中包括2个Label,一个LineEdit,两个pushbutton,上面是一个TextBrowser用于服务器显示通信记录,下面一个TextEdit用于发送信息。这样一个简单的界面就搭建完成了~~~

二.服务器的启动

首先我们肯定需要实现点击启动服务器按钮来启动服务器

1.在界面中右击启动按钮 ----> 转到槽

2.实现逻辑,这里直接放代码,其中serverisworking 是我在widget.h 中声明的一个bool类型,用于判断服务器是否启动,同时更改按钮的文本显示内容,以及弹出对话框提示。

//点击开始启动服务器
void Widget::on_pushButton_StartServer_clicked()
{
    //如果服务器没有启动
     if (!this->serverisworking) {
        if(this->m_tcpserver->listen(QHostAddress::Any,ui->lineEdit_Port->text().toUShort())){
            QMessageBox::information(this,"成功!","启动成功!");
            ui->pushButton_StartServer->setText("关闭服务器");
            this->serverisworking = true;
        }
        else {
            QMessageBox::critical(this,"失败!","服务器启动失败请检查设置!");
        }
    }
    //如果服务器正在启动
     else if(this->serverisworking) {
         m_tcpserver->close();
         if(!m_tcpserver->isListening()){
             ui->pushButton_StartServer->setText("启动服务器");
             this->serverisworking = false;
             QMessageBox::information(this,"提示","关闭成功!");
         }
         else {
             QMessageBox::critical(this,"错误","关闭失败,请重试!");
             return;
         }

     }
}

三.实现自定义的TcpServer类

1.首先我们搞清楚,这个类是负责干嘛的?这个类我们要继承QTcpServer,重写虚函数incomingConnetion

2.这是头文件,如果有报错,注意看使用#pragma once没有,因为可能涉及到头文件重复包含

//tcpserver.h
#pragma once
#ifndef TCPSERVER_H
#define TCPSERVER_H

#include <QObject>
#include<QTcpServer>

#include <widget.h>
#include"serverthread.h"

class TcpServer : public QTcpServer
{
    Q_OBJECT
public:
    explicit TcpServer(QObject *parent = nullptr);

private:

    //重写虚函数
    void incomingConnection(qintptr sockDesc);

private:
    
    //用来保存连接进来的套接字,存到ComboBox中
    QList<qintptr> m_socketList;
    
    //再包含一个widget对象
    Widget *m_widget;

};

#endif // TCPSERVER_H

1.在widget中声明自定义TcpServer类的成员变量

//widget.h
#pragma once
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include<QMessageBox>
#include"tcpserver.h"

namespace Ui {
class Widget;
}

class TcpServer;

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = nullptr);
    ~Widget();

public:
    //当有新连接的时候在页面上显示
    void showNewConnection(qintptr sockDesc);

    //断开时显示
    void showDisconnection(qintptr sockDesc);

    //当服务器发送消息后,通知窗口更新消息
    void UpdateServerMsg(qintptr Desc,const QByteArray &msg);


private slots:

    //按钮被触发
    void on_pushButton_StartServer_clicked();

    void on_pushButton_Trans_clicked();


public slots:

    //当服务器收到客户端发送的消息后,更新消息
    void RecvMsg(QString Desc,const QByteArray &msg);


signals :
   void  sendData(qintptr Desc ,const QByteArray &msg);

private:
    Ui::Widget *ui;

    TcpServer *m_tcpserver;

    bool serverisworking;
};

#endif // WIDGET_H

其他的我们先不去讨论,这里我们就声明一个TcpServer类型的m_tcpserver 以及一个bool类型的serverisworking用来判断服务器是否在工作

2.在TcpServer的构造函数中对于我们声明的m_widget进行初始化,m_widget我们用于后续的显示消息等,说白了就是主界面的更新显示等

TcpServer::TcpServer(QObject *parent) : QTcpServer(parent)
{
     m_widget = dynamic_cast<Widget *>(parent);

}

那么,问题来了,我们要重写TcpServer的incomingConnection函数,里面要涉及到线程,那么我们需要去写一个自定义的线程类

//当有新的连接进来的时候会自动调用这个函数,不需要你去绑定信号槽
void TcpServer::incomingConnection(qintptr sockDesc)
{
    //将标识符保存进list
    m_socketList.append(sockDesc);

    //产生线程用于通信
    ServerThread *thread = new ServerThread(sockDesc);

    //窗口中显示有新的连接
    m_widget->showNewConnection(sockDesc);

    //线程中发出断开tcp连接,触发widget中显示断开
    connect(thread, &ServerThread::disconnectTCP, this,[=]{
         m_widget->showDisconnection(sockDesc);
    });



    //当socket 底层有readyread信号的时候  -> 发送socket_getmsg信号  -> 发送socket_getmsg_thread
    //将socket_getmsg_thread 与 widget中 RecvMsg 绑定,RecvMsg 用于处理将收到的消息进行显示
    connect(thread,&ServerThread::socket_getmsg_thread,this->m_widget,&Widget::RecvMsg);

    //当点击发送的时候-> 产生一个SendData 信号  -> 调用线程中SendDataSlot函数用于发送sendData信号来使socket来发送消息
    connect(this->m_widget,&Widget::sendData,thread,&ServerThread::sendDataSlot);

    //当服务器给客户端发送下消息后,会产生一个writeover信号-> 触发线程发送writeover信号给 Tcpserver -> Tcpserver中widget更新消息
    connect(thread,&ServerThread::writeover,[=](qintptr Desc,const QByteArray &msg){
           m_widget->UpdateServerMsg(Desc,msg);
    });


    thread->start();
}

我们要实现线程类,线程的工作是需要每个线程里面都有一个不同的Tcpsocket类实例,但是我们要在不同的线程里面 工作不同的socket,那么我们可以在一个线程中 去使用套接字标识符(socketDesc)去区分不同的套接字,然后服务器也可以通过不同的套接字标识符去进行与不同的套接字进行通信,所以我们需要先去实现一个自定义的TcpSocket类

四.实现自定义的TcpSocket类

1.TcpSocket.h   先忽略掉信号与槽函数,关注构造函数与qintptr类型的 m_sockDesc

为什么是qintptr类型呢,我们查看官方文档,使用qintpt类型作为参数更方便,

#pragma once
#ifndef SERVERSOCKET_H
#define SERVERSOCKET_H

#include <QObject>
#include<QTcpSocket>



class ServerSocket : public QTcpSocket
{
    Q_OBJECT
public:
    explicit ServerSocket(qintptr socketDesc,QObject *parent = nullptr);

signals:
    void socket_getmsg(QString Desc, const QByteArray &msg);

     void writeover(qintptr Desc,const QByteArray &msg);

public slots:
    void sendData(qintptr Desc, const QByteArray &data);

private:
        qintptr m_sockDesc;
};

#endif // SERVERSOCKET_H

五.实现自定义线程类

1.主要关注run函数,其中run函数是继承QThread中的虚函数,需要我们进行重写

//serverthread.h
#pragma once
#ifndef SERVERTHREAD_H
#define SERVERTHREAD_H

#include <QObject>

#include <QThread>
#include<serversocket.h>

class ServerThread : public QThread
{
    Q_OBJECT
public:

    //构造函数初始化套接字标识符
    explicit ServerThread(qintptr sockDesc,QObject *parent = nullptr);

    void run() override;

    ~ServerThread();

signals:
    void disconnectTCP(qintptr m_sockDesc);

    void sendData(qintptr Desc, const QByteArray& msg);

    void socket_getmsg_thread(QString Desc,const QByteArray &msg);

    void  writeover(qintptr Desc,const QByteArray &msg);

public  slots:
    void sendDataSlot(qintptr Desc, const QByteArray& msg);



private:
    qintptr m_socketDesc;

    ServerSocket *m_socket;

};



#endif // SERVERTHREAD_H

2.实现某个客户端断开连接时通过信号与槽让主界面改变

1)我们在run函数中,其实就是对某个对应的用来通信套接字运行一个线程,所以我们在run中,先对m_socket进行初始化,将自身的m_socketDesc 作为参数传给m_socket的有参构造。并且使用TcpSocket的setSocketDescriptor方法对m_socket进行绑定标识符,这样我们每个线程内工作的套接字都是不同的

  m_socket = new ServerSocket(this->m_socketDesc);

    //绑定套接字标识符绑定给自定义套接字对象
    if (!m_socket->setSocketDescriptor(this->m_socketDesc)) {
        return ;
    }

2)在线程中,当该线程中的套接字断开时,底层会发射出disconnected信号,我们线程可以此信号与一个用来发射信号的槽函数绑定起来,实现当套接字发送disconnect信号的时候,线程发射出一个disconnectTcp这样一个自定义信号通知服务器套接字断开,server在调用widget成员的方法实现在主界面中显示断开连接

 //run()中:
    //当套接字断开时,发送底层的disconnected信号
    connect(m_socket, &ServerSocket::disconnected, this, [=]{

        //此信号可以出发server的槽函数然后再调用widget中combobox清除该socketDesc
        emit disconnectTCP(this->m_socketDesc);

        //让该线程中的套接字断开连接
        m_socket->disconnectFromHost();//断开连接

        //线程退出
        this->quit();
    

//incommingConnection中
  //线程中发出断开tcp连接,触发widget中显示断开
    connect(thread, &ServerThread::disconnectTCP, this,[=]{
         m_widget->showDisconnection(sockDesc);
    });
//widget.cpp中
//用以显示连接断开
void Widget::showDisconnection(qintptr sockDesc)
{
    ui->textBrowser_ServerMess->append(QString::number(sockDesc)+"断开了连接");
    
    //通过信号传递的标识符,将其删除
    int index = ui->comboBox_CilentID->findData(sockDesc);

    ui->comboBox_CilentID->removeItem(index);
}

3.实现有新的客户端连接时主界面更新

当有新的客户端连接的时候,会自动调用server中的incommingConnect函数,直接在此函数中调用widget->showNewconnection函数

//incomingConnection函数中:
    //窗口中显示有新的连接
    m_widget->showNewConnection(sockDesc);
//widget.cpp
void Widget::showNewConnection(qintptr sockDesc)
{
    ui->textBrowser_ServerMess->append("有新的连接!,新接入"+QString::number(sockDesc));
    ui->comboBox_CilentID->addItem(QString("%1").arg(sockDesc), sockDesc);
}

 通过这两个连接就可以直接实现有新的客户端连接时主界面更新。

六.服务器收到多客户端消息进行显示的流程实现

//serversocket.cpp
ServerSocket::ServerSocket(qintptr socketDesc,QObject *parent) : QTcpSocket(parent)
{
    this->m_sockDesc = socketDesc;


    connect(this,&ServerSocket::readyRead,this,[=]{

        QString name = QString::number(m_sockDesc);

        QByteArray msg = readAll();

        emit socket_getmsg(name,msg);

    });
}
//serverthread::run()中
    //套接字发出有消息的信号,然后触发线程中发出有消息的信号
    connect(m_socket, &ServerSocket::socket_getmsg, this,[=](QString Desc,const QByteArray &msg){


                emit socket_getmsg_thread(Desc,msg);

    });
//server.cpp
   //当socket 底层有readyread信号的时候  -> 发送socket_getmsg信号  -> 发送socket_getmsg_thread
    //将socket_getmsg_thread 与 widget中 RecvMsg 绑定,RecvMsg 用于处理将收到的消息进行显示
    connect(thread,&ServerThread::socket_getmsg_thread,this->m_widget,&Widget::RecvMsg);
//widget.cpp
//当客户端发送消息,服务器收到后,显示消息
void Widget::RecvMsg(QString Desc,const QByteArray &msg)
{
    ui->textBrowser_ServerMess->append(Desc+":"+msg);
}

实现收到客户端消息进行显示

七.服务器发送消息给某个客户端流程

void Widget::on_pushButton_Trans_clicked()
{
    if(serverisworking){

        //如果连接个数大于0,发送发送消息的信号
        if(ui->comboBox_CilentID->count() >0)
        {
            //发射 发送信号
            emit sendData( ui->comboBox_CilentID->currentText().toInt(), ui->textEdit_SendMess->toPlainText().toUtf8());
            qDebug()<<"发送了sendData信号"<<endl;
            ui->textEdit_SendMess->clear();
        }

    }
    else {
        QMessageBox::critical(this,"错误","请检查连接");
        return;
    }

}
//Tcpserver.cpp  incomingConnection中
    //当点击发送的时候-> 产生一个SendData 信号  -> 调用线程中SendDataSlot函数用于发送sendData信号来使socket来发送消息
    connect(this->m_widget,&Widget::sendData,thread,&ServerThread::sendDataSlot);
void ServerThread::sendDataSlot(qintptr Desc, const QByteArray &msg)
{

     emit sendData(Desc, msg);
}
//run()中
   connect(this,&ServerThread::sendData,m_socket,&ServerSocket::sendData);
void ServerSocket::sendData(qintptr Desc, const QByteArray &msg)
{
    if (Desc == m_sockDesc && !msg.isEmpty()) {
        this->write(msg);

        //发送完毕,发出信号,通知主页面更新聊天框
        emit writeover(Desc,msg);
    }
}

八.服务器发送信息后,要在主页面信息消息更新显示的流程

void ServerSocket::sendData(qintptr Desc, const QByteArray &msg)
{
    if (Desc == m_sockDesc && !msg.isEmpty()) {
        this->write(msg);

        //发送完毕,发出信号,通知主页面更新聊天框
        emit writeover(Desc,msg);
    }
}
//serverthread.cpp
    //socket 发送 writeorver 通知线程发送writeover 用来提醒server中的widget更新消息
    connect(m_socket,&ServerSocket::writeover,this,[=](qintptr Desc, const QByteArray& msg){
            emit writeover(Desc,msg);
    });
//server.cpp
    //当服务器给客户端发送下消息后,会产生一个writeover信号-> 触发线程发送writeover信号给 Tcpserver -> Tcpserver中widget更新消息
    connect(thread,&ServerThread::writeover,[=](qintptr Desc,const QByteArray &msg){
           m_widget->UpdateServerMsg(Desc,msg);
    });
//widget.cpp
//当服务器发送消息后,通知主窗口更新信号
void Widget::UpdateServerMsg(qintptr Desc, const QByteArray &msg)
{
    ui->textBrowser_ServerMess->append("服务器:"+msg+" to "+QString::number(Desc));
}

注意:

注册自定义信号参数,否则信号槽机制使用时会出现保存

#include "widget.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    qRegisterMetaType<qintptr>("qintptr");
    QApplication a(argc, argv);
    Widget w;
    w.show();

    return a.exec();
}

效果演示:

源码下载地址:

yuanzhaoyi/My_project at master (github.com)icon-default.png?t=N7T8https://github.com/yuanzhaoyi/My_project/tree/master

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

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

相关文章

离线安装vscode插件

使用vsix 1.从vscode插件市场All categories Extensions - Visual Studio Marketplace下载需要的vscode插件&#xff0c;拿Prettier - Code formatter插件举例 查看相应版本信息并选择适合的版本进行下载&#xff0c;建议选择最新版本&#xff0c;一般比较稳定 2.将已经下载好…

【[NOIP1999 普及组] Cantor 表】

题目描述 现代数学的著名证明之一是 Georg Cantor 证明了有理数是可枚举的。他是用下面这一张表来证明这一命题的&#xff1a; 我们以 Z 字形给上表的每一项编号。第一项是 1 / 1 1/1 1/1&#xff0c;然后是 1 / 2 1/2 1/2&#xff0c; 2 / 1 2/1 2/1&#xff0c; 3 / 1 3/1…

如何理解Java不可变集合?有什么使用场景?

目录 1. 怎样理解不可变集合&#xff1f; 2. 不可变集合的应用场景 3. 不可变集合的创建和使用&#xff1f; 1. 怎样理解不可变集合&#xff1f; 温馨提示&#xff1a;JDK8版本中不支持不可变集合&#xff0c;建议升级至JDK11版本以上&#xff01;&#xff01;&#xff01; …

FPGA时钟资源详解(4)——区域时钟资源

FPGA时钟系列文章总览&#xff1a;FPGA原理与结构&#xff08;14&#xff09;——时钟资源https://ztzhang.blog.csdn.net/article/details/132307564 目录 一、概述 二、Clock-Capable I/O 三、I/O 时钟缓冲器 —— BUFIO 3.1 I/O 时钟缓冲器 3.2 BUFIO原语 四、区域时钟…

SpringBoot+Vue前后端分离项目在Linux系统中基于Docker打包发布,并上传镜像到阿里镜像私仓

文章目录 SpringBootVue前后端分离项目在Linux系统中基于Docker打包发布&#xff0c;并上传镜像到阿里镜像私仓一、Java项目基于Docker打包发布1.打包应用&#xff0c;将打好的jar包放到我们的linux系统中2.新建dockerfile3.打包镜像4.测试运行5.上传镜像到阿里云免费私仓 二、…

物联网云组态是什么?部署物联网云组态有什么作用?

在信息化与工业化的深度融合进程中&#xff0c;物联网云组态以其独特的优势&#xff0c;正在成为企业数字化转型的重要工具。那么&#xff0c;物联网云组态究竟是什么呢&#xff1f;部署物联网云组态又能给企业带来哪些实质性的好处呢&#xff1f;今天&#xff0c;我们将围绕这…

聚酰亚胺PI材料难于粘接,用什么胶水粘接?那么让我们先一步步的从认识它开始(九): 聚酰亚胺PI薄膜的缺点有哪些

聚酰亚胺PI薄膜的缺点有哪些 聚酰亚胺&#xff08;Polyimide&#xff0c;简称PI&#xff09;薄膜是一种高性能、高温、高压、化学稳定、耐磨损、耐火、耐腐蚀的薄膜材料&#xff0c;具有优良的电绝缘性能、低介电常数和低介电损耗&#xff0c;能够有效阻止电流流动&#xff0c…

主流公链 - Solana

探索Solana区块链&#xff1a;下一代高性能区块链平台 1. Solana简介 Solana是一个高性能的区块链平台&#xff08;TPS能达到10W级别&#xff09;&#xff0c;旨在实现高吞吐量和低延迟的区块链交易处理。它采用了一系列创新技术&#xff0c;其中包括Proof of History (PoH)&a…

状态机高阶讲解-13

2213 01:31:54,290 --> 01:31:56,604 那么这里就出现了一个Hello这样 2214 01:31:56,604 --> 01:31:59,549 说明这个对象已经创建了嘛 2215 01:31:59,549 --> 01:31:59,970 对吧 2216 01:32:04,090 --> 01:32:06,868 如果想看到源代码的话 2217 01:32:06,868 -…

Unity VisionOS开发流程

Unity开发环境 Unity Pro, Unity Enterprise and Unity Industry 国际版 Mac Unity Editor(Apple silicon) visionOS Build Support (experimental) 实验版 Unity 2022.3.11f1 NOTE: 国际版与国服版Pro账通用&#xff0c;需要激活Pro的许可证。官方模板v0.6.2,非Pro版本会打…

“宋仕强论道”系列讲座的文章

“宋仕强论道”系列讲座的文章暨宋仕强先生&#xff08;Huaqiangbei Songshiqiang&#xff09;研究华强北模式和华强北文化的系列文章&#xff0c;再次迎来更新&#xff01;《宋仕强论道华强北科技创新与电子信息产业生态》由新华社新华瞭望网在主页首发&#xff0c;当日点击量…

【python】获取4K壁纸保存到本地文件夹【附源码】

图片信息丰富多彩&#xff0c;许多网站上都有大量精美的图片资源。有时候我们可能需要批量下载这些图片&#xff0c;而手动一个个下载显然效率太低。因此&#xff0c;编写一个简单的网站图片爬取程序可以帮助我们高效地获取所需的图片资源。 目标网站&#xff1a; 如果出现模…

Java毕业设计 基于SSM网上二手书店系统

Java毕业设计 基于SSM网上二手书店系统 SSM jsp 网上二手书店系统 功能介绍 用户&#xff1a;首页 图片轮播 图书查询 图书分类显示 友情链接 登录 注册 图书信息 图片详情 评价信息 加入购物车 资讯信息 资讯详情 个人中心 个人信息 修改密码 意见信息 图书收藏 已经付款 邮…

【MySQL】11. 复合查询(重点)

4. 子查询 子查询是指嵌入在其他sql语句中的select语句&#xff0c;也叫嵌套查询 4.1 单行子查询 返回一行记录的子查询 显示SMITH同一部门的员工 mysql> select * from emp where deptno (select deptno from emp where ename SMITH); -----------------------------…

12种卫星X波段SAR卫星简介

X波段SAR卫星简介及相关12种卫星 简介 SAR卫星是一种主动式微波遥感卫星,通过向地面发射电磁波并接收目标反射信号成像,可全天时全天候对地观测。常见的Sentinel-1是一颗C波段的合成孔径雷达(Synthetic Aperture Radar, SAR)卫星&#xff0c;TerraSAR-X是一颗X波段的SAR卫星。…

蓝桥杯练习题——博弈论

1.必胜态后继至少存在一个必败态 2.必败态后继均为必胜态 Nim游戏 思路 2 3&#xff0c;先手必赢&#xff0c;先拿 1&#xff0c;然后变成 2 2&#xff0c;不管后手怎么拿&#xff0c;先手同样操作&#xff0c;后手一定先遇到 0 0 a1 ^ a2 ^ a3 … ^ an 0&#xff0c;先…

c语言--内存函数的使用(memcpy、memcmp、memset、memmove)

目录 一、memcpy()1.1声明1.2参数1.3返回值1.4memcpy的使用1.5memcpy模拟使用1.6注意 二、memmove()2.1声明2.2参数2.3返回值2.4使用2.5memmove&#xff08;&#xff09;模拟实现 三、memset3.1声明3.2参数3.3返回值3.4使用 四、memcmp()4.1声明4.2参数4.3返回值4.4使用 五、注…

mysql增量备份与修复

MySQL数据库增量恢复 1.一般恢复 将所有备份的二进制日志内容全部恢复 2.基于位置恢复 数据库在某一时间点可能既有错误的操作也有正确的操作 可以基于精准的位置跳过错误的操作 发生错误节点之前的一个节点&#xff0c;上一次正确操作的位置点停止 3.基于时间点恢复 跳过…

群晖NAS安装Video Station结合内网穿透实现公网访问本地影音文件

文章目录 1.使用环境要求&#xff1a;2.下载群晖videostation&#xff1a;3.公网访问本地群晖videostation中的电影&#xff1a;4.公网条件下使用电脑浏览器访问本地群晖video station5.公网条件下使用移动端&#xff08;搭载安卓&#xff0c;ios&#xff0c;ipados等系统的设备…

宽光谱SOA光芯片设计(一)

-本文翻译自由Geoff H. Darling于2003年撰写的文章。尽管文章较早&#xff0c;但可以了解一些SOA底层原理&#xff0c;并可看到早期SOA研究的思路和过程&#xff0c;于今仍有很高借鉴价值。 摘要 本文介绍一种新型宽光谱半导体光放大器&#xff08;SOA&#xff09;技术&#x…