当前位置:   article > 正文

Qt实现客户端与服务器消息发送与文件传输_qt传输文件

qt传输文件

客户端与服务器之间的数据传送在很多案例场景里都会有应用。这里Jungle用Qt来简单设计实现一个场景,即:
①两端:服务器QtServer和客户端QtClient
②功能:服务端连接客户端,两者能够互相发送消息,传送文件,并且显示文件传送进度。
环境:VS2008+Qt4.8.6+Qt设计师

1.基本概念

客户端与服务器的基本概念不说了,关于TCP通信的三次握手等等,在经典教材谢希仁的《计算机网络》里都有详细介绍。这里说下两者是如何建立起通信连接的。
①IP地址:首先服务器和每一个客户端都有一个地址,即IP地址。(底层的MAC地址,不关心,因为TCP通信以及IP,是七层架构里面的网络层、传输层了,底层透明)。对于服务器来说,客户端的数量及地址是未知的,除非建立了连接。但是对于客户端来说,必须知道服务器的地址,因为两者之间的连接是由客户端主动发起的。
②端口号:软件层面的端口号,指的是“应用层的各种协议进程与运输实体进行层间交互的一种地址”。简而言之,每一个TCP连接都是一个进程,操作系统需要为每个进程分配一个协议端口(即每一个客户端与服务端的连接,不是两台主机的连接,而是两个端口的连接)。但一台主机通常会有很多服务,很多进程,单靠一个IP地址不能标识某个具体的进程或者连接。所以用端口号来标识访问的目标服务器以及服务器的目标服务类型。端口号也有分类,但这不是本文的重点,详见教材。
③TCP连接:总的来说,TCP的连接管理分为单个阶段:建立连接->数据传送->连接释放。在②里说到,每个TCP连接的是具体IP地址的主机的两个端口,即TCP连接的两个端点由IP地址和端口号组成,这即是套接字的概念:
套接字socket=IP:端口号
因此,我们要通过建立套接字来建立服务端与客户端的通信连接。

2.Qt相关类

QTcpSocket:提供套接字
QTcpServer:提供基于TCP的服务端,看官方文档的解释如下:
This class makes it possible to accept incoming TCP connections. You can specify the port or have QTcpServer pick one automatically. You can listen on a specific address or on all the machine’s addresses.
这个解释里面提到两点:
①指定端口:即开通哪一个端口用于建立TCP连接;
②监听:监听①中指定的端口是否有连接的请求。

3.UI设计

客户端:
这里写图片描述
服务端:
这里写图片描述

4.客户端实现

类设计如下:

class QtClient : public QWidget
{
	Q_OBJECT
public:
	QtClient(QWidget *parent = 0, Qt::WFlags flags = 0);
	~QtClient();

	void initTCP();
	void newConnect();

	private slots:
		连接服务器
		void connectServer();
		与服务器断开连接
		void disconnectServer();
		接收服务器发送的数据
		void receiveData();	
		向服务器发送数据
		void sendData();

		浏览文件
		void selectFile();
		发送文件
		void sendFile();
		更新文件发送进度
		void updateFileProgress(qint64);
		更新文件接收进度
		void updateFileProgress();

private:
	Ui::QtClientClass ui;
	QTcpSocket *tcpSocket;
	QTcpSocket *fileSocket;

	///文件传送
	QFile *localFile;
	///文件大小
	qint64 totalBytes;      //文件总字节数
	qint64 bytesWritten;    //已发送的字节数
	qint64 bytestoWrite;    //尚未发送的字节数
	qint64 filenameSize;    //文件名字的字节数
	qint64 bytesReceived;   //接收的字节数
	///每次发送数据大小
	qint64 perDataSize;
	QString filename;
	///数据缓冲区
	QByteArray inBlock;
	QByteArray outBlock;

	系统时间
	QDateTime current_date_time;
	QString str_date_time;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

类实现如下:

#include "qtclient.h"

QtClient::QtClient(QWidget *parent, Qt::WFlags flags)
	: QWidget(parent, flags)
{
	ui.setupUi(this);
	this->initTCP();

	/文件传送相关变量初始化
	///每次发送数据大小为64kb
	perDataSize = 64*1024;
	totalBytes = 0;
	bytestoWrite = 0;
	bytesWritten = 0;
	bytesReceived = 0;
	filenameSize = 0;

	connect(this->ui.pushButton_openFile,SIGNAL(clicked()),this,SLOT(selectFile()));
	connect(this->ui.pushButton_sendFile,SIGNAL(clicked()),this,SLOT(sendFile()));
}

QtClient::~QtClient()
{
}

void QtClient::initTCP()
{
	this->tcpSocket = new QTcpSocket(this);
	connect(ui.pushButton_connect,SIGNAL(clicked()),this,SLOT(connectServer()));
	connect(ui.pushButton_disconnect,SIGNAL(clicked()),this,SLOT(disconnectServer()));
	connect(ui.pushButton_send,SIGNAL(clicked()),this,SLOT(sendData()));
}

void QtClient::connectServer()
{
	tcpSocket->abort();
	tcpSocket->connectToHost("127.0.0.1",6666);
	connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(receiveData()));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

这里说明一下两个方法:
①abort():官方文档给出了说明:
Aborts the current connection and resets the socket. Unlike disconnectFromHost(), this function immediately closes the socket, discarding any pending data in the write buffer.
即终止之前的连接,重置套接字。
②connectToHost():给定IP地址和端口号,连接服务器。这里我们给127.0.0.1,即本机地址,端口号随便给了个,一般来说介于49152~65535之间的都行。

void QtClient::disconnectServer()
{
	//这里不做实现了,大家自己定义吧O(∩_∩)O哈哈~
}

void QtClient::receiveData()
{
	/获取当前时间
	current_date_time = QDateTime::currentDateTime();
	str_date_time = current_date_time.toString("yyyy-MM-dd hh:mm:ss")+"\n";
	接收数据
	QString str = tcpSocket->readAll();
	显示
	str = "Server "+str_date_time+str;
	this->ui.textEdit->append(str);
}

void QtClient::sendData()
{
	发送数据
	QString str = ui.lineEdit->text();
	this->tcpSocket->write(ui.lineEdit->text().toLatin1());
	显示
	current_date_time = QDateTime::currentDateTime();
	str_date_time = current_date_time.toString("yyyy-MM-dd hh:mm:ss");
	str = "You "+str_date_time+"\n"+str;
	ui.textEdit->append(str);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

这里说明QTCPSocket的两个方法:
①readAll():如果把一个socket比作一个通讯管道,那么这个方法的作用是读取该管道里的所有数据(格式为QByteArray);
②write():同上面的比喻,这个方法的作用是向管道里塞数据。

void QtClient::selectFile()
{
	this->fileSocket = new QTcpSocket(this);
	fileSocket->abort();
	fileSocket->connectToHost("127.0.0.1",8888);
	文件传送进度更新
	connect(fileSocket,SIGNAL(bytesWritten(qint64)),this,SLOT(updateFileProgress(qint64)));
	connect(fileSocket,SIGNAL(readyRead()),this,SLOT(updateFileProgress()));

	this->ui.progressBar->setValue(0);
	this->filename = QFileDialog::getOpenFileName(this,"Open a file","/","files (*)");
	ui.lineEdit_filename->setText(filename);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

从上面那段代码可以看出,Jungle设计了两个socket,一个用于发送字符数据,另一个套接字用于传送文件。两个socket分别使用两个不同的端口。在服务端里也是这样,待会儿不再解释了。

void QtClient::sendFile()
{
	this->localFile = new QFile(filename);
	if(!localFile->open(QFile::ReadOnly))
	{
		ui.textEdit->append(tr("Client:open file error!"));
		return;
	}
	///获取文件大小
	this->totalBytes = localFile->size();
	QDataStream sendout(&outBlock,QIODevice::WriteOnly);
	sendout.setVersion(QDataStream::Qt_4_8);
	QString currentFileName = filename.right(filename.size()-filename.lastIndexOf('/')-1);
	
	qDebug()<<sizeof(currentFileName);
	保留总代大小信息空间、文件名大小信息空间、文件名
	sendout<<qint64(0)<<qint64(0)<<currentFileName;
	totalBytes += outBlock.size();
	sendout.device()->seek(0);
	sendout<<totalBytes<<qint64((outBlock.size()-sizeof(qint64)*2));

	bytestoWrite = totalBytes-fileSocket->write(outBlock);
	outBlock.resize(0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

这里同样说明两点:
①setVision():设定数据序列的版本,官方文档里说明这个不是必须的,但是推荐我们要去进行这一步的工作。我这里是Qt4.8.6,所以设定为Qt4.8.见下图(截自Qt官方文档)
这里写图片描述
②qint64:这个类型在Jungle之前的博客里也提到过,是指qt的无符号的整型,64位。

void QtClient::updateFileProgress(qint64 numBytes)
{
	已经发送的数据大小
	bytesWritten += (int)numBytes;

	如果已经发送了数据
	if(bytestoWrite > 0)
	{
		outBlock = localFile->read(qMin(bytestoWrite,perDataSize));
		///发送完一次数据后还剩余数据的大小
		bytestoWrite -= ((int)fileSocket->write(outBlock));
		///清空发送缓冲区
		outBlock.resize(0);
	}
	else
		localFile->close();

	更新进度条
	this->ui.progressBar->setMaximum(totalBytes);
	this->ui.progressBar->setValue(bytesWritten);

	如果发送完毕
	if(bytesWritten == totalBytes)
	{
		localFile->close();
		//fileSocket->close();
	}
}

void QtClient::updateFileProgress()
{
	QDataStream inFile(this->fileSocket);
	inFile.setVersion(QDataStream::Qt_4_8);

	///如果接收到的数据小于16个字节,保存到来的文件头结构
	if(bytesReceived <= sizeof(qint64)*2)
	{
		if((fileSocket->bytesAvailable()>=(sizeof(qint64))*2) && (filenameSize==0))
		{
			inFile>>totalBytes>>filenameSize;
			bytesReceived += sizeof(qint64)*2;
		}
		if((fileSocket->bytesAvailable()>=filenameSize) && (filenameSize != 0))
		{
			inFile>>filename;
			bytesReceived += filenameSize;
			filename = "ServerData/"+filename;
			localFile = new QFile(filename);
			if(!localFile->open(QFile::WriteOnly))
			{
				qDebug()<<"Server::open file error!";
				return;
			}
		}
		else
			return;
	}
	/如果接收的数据小于总数据,则写入文件
	if(bytesReceived < totalBytes)
	{
		bytesReceived += fileSocket->bytesAvailable();
		inBlock = fileSocket->readAll();
		localFile->write(inBlock);
		inBlock.resize(0);
	}

	数据接收完成时
	if(bytesReceived == totalBytes)
	{
		this->ui.textEdit->append("Receive file successfully!");
		bytesReceived = 0;
		totalBytes = 0;
		filenameSize = 0;
		localFile->close();
		//fileSocket->close();
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77

5.服务端实现

类的设计:

class QtServer : public QWidget
{
	Q_OBJECT

public:
	QtServer(QWidget *parent = 0, Qt::WFlags flags = 0);
	~QtServer();

	QTcpServer *server;
	QTcpSocket *socket;
	QTcpServer *fileserver;
	QTcpSocket *filesocket;

private slots:  
	void sendMessage(); 
	void acceptConnection();
	接收客户端发送的数据
	void receiveData();

	void acceptFileConnection();
	void updateFileProgress();
	void displayError(QAbstractSocket::SocketError socketError);

	///选择发送的文件
	void selectFile();
	void sendFile();
	更新文件传送进度
	void updateFileProgress(qint64);

private:
	Ui::QtServerClass ui;

	传送文件相关变量
	qint64 totalBytes;
	qint64 bytesReceived;
	qint64 bytestoWrite;
	qint64 bytesWritten;
	qint64 filenameSize;
	QString filename;
	///每次发送数据大小
	qint64 perDataSize;
	QFile *localFile;
	本地缓冲区
	QByteArray inBlock;
	QByteArray outBlock;

	系统时间
	QDateTime current_date_time;
	QString str_date_time;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

实现:

#include "qtserver.h"
#include <QDataStream>
#include <QMessageBox>
#include <QString>
#include <QByteArray>

QtServer::QtServer(QWidget *parent, Qt::WFlags flags)
	: QWidget(parent, flags)
{
	ui.setupUi(this);

	this->socket = new QTcpSocket(this);
	this->server = new QTcpServer(this);
	///开启监听
	this->server->listen(QHostAddress::Any,6666);
	connect(this->server,SIGNAL(newConnection()),this,SLOT(acceptConnection()));
	connect(ui.pushButton_send,SIGNAL(clicked()),this,SLOT(sendMessage()));

	///文件传送套接字
	this->filesocket = new QTcpSocket(this);
	this->fileserver = new QTcpServer(this);
	this->fileserver->listen(QHostAddress::Any,8888);
	connect(this->fileserver,SIGNAL(newConnection()),this,SLOT(acceptFileConnection()));

	 文件传送相关变量初始化
	bytesReceived = 0;
	totalBytes = 0;
	filenameSize = 0;
	connect(ui.pushButton_selectFile,SIGNAL(clicked()),this,SLOT(selectFile()));
	connect(ui.pushButton_sendFile,SIGNAL(clicked()),this,SLOT(sendFile()));
}

QtServer::~QtServer()
{

}

void QtServer::acceptConnection()
{
	返回一个socket连接
	this->socket = this->server->nextPendingConnection();
	connect(socket,SIGNAL(readyRead()),this,SLOT(receiveData()));
}

void QtServer::acceptFileConnection()
{
	bytesWritten = 0;
	///每次发送数据大小为64kb
	perDataSize = 64*1024;
	this->filesocket = this->fileserver->nextPendingConnection();
	///接受文件
	connect(filesocket,SIGNAL(readyRead()),this,SLOT(updateFileProgress()));	
	connect(filesocket,SIGNAL(error(QAbstractSocket::SocketError)),this,SLOT(updateFileProgress(qint64)));
	connect(filesocket,SIGNAL(bytesWritten(qint64)),this,SLOT(displayError(QAbstractSocket::SocketError socketError)));
}

void QtServer::sendMessage()
{
	this->socket->write(ui.lineEdit->text().toLatin1());
	显示
	current_date_time = QDateTime::currentDateTime();
	str_date_time = current_date_time.toString("yyyy-MM-dd hh:mm:ss");
	QString str = "You "+str_date_time+"\n"+ui.lineEdit->text();
	ui.browser->append(str);
}

void QtServer::receiveData()
{
	/获取当前时间
	current_date_time = QDateTime::currentDateTime();
	str_date_time = current_date_time.toString("yyyy-MM-dd hh:mm:ss")+"\n";

	接收数据
	QString str = this->socket->readAll();

	显示
	str = "Client "+str_date_time+str;
	this->ui.browser->append(str);
}

void QtServer::updateFileProgress()
{
	QDataStream inFile(this->filesocket);
	inFile.setVersion(QDataStream::Qt_4_8);
	
	///如果接收到的数据小于16个字节,保存到来的文件头结构
	if(bytesReceived <= sizeof(qint64)*2)
	{
		if((filesocket->bytesAvailable()>=(sizeof(qint64))*2) && (filenameSize==0))
		{
			inFile>>totalBytes>>filenameSize;
			bytesReceived += sizeof(qint64)*2;
		}
		if((filesocket->bytesAvailable()>=filenameSize) && (filenameSize != 0))
		{
			inFile>>filename;
			bytesReceived += filenameSize;
			接收的文件放在指定目录下
			filename = "ClientData/"+filename;
			localFile = new QFile(filename);
			if(!localFile->open(QFile::WriteOnly))
			{
				qDebug()<<"Server::open file error!";
				return;
			}
		}
		else
			return;
	}
	/如果接收的数据小于总数据,则写入文件
	if(bytesReceived < totalBytes)
	{
		bytesReceived += filesocket->bytesAvailable();
		inBlock = filesocket->readAll();
		localFile->write(inBlock);
		inBlock.resize(0);
	}
	更新进度条显示
	this->ui.progressBar_fileProgress->setMaximum(totalBytes);
	this->ui.progressBar_fileProgress->setValue(bytesReceived);
	数据接收完成时
	if(bytesReceived == totalBytes)
	{
		this->ui.browser->append("Receive file successfully!");
		bytesReceived = 0;
		totalBytes = 0;
		filenameSize = 0;
		localFile->close();
		//filesocket->close();
	}
}

void QtServer::displayError(QAbstractSocket::SocketError socketError)
{
	qDebug()<<socket->errorString();
	socket->close();
}

void QtServer::selectFile()
{
	filesocket->open(QIODevice::WriteOnly);
	文件传送进度更新
	connect(filesocket,SIGNAL(bytesWritten(qint64)),this,SLOT(updateFileProgress(qint64)));

	this->filename = QFileDialog::getOpenFileName(this,"Open a file","/","files (*)");
	ui.lineEdit_fileName->setText(filename);
}

void QtServer::sendFile()
{
	this->localFile = new QFile(filename);
	if(!localFile->open(QFile::ReadOnly))
	{
		return;
	}
	///获取文件大小
	this->totalBytes = localFile->size();
	QDataStream sendout(&outBlock,QIODevice::WriteOnly);
	sendout.setVersion(QDataStream::Qt_4_8);
	QString currentFileName = filename.right(filename.size()-filename.lastIndexOf('/')-1);

	保留总代大小信息空间、文件名大小信息空间、文件名
	sendout<<qint64(0)<<qint64(0)<<currentFileName;
	totalBytes += outBlock.size();
	sendout.device()->seek(0);
	sendout<<totalBytes<<qint64((outBlock.size()-sizeof(qint64)*2));

	bytestoWrite = totalBytes-filesocket->write(outBlock);
	outBlock.resize(0);
}

void QtServer::updateFileProgress(qint64 numBytes)
{
	已经发送的数据大小
	bytesWritten += (int)numBytes;

	如果已经发送了数据
	if(bytestoWrite > 0)
	{
		outBlock = localFile->read(qMin(bytestoWrite,perDataSize));
		///发送完一次数据后还剩余数据的大小
		bytestoWrite -= ((int)filesocket->write(outBlock));
		///清空发送缓冲区
		outBlock.resize(0);
	}
	else
		localFile->close();

	如果发送完毕
	if(bytesWritten == totalBytes)
	{
		localFile->close();
		//filesocket->close();
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195

6.测试

这里写图片描述
这里写图片描述
这里发送了几条消息,并从客户端将《Windows网络编程技术.pdf》传到服务端,在服务端的ClientData文件夹里,该文件存在,证明程序可行!

7.源码

程序包:https://download.csdn.net/download/sinat_21107433/10823712

Github更新到了vs2013+Qt5,源代码上传到Git了,地址:https://github.com/FengJungle/QtSocket.git

欢迎关注知乎专栏Jungle是一个用Qt的工业Robot
欢迎关注Jungle的微信公众号:Jungle笔记
在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/305147?site
推荐阅读
相关标签
  

闽ICP备14008679号