1 需求描述
- 实现点对点的文件传输功能;
- 可以批量传输文件。
2 设计思路
说到文件的传输当然使用QTcpSocket,思路还是蛮简单的,发送端维护一个文件队列,然后再将队列中的文件逐个传输到服务端,服务端使用QTcpServer进行监听,并逐个接收文件。
为了实现文件名的统一,客户端每次发送新文件时需要先发送文件名以及文件的大小,这样服务端才能做好后续处理。
3 代码实现
3.1 服务端(接收端)
服务端处理过程:打开监听->处理连接->接收数据->文件落盘
- 服务端首先打开端口监听,以便处理客户端的连接请求,相关代码如下:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
m_pSocket(nullptr),
m_fileSize(0),
m_fileBytesReceived(0)
{
ui->setupUi(this);
setWindowTitle(QApplication::applicationName() + QStringLiteral(" Qt小罗"));
qApp->setStyle(QStyleFactory::create("fusion"));
if (m_server.listen()) {
ui->statusBar->showMessage(QStringLiteral("状态:正在监听!"));
} else {
ui->statusBar->showMessage(QStringLiteral("状态:监听失败!"));
}
ui->labelListenPort->setText(QString::number(m_server.serverPort()));
connect(&m_server, &QTcpServer::newConnection, this, &MainWindow::onNewConnection);
connect(ui->pushButtonCancel, &QPushButton::clicked, this, &MainWindow::close);
}
void MainWindow::onNewConnection()
{
m_pSocket = m_server.nextPendingConnection();
connect(m_pSocket, &QTcpSocket::disconnected, m_pSocket, &QObject::deleteLater);
connect(m_pSocket, &QIODevice::readyRead, this, &MainWindow::onReadyRead);
connect(m_pSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onSocketError(QAbstractSocket::SocketError)));
m_inStream.setDevice(m_pSocket);
m_inStream.setVersion(QDataStream::Qt_5_0);
}
- 然后接收客户端的数据,先接收文件名和文件大小信息,然后接收文件的二进制数据,接收代码如下:
void MainWindow::onReadyRead()
{
if (0 == m_fileSize && m_pSocket->bytesAvailable() > sizeof(qint64)) {
m_inStream >> m_fileSize >> m_fileName;
m_file.setFileName(m_fileName);
if (!m_file.open(QIODevice::WriteOnly)) {
qCritical() << m_file.errorString();
return;
}
ui->plainTextEditLog->appendPlainText(QStringLiteral("正在接收【%1】 ...").arg(m_fileName));
}
qint64 size = qMin(m_pSocket->bytesAvailable(), m_fileSize - m_fileBytesReceived);
QByteArray arry(size, 0);
m_inStream.readRawData(arry.data(), size);
m_file.write(arry);
m_fileBytesReceived += size;
if (m_fileBytesReceived == m_fileSize) {
m_file.close();
QFileInfo info(m_fileName);
ui->plainTextEditLog->appendPlainText(QStringLiteral("成功接收【%1】 -> %2").arg(m_fileName).arg(info.absoluteFilePath()));
reset();
}
}
到这里,服务端已准备就绪,随时准备接收客户端的连接请求。
3.2 客户端(发送端)
客户端处理过程:选择文件列表->连接服务端->连接建立后自动逐个打开队列中的文件并发送
- 文件选择后,点击发送按钮,连接服务端,相关代码如下:
void MainWindow::sendFile()
{
QString address = ui->lineEditIpAddress->text();
int port = ui->spinBoxPort->text().toInt();
QHostAddress hostAddress;
if (!hostAddress.setAddress(address)) {
QMessageBox::critical(this, QStringLiteral("错误"), QStringLiteral("目标网络地址错误!"));
return;
}
if (0 == ui->listWidget->count()) {
QMessageBox::information(this, QStringLiteral("提示"), QStringLiteral("请选择需要发送的文件!"));
addFile();
return;
}
m_fileQueue.clear();
int count = ui->listWidget->count();
for (int i = 0; i < count; ++i) {
QString file = ui->listWidget->item(i)->text();
m_fileQueue.append(file);
QFileInfo info(file);
m_totalFileSize += info.size();
}
m_socket.connectToHost(address, port);
}
- 与服务端的连接建立后,客户端socket状态改变发出信号,对应的槽函数内调用send自动发送文件,相关代码如下:
void MainWindow::onSocketStateChanged(QAbstractSocket::SocketState state)
{
switch (state) {
case QAbstractSocket::UnconnectedState:
qDebug() << m_totalFileSize << " " << m_totalFileBytesWritten;
qDebug() << __FUNCTION__ << "QAbstractSocket::UnconnectedState";
break;
case QAbstractSocket::HostLookupState:
qDebug() << __FUNCTION__ << "QAbstractSocket::HostLookupState";
break;
case QAbstractSocket::ConnectingState:
qDebug() << __FUNCTION__ << "QAbstractSocket::ConnectingState";
break;
case QAbstractSocket::ConnectedState:
qDebug() << __FUNCTION__ << "QAbstractSocket::ConnectedState";
m_timer.restart();
send();
break;
case QAbstractSocket::BoundState:
break;
case QAbstractSocket::ClosingState:
qDebug() << __FUNCTION__ << "QAbstractSocket::ClosingState";
break;
case QAbstractSocket::ListeningState:
break;
default:
break;
}
}
void MainWindow::send()
{
m_file.setFileName(m_fileQueue.dequeue());
if (!m_file.open(QIODevice::ReadOnly)) {
qCritical() << m_file.errorString();
QMessageBox::critical(this, QStringLiteral("错误"), m_file.errorString());
return;
}
m_currentFileSize = m_file.size();
//设置当前文件进度显示格式
ui->currentProgressBar->setFormat(QStringLiteral("%1 : %p%").arg(m_file.fileName()));
m_outStream.setDevice(&m_socket);
m_outStream.setVersion(QDataStream::Qt_5_0);
QFileInfo info(m_file.fileName());
QString fileName = info.fileName();
//发送文件大小及文件名
m_outStream << m_currentFileSize << fileName;
//开始传输文件
QByteArray arry = m_file.read(m_blockSize);
int size = arry.size();
m_outStream.writeRawData(arry.constData(), size);
ui->pushButtonSend->setEnabled(false);
updateProgress(size);
}
- 客户端每次发送数据后,socket会发出bytesWritten信号,通过该信号进行循环发送,直到文件发送完毕,对应的槽函数如下:
void MainWindow::onBytesWritten(const qint64 &bytes)
{
Q_UNUSED(bytes)
QByteArray arry = m_file.read(m_blockSize);
if (arry.isEmpty()) {
reset();
return;
}
int size = arry.size();
m_outStream.writeRawData(arry.constData(), size);
updateProgress(size);
}
void MainWindow::reset()
{
m_file.close();
ui->pushButtonSend->setEnabled(true);
m_currentFileBytesWritten = 0;
if (m_fileQueue.isEmpty()) {
m_socket.close();
qint64 milliseconds = m_timer.elapsed();
QMessageBox::information(this, QStringLiteral("提示"), QStringLiteral("共耗时:%1 毫秒 平均:%2 KB/s")
.arg(QString::number(milliseconds))
.arg(QString::number(((double(m_totalFileSize) / 1024) / (double(milliseconds) / 1000)), 'f', 3)));
m_totalFileSize = 0;
m_totalFileBytesWritten = 0;
} else {
send();
}
}
到此,客户端已经具备批量发送文件的能力了。
4 总结
理清思路后,用Qt实现文件传输功能还是很简单的。当然如果需要的话,也可以让服务端单独启动线程接收文件,这样客户端就可以多个文件同时发送,服务端多个文件同时接收,这样效率貌似会更高,这算是一个拓展吧,不管怎样理清设计思路才是根本所在。
由于文件传输过程中会进行界面显示处理,性能可能会丢失一部分,如果将本例子程序改为纯后台的,效率应该会高一些。