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()
{
while (m_pSocket->bytesAvailable()) {
if (0 == m_fileSize && m_pSocket->bytesAvailable() > sizeof(qint64)) {
m_inStream >> m_fileSize >> m_fileName;
m_file.close();
m_file.setFileName(m_fileName);
if (!m_file.open(QIODevice::WriteOnly)) {
qCritical() << m_file.errorString();
return;
}
ui->plainTextEditLog->appendPlainText(QStringLiteral("正在接收【%1】 ...").arg(m_fileName));
} else {
qint64 size = qMin(m_pSocket->bytesAvailable(), m_fileSize - m_fileBytesReceived);
if (size == 0) {
reset();
continue;
}
QByteArray arry(size, 0);
m_inStream.readRawData(arry.data(), size);
m_file.write(arry);
m_fileBytesReceived += size;
if (m_fileBytesReceived == m_fileSize) {
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:
m_file.close();
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.close();
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()
{
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實現文件傳輸功能還是很簡單的。當然如果需要的話,也可以讓服務端單獨啟動線程接收文件,這樣客戶端就可以多個文件同時發送,服務端多個文件同時接收,這樣效率貌似會更高,這算是一個拓展吧,不管怎樣理清設計思路才是根本所在。
由於文件傳輸過程中會進行界面顯示處理,性能可能會丟失一部分,如果將本例子程序改為純后台的,效率應該會高一些。