Qt實現客戶端與伺服器消息發送與文件傳輸
來自專欄 Jungle是一個用Qt的工業Robot
原文地址:Qt實現客戶端與伺服器消息發送與文件傳輸 - CSDN博客
客戶端與伺服器之間的數據傳送在很多案例場景里都會有應用。這裡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)的概念: 套接字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_OBJECTpublic: 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 updateSendedFileProgress(qint64); ////更新文件接收進度 void updateReceivedFileProgress();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")+"
"; ////接收數據 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+"
"+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(updateSendedFileProgress(qint64))); connect(fileSocket,SIGNAL(readyRead()),this,SLOT(updateReceivedFileProgress())); 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::updateSendedFileProgress(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::updateReceivedFileProgress(){ 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_OBJECTpublic: 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 updateReceivedFileProgress(); void displayError(QAbstractSocket::SocketError socketError); ///選擇發送的文件 void selectFile(); void sendFile(); ////更新文件傳送進度 void updateSendedFileProgress(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(acceptSendedFileConnection())); //// 文件傳送相關變數初始化 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(updateReceivedFileProgress(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+"
"+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")+"
"; ////接收數據 QString str = this->socket->readAll(); ////顯示 str = "Client "+str_date_time+str; this->ui.browser->append(str);}void QtServer::updateReceivedFileProgress(){ 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::updateSendedFileProgress(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(); }}
https://www.zhihu.com/video/977904011814371328
6.測試效果
見視頻
推薦閱讀: