詳解 QT 多線程 TCP 文件接收服務(wù)器實(shí)例
本文介紹的是QT 多線程 TCP 文件接收服務(wù)器實(shí)例,如果你想深入了解這方面的資料的話,請關(guān)注本文末尾,不多說,我們先來看內(nèi)容。
因?yàn)轫?xiàng)目需要,需要跨平臺編寫網(wǎng)絡(luò)傳輸程序。
目標(biāo):
用戶端:linux(arm平臺),完成文件的傳輸
服務(wù)器:windows ,使用多線程的文件的接收
實(shí)現(xiàn)無線的文件傳輸功能
用戶端程序,用標(biāo)準(zhǔn)的socket完成文件傳輸?shù)墓δ埽a如下:
- // Linux下網(wǎng)絡(luò)編程,客戶端程序代碼
- //程序運(yùn)行參數(shù):
- // ./client IPADDRESS PORTNUMBER
- // (其中IPADDRESS是服務(wù)端IP地址,PORTNUMBER是服務(wù)端用于監(jiān)聽的端口)
- //
- #include <stdio.h>
- #include <stdlib.h>
- #include <errno.h>
- #include <string.h>
- #include <netdb.h>
- #include <ctype.h>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <sys/time.h>
- //用這個my_read()函數(shù)代替本來的read()函數(shù)原因有以下幾點(diǎn):
- //
- //ssize_t read(int fd,void *buf,size_t nbyte)
- //read函數(shù)是負(fù)責(zé)從fd中讀取內(nèi)容。當(dāng)讀成功時,read返回實(shí)際所讀的字節(jié)數(shù);如果
- //返回的值是0,表示已經(jīng)讀到文件的結(jié)束了;小于0表示出現(xiàn)了錯誤。
- //
- // 1)如果錯誤為EINTR說明read出錯是由中斷引起的,繼續(xù)讀。
- // 2)如果是ECONNREST表示網(wǎng)絡(luò)連接出了問題,停止讀取。
- size_t min(size_t a,size_t b)
- {
- return( (a<b) ? a : b);
- }
- ssize_t my_write(int fd,void *buffer,size_t length)
- {
- size_t bytes_left; //尚未寫的文件大小
- size_t writesize = 4* 1024;
- ssize_t written_bytes; //已經(jīng)寫的文件大小
- char *ptr;
- ptr=buffer;
- bytes_left=length;
- while(bytes_left>0)
- {
- //開始寫
- written_bytes=write(fd,ptr,min(bytes_left,writesize));
- //出現(xiàn)了寫錯誤
- if(written_bytes<=0)
- {
- //中斷錯誤,置零重新寫
- if(errno==EINTR)
- written_bytes=0;
- //其他錯誤,退出不寫了
- else
- return(-1);
- }
- //從剩下的地方繼續(xù)寫
- bytes_left-=written_bytes;
- ptr+=written_bytes;
- }
- return(0);
- }
- : int main(int argc, char *argv[])
- {
- int sockfd; //通信套接字描述符
- char *buffer; //緩沖區(qū)
- struct sockaddr_in server_addr; //服務(wù)器地址結(jié)構(gòu)
- struct hostent *host; //主機(jī)地址與名稱信息結(jié)構(gòu)
- int nbytes; //端口號、字節(jié)數(shù)
- FILE *fp; //文件指針
- int nfilesize; //文件大小
- char str[128]; //文件名
- char yes='Y'; //流程控制
- struct timeval tpstart,tpend; //用于記錄文件傳輸時間
- float timeuse; //文件傳輸所用時間
- char *hostname="127.0.0.1";//主機(jī)名/ip地址
- int portnumber=4321;//端口號
- //提示用戶輸入完整的命令行參數(shù)
- if(argc!=3)
- {
- fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]);
- printf("using defaults:\nhostname: %s\nportnumber: %d\n",hostname,portnumber);
- }
- //如果利用用戶輸入的域名無法獲得正確的主機(jī)地址信息,則退出
- if (argc>1)
- {
- if((host=gethostbyname(argv[1]))==NULL)
- {
- fprintf(stderr,"Gethostname error\n");
- exit(1);
- }
- }
- else
- if((host=gethostbyname(hostname))==NULL)
- {
- fprintf(stderr,"Gethostname error\n");
- exit(1);
- }
- if(argc>2)
- //如果用戶輸入的端口不正確,則提示并退出
- if((portnumber=atoi(argv[2]))<0)
- {
- fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]);
- exit(1);
- }
- //客戶程序開始建立 sockfd描述符,創(chuàng)建通信套接字
- if((sockfd=socket(AF_INET,SOCK_STREAM,6))==-1)
- {
- fprintf(stderr,"Socket Error:%s\a\n",strerror(errno));
- exit(1);
- }
- //客戶程序填充服務(wù)端的地址信息
- bzero(&server_addr,sizeof(server_addr));
- server_addr.sin_family=AF_INET;
- server_addr.sin_port=htons(portnumber);
- server_addr.sin_addr=*((struct in_addr *)host->h_addr);
- //客戶程序發(fā)起連接請求
- if(connect(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
- {
- fprintf(stderr,"Connect Error:%s\a\n",strerror(errno));
- exit(1);
- }
- printf("Connection Succeed!\n");
- while (toupper(yes)=='Y')
- {
- //提示用戶輸入文件路徑
- printf("Please input the file location:");
- scanf("%s",str);
- while ((fp=fopen(str,"r"))==NULL)
- {
- fprintf(stderr,"File open error,Retry!\n");
- printf("Please input the file location:");
- scanf("%s",str);
- //exit(1);
- }
- getchar();
- //獲取打開的文件的大小,并將文件整個讀入內(nèi)存中
- fseek(fp,0L,SEEK_END);
- nfilesize=ftell(fp);
- rewind(fp);//most important!!!!!
- char *p=(char *)malloc(nfilesize);
- if (fread((void *)p,nfilesize,1,fp)<1) {
- if (feof(fp))
- printf("read end of file!\nquit!\n");
- else
- printf("read file error!\n");
- }
- //將要傳輸?shù)奈募拇笮⌒畔l(fā)送給客戶端
- if (my_write(sockfd,(void *)&nfilesize,4)==-1)
- {
- fprintf(stderr,"Write Error:%s\n",strerror(errno));
- exit(1);
- }
- printf("Begin to transfer the file!\n");
- getchar();
- //獲取傳輸初始時間
- gettimeofday(&tpstart,NULL);
- //傳輸文件
- if (my_write(sockfd,p,nfilesize)==-1)
- {
- fprintf(stderr,"Transfer failed!");
- exit(1);
- }
- //獲取傳輸結(jié)束時間
- gettimeofday(&tpend,NULL);
- //計(jì)算整個傳輸用時
- timeuse=1000000*(tpend.tv_sec-tpstart.tv_sec)+(tpend.tv_usec-tpstart.tv_usec);
- timeuse/=1000000;
- printf("Transfer Succeed!\nFile Name: %s\nFile Size: %d bytes\nTotal Time:
- %f seconds\nTransfer Speed: %f bytes/second",str,nfilesize,timeuse,((float)nfilesize)/timeuse);
- free(p); //釋放文件內(nèi)存
- fclose(fp); //關(guān)閉文件
- // printf("\nTransfer another file?(Y/N): ");
- //scanf("%c",&yes);
- // getchar();
- yes='n';
- }
- //結(jié)束通訊,關(guān)閉套接字,關(guān)閉連接
- close(sockfd);
- printf("\nClient Exit!~~\n");
- exit(0);
- }
服務(wù)器端代碼列表:
具體代碼如下:
- “tcpserver.h”
- #ifndef TCPSERVER_H
- #define TCPSERVER_H
- #include <QTcpServer>
- //繼承自QTcpServer,完成TCPSEVER的建立的類
- class TcpServer : public QTcpServer
- {
- Q_OBJECT
- public:
- explicit TcpServer(QObject *parent = 0);
- //此信號用來更新UI
- signals:
- void bytesArrived(qint64,qint32,int);
- //QTcpServer類自帶的函數(shù),詳情參考Class Reference
- protected:
- void incomingConnection(int socketDescriptor);
- };
- #endif // TCPSERVER_H
TCPSERVER繼承QTcpServer,主要完成TCP服務(wù)器的建立,類中最主要的成員函數(shù)為虛函數(shù)incomingConnection(int socketDescriptor)的定義。
#p#
“tcpserver.cpp”
- #include "tcpserver.h"
- #include "tcpthread.h"
- //構(gòu)造函數(shù)
- TcpServer::TcpServer(QObject *parent) :
- QTcpServer(parent)
- {
- }
- //重新定義了incomingConnection這個虛函數(shù),
- //開辟一個新的tcpsocket線程,從TcpServer獲得socketDescriptor,
- //并完成相應(yīng)的信號連接。
- void TcpServer::incomingConnection(int socketDescriptor)
- {
- TcpThread *thread = new TcpThread(socketDescriptor, this);
- //將線程結(jié)束信號與線程的deleteLater槽關(guān)聯(lián)
- connect(thread, SIGNAL(finished()),
- thread, SLOT(deleteLater()));
- //關(guān)聯(lián)相應(yīng)的UI更新槽
- connect(thread,SIGNAL(bytesArrived(qint64,qint32,int)),
- this,SIGNAL(bytesArrived(qint64,qint32,int)));
- //QT的線程都是從start開始,調(diào)用run()函數(shù)
- thread->start();
- }
極其簡單的構(gòu)造函數(shù),在incomingConnection()中,定義一個線程TcpThread,并將socketDescriptor傳遞給其構(gòu)造函數(shù),完成線程的創(chuàng)建,并且調(diào)用QThread的start函數(shù),開始執(zhí)行線程的虛函數(shù)run()。
“tcpthread.h”
- #ifndef TCPTHREAD_H
- #define TCPTHREAD_H
- #include <QThread>
- #include <QTcpSocket>
- #include <QtNetwork>
- //繼承QThread的TCP傳輸線程
- //主要是完成run()虛函數(shù)的定義
- //還有一些輔助變量的聲明
- class QFile;
- class QTcpSocket;
- class TcpThread : public QThread
- {
- Q_OBJECT
- public:
- TcpThread(int socketDescriptor, QObject *parent);
- void run();
- signals:
- void error(QTcpSocket::SocketError socketError);
- void bytesArrived(qint64,qint32,int);
- public slots:
- void receiveFile();
- private:
- int socketDescriptor;
- qint64 bytesReceived; //收到的總字節(jié)
- qint64 byteToRead; //準(zhǔn)備讀取的字節(jié)
- qint32 TotalBytes; //總共傳輸?shù)淖止?jié)
- QTcpSocket *tcpSocket;
- QHostAddress fileName; //文件名
- QFile *localFile;
- QByteArray inBlock; //讀取緩存
- };
- #endif // TCPTHREAD_H
繼承自QThread類,在此線程中完成TCPSOCKET的建立,和文件的接收。
“tcpthread.cpp”
- #include "tcpthread.h"
- #include <QtGui>
- #include <QtNetwork>
- //構(gòu)造函數(shù)完成簡單的賦值/
- TcpThread::TcpThread(int socketDescriptor, QObject *parent):
- QThread(parent),socketDescriptor(socketDescriptor)
- {
- bytesReceived = 0;
- }
- //因?yàn)镼T的線程的執(zhí)行都是從run()開始,
- //所以在此函數(shù)里完成tcpsocket的創(chuàng)建,相關(guān)信號的綁定
- void TcpThread::run()
- {
- tcpSocket = new QTcpSocket;
- //將Server傳來的socketDescriptor與剛創(chuàng)建的tcpSocket關(guān)聯(lián)
- if (!tcpSocket->setSocketDescriptor(socketDescriptor)) {
- emit error(tcpSocket->error());
- return;
- }
- qDebug()<<socketDescriptor;
- : //這是重中之重,必須加Qt::BlockingQueuedConnection!
- //這里困擾了我好幾天,原因就在與開始沒加,默認(rèn)用的Qt::AutoConnection。
- //簡單介紹一下QT信號與槽的連接方式:
- //Qt::AutoConnection表示系統(tǒng)自動選擇相應(yīng)的連接方式,如果信號與槽在同一線程,就采用Qt::DirectConnection,
- 如果信號與槽不在同一線程,將采用Qt::QueuedConnection的連接方式。
- //Qt::DirectConnection表示一旦信號產(chǎn)生,立即執(zhí)行槽函數(shù)。
- //Qt::QueuedConnection表示信號產(chǎn)生后,將發(fā)送Event給你的receiver所在的線程,postEvent(QEvent::MetaCall,...),
- slot函數(shù)會在receiver所在的線程的event loop中進(jìn)行處理。
- //Qt::BlockingQueuedConnection表示信號產(chǎn)生后調(diào)用sendEvent(QEvent::MetaCall,...),
- 在receiver所在的線程處理完成后才會返回;只能當(dāng)sender,receiver不在同一線程時才可以。
- //Qt::UniqueConnection表示只有它不是一個重復(fù)連接,連接才會成功。如果之前已經(jīng)有了一個鏈接(相同的信號連接到同一對象的同一個槽上),那么連接將會失敗并將返回false。
- //Qt::AutoCompatConnection與QT3保持兼容性
- //說明一下,對于任何的QThread來說,其線程只存在于run()函數(shù)內(nèi),其它的函數(shù)都不在線程內(nèi),所以此處要采用Qt::BlockingQueuedConnection,
- //因?yàn)楫?dāng)SOCKET有數(shù)據(jù)到達(dá)時就會發(fā)出readyRead()信號,但是此時可能之前的receiveFile()還未執(zhí)行完畢,之前使用的Qt::AutoConnection,
- //結(jié)果傳輸大文件的時候就會出錯,原因就在于只要有數(shù)據(jù)到達(dá)的時候,就會連接信號,但是數(shù)據(jù)接收還沒處理完畢,而Qt::BlockingQueuedConnection會阻塞
- //此連接,直到receiveFile()處理完畢并返回后才發(fā)送信號。
- connect(tcpSocket, SIGNAL(readyRead()),
- this, SLOT(receiveFile()),Qt::BlockingQueuedConnection);
- exec();
- }
- void TcpThread::receiveFile()
- {
- //將tcpsocket封裝到QDataStream里,便于使用操作符>>
- QDataStream in(tcpSocket);
- if(bytesReceived < sizeof(qint32))
- {
- //先接收32bit的文件大小
- if(tcpSocket->bytesAvailable() >= sizeof(qint32))
- {
- 63: in.setByteOrder(QDataStream::LittleEndian); //必須的,因?yàn)榘l(fā)送端為LINUX系統(tǒng)
- in>>TotalBytes;
- TotalBytes += 4;
- qDebug()<<TotalBytes;
- bytesReceived += sizeof(qint32);
- fileName = tcpSocket->peerAddress();
- quint16 port = tcpSocket->peerPort();
- localFile = new QFile(fileName.toString()+(tr(".%1").arg(port))); //用戶端的IP地址作為保存文件名
- if (!localFile->open(QFile::WriteOnly ))
- {
- }
- }
- }
- //如果讀取的文件小于文件大小就繼續(xù)讀
- if (bytesReceived < TotalBytes){
- byteToRead = tcpSocket->bytesAvailable();
- bytesReceived += byteToRead;
- inBlock = tcpSocket->readAll();
- qDebug()<<"bytesReceived is:"<<bytesReceived;
- localFile->write(inBlock);
- inBlock.resize(0);
- }
- emit bytesArrived(bytesReceived,TotalBytes,socketDescriptor);
- if (bytesReceived == TotalBytes) {
- localFile->close();
- qDebug()<<bytesReceived;
- emit finished();
- QApplication::restoreOverrideCursor();
- }
- }
代碼中已經(jīng)有很詳細(xì)的注釋,需要再說明的一點(diǎn)就是在多線程的編寫中,信號/槽的連接方式一定要根據(jù)實(shí)際情況來進(jìn)行選擇!
“widget.h”
- #ifndef WIDGET_H
- #define WIDGET_H
- #include <QWidget>
- #include "tcpthread.h"
- #include "tcpserver.h"
- : class QDialogButtonBox;
- class QTcpSocket;
- namespace Ui {
- class Widget;
- }
- : class Widget : public QWidget
- : {
- Q_OBJECT
- public:
- explicit Widget(QWidget *parent = 0);
- ~Widget();
- private:
- Ui::Widget *ui;
- TcpServer tcpServer;
- private slots:
- void on_OkButton_clicked();
- void updateProgress(qint64,qint32,int);
- };
- #endif // WIDGET_H
簡單的widget類。
“widget.cpp”
- #include "widget.h"
- #include "ui_widget.h"
- #include <QtNetwork>
- #include <QtGui>
- Widget::Widget(QWidget *parent) :
- QWidget(parent),
- ui(new Ui::Widget)
- {
- ui->setupUi(this);
- ui->progressBar->setMaximum(2);
- ui->progressBar->setValue(0);
- }
- Widget::~Widget()
- {
- delete ui;
- }
- void Widget::on_OkButton_clicked()
- {
- ui->OkButton->setEnabled(false);
- QApplication::setOverrideCursor(Qt::WaitCursor);
- //bytesReceived = 0;
- while (!tcpServer.isListening() && !tcpServer.listen(QHostAddress::Any,12345))
- {
- QMessageBox::StandardButton ret = QMessageBox::critical(this,
- tr("回環(huán)"),
- tr("無法開始測試: %1.")
- .arg(tcpServer.errorString()),
- QMessageBox::Retry
- | QMessageBox::Cancel);
- if (ret == QMessageBox::Cancel)
- return;
- }
- ui->statuslabel->setText(tr("監(jiān)聽端口:%1").arg("12345"));
- connect(&tcpServer,SIGNAL(bytesArrived(qint64,qint32,int)),
- this,SLOT(updateProgress(qint64,qint32,int)));
- }
- void Widget::updateProgress(qint64 bytesReceived, qint32 TotalBytes, int socketDescriptor)
- {
- ui->progressBar->setMaximum(TotalBytes);
- ui->progressBar->setValue(bytesReceived);
- ui->statuslabel->setText(tr("已接收 %1MB")
- .arg(bytesReceived / (1024 * 1024)));
- ui->textBrowser->setText(tr("現(xiàn)在連接的socket描述符:%1").arg(socketDescriptor));
- }
完成服務(wù)器的監(jiān)聽,和進(jìn)度條的更新。
點(diǎn)擊開始后,處于監(jiān)聽狀態(tài)。
傳輸文件時:
小結(jié):關(guān)于詳解 QT 多線程 TCP 文件接收服務(wù)器實(shí)例的內(nèi)容介紹完了,關(guān)于QT 網(wǎng)絡(luò)的內(nèi)容,如果你有興趣的話,請參考編輯推薦。