Qt實現客戶端與伺服器訊息傳送與檔案傳輸
客戶端與伺服器之間的資料傳送在很多案例場景裡都會有應用。這裡Jungle用Qt來簡單設計實現一個場景,即:
①兩端:伺服器QtServer和客戶端QtClient
②功能:服務端連線客戶端,兩者能夠互相傳送訊息,傳送檔案,並且顯示檔案傳送進度。
環境:VS2008+Qt4.8.6+Qt設計師
1.基本概念
客戶端與伺服器的基本概念不說了,關於TCP通訊的三次握手等等,在經典教材謝希仁的《計算機網路》裡都有詳細介紹。這裡說下兩者是如何建立起通訊連線的。
①IP地址:首先伺服器和每一個客戶端都有一個地址,即IP地址。(底層的MAC地址,不關心,因為TCP通訊以及IP,是七層架構裡面的網路層、傳輸層了,底層透明)。對於伺服器來說,客戶端的數量及地址是未知的,除非建立了連線。但是對於客戶端來說,必須知道伺服器的地址,因為兩者之間的連線是由客戶端主動發起的。
②埠號:
③TCP連線:總的來說,TCP的連線管理分為單個階段:建立連線->資料傳送->連線釋放。在②裡說到,每個TCP連線的是具體IP地址的主機的兩個埠,即TCP連線的兩個端點由IP地址和埠號組成,這即是套接字(socket)
套接字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;
};
類實現如下:
#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()));
}
這裡說明一下兩個方法:
①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);
}
這裡說明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);
}
從上面那段程式碼可以看出,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);
}
這裡同樣說明兩點:
①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();
}
}
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;
};
實現:
#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();
}
}
6.測試
這裡傳送了幾條訊息,並從客戶端將《Windows網路程式設計技術.pdf》傳到服務端,在服務端的ClientData資料夾裡,該檔案存在,證明程式可行!