本文介紹的是QT 多線程 TCP 文件接收服務器實例,如果你想深入瞭解這方面的資料的話,請關注本文末尾,不多說,我們先來看內容。
因爲項目需要,需要跨平臺編寫網絡傳輸程序。
目標:
用戶端:Linux(arm平臺),完成文件的傳輸
服務器:Windows ,使用多線程的文件的接收
實現無線的文件傳輸功能
用戶端程序,用標準的socket完成文件傳輸的功能,代碼如下:
- // Linux下網絡編程,客戶端程序代碼
- //程序運行參數:
- // ./client IPADDRESS PORTNUMBER
- // (其中IPADDRESS是服務端IP地址,PORTNUMBER是服務端用於監聽的端口)
- //
- #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()函數代替本來的read()函數原因有以下幾點:
- //
- //ssize_t read(int fd,void *buf,size_t nbyte)
- //read函數是負責從fd中讀取內容。當讀成功時,read返回實際所讀的字節數;如果
- //返回的值是0,表示已經讀到文件的結束了;小於0表示出現了錯誤。
- //
- // 1)如果錯誤爲EINTR說明read出錯是由中斷引起的,繼續讀。
- // 2)如果是ECONNREST表示網絡連接出了問題,www.linuxidc.com停止讀取。
- 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; //已經寫的文件大小
- char *ptr;
- ptr=buffer;
- bytes_left=length;
- while(bytes_left>0)
- {
- //開始寫
- written_bytes=write(fd,ptr,min(bytes_left,writesize));
- //出現了寫錯誤
- if(written_bytes<=0)
- {
- //中斷錯誤,置零重新寫
- if(errno==EINTR)
- written_bytes=0;
- //其他錯誤,退出不寫了
- else
- return(-1);
- }
- //從剩下的地方繼續寫
- bytes_left-=written_bytes;
- ptr+=written_bytes;
- }
- return(0);
- }
- : int main(int argc, char *argv[])
- {
- int sockfd; //通信套接字描述符
- char *buffer; //緩衝區
- struct sockaddr_in server_addr; //服務器地址結構
- struct hostent *host; //主機地址與名稱信息結構www.linuxidc.com
- int nbytes; //端口號、字節數
- FILE *fp; //文件指針
- int nfilesize; //文件大小
- char str[128]; //文件名
- char yes='Y'; //流程控制
- struct timeval tpstart,tpend; //用於記錄文件傳輸時間
- float timeuse; //文件傳輸所用時間
- char *hostname="127.0.0.1";//主機名/ip地址
- int portnumber=4321;//端口號
- //提示用戶輸入完整的命令行參數
- if(argc!=3)
- {
- fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]);
- printf("using defaults:\nhostname: %s\nportnumber: %d\n",hostname,portnumber);
- }
- //如果利用用戶輸入的域名無法獲得正確的主機地址信息,則退出
- 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描述符,創建通信套接字
- if((sockfd=socket(AF_INET,SOCK_STREAM,6))==-1)
- {
- fprintf(stderr,"Socket Error:%s\a\n",strerror(errno));
- exit(1);
- }
- //客戶程序填充服務端的地址信息
- 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);
- //客戶程序發起連接請求
- 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();
- //獲取打開的文件的大小,www.linuxidc.com並將文件整個讀入內存中
- 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");
- }
- //將要傳輸的文件的大小信息發送給客戶端
- 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);
- }
- //獲取傳輸結束時間
- gettimeofday(&tpend,NULL);
- //計算整個傳輸用時
- 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); //釋放文件內存
- fclose(fp); //關閉文件
- // printf("\nTransfer another file?(Y/N): ");
- //scanf("%c",&yes);
- // getchar();
- yes='n';
- }
- //結束通訊,關閉套接字,www.linuxidc.com關閉連接
- close(sockfd);
- printf("\nClient Exit!~~\n");
- exit(0);
- }
服務器端代碼列表:
具體代碼如下:
- “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類自帶的函數,詳情參考Class Reference
- protected:
- void incomingConnection(int socketDescriptor);
- };
- #endif // TCPSERVER_H
TCPSERVER繼承QTcpServer,主要完成TCP服務器的建立,類中最主要的成員函數爲虛函數incomingConnection(int socketDescriptor)的定義。
“tcpserver.cpp”
- #include "tcpserver.h"
- #include "tcpthread.h"
- //構造函數
- TcpServer::TcpServer(QObject *parent) :
- QTcpServer(parent)
- {
- }
- //重新定義了incomingConnection這個虛函數,
- //開闢一個新的tcpsocket線程,從TcpServer獲得socketDescriptor,
- //並完成相應的信號連接。
- void TcpServer::incomingConnection(int socketDescriptor)
- {
- TcpThread *thread = new TcpThread(socketDescriptor, this);
- //將線程結束信號與線程的deleteLater槽關聯
- connect(thread, SIGNAL(finished()),
- thread, SLOT(deleteLater()));
- //關聯相應的UI更新槽
- connect(thread,SIGNAL(bytesArrived(qint64,qint32,int)),
- this,SIGNAL(bytesArrived(qint64,qint32,int)));
- //QT的線程都是從start開始,調用run()函數
- thread->start();
- }
極其簡單的構造函數,在incomingConnection()中,定義一個線程TcpThread,並將socketDescriptor傳遞給其構造函數,完成線程的創建,並且調用QThread的start函數,開始執行線程的虛函數run()。
“tcpthread.h”
- #ifndef TCPTHREAD_H
- #define TCPTHREAD_H
- #include <QThread>
- #include <QTcpSocket>
- #include <QtNetwork>
- //繼承QThread的TCP傳輸線程
- //主要是完成run()虛函數的定義
- //還有一些輔助變量的聲明
- 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; //收到的總字節
- qint64 byteToRead; //準備讀取的字節
- qint32 TotalBytes; //總共傳輸的字節
- QTcpSocket *tcpSocket;
- QHostAddress fileName; //文件名
- QFile *localFile;
- QByteArray inBlock; //讀取緩存
- };
- #endif // TCPTHREAD_H
繼承自QThread類,在此線程中完成TCPSOCKET的建立,www.linuxidc.com和文件的接收。
“tcpthread.cpp”
- #include "tcpthread.h"
- #include <QtGui>
- #include <QtNetwork>
- //構造函數完成簡單的賦值/
- TcpThread::TcpThread(int socketDescriptor, QObject *parent):
- QThread(parent),socketDescriptor(socketDescriptor)
- {
- bytesReceived = 0;
- }
- //因爲QT的線程的執行都是從run()開始,
- //所以在此函數裏完成tcpsocket的創建,相關信號的綁定www.linuxidc.com
- void TcpThread::run()
- {
- tcpSocket = new QTcpSocket;
- //將Server傳來的socketDescriptor與剛創建的tcpSocket關聯
- if (!tcpSocket->setSocketDescriptor(socketDescriptor)) {
- emit error(tcpSocket->error());
- return;
- }
- qDebug()<<socketDescriptor;
- : //這是重中之重,必須加Qt::BlockingQueuedConnection!
- //這裏困擾了我好幾天,原因就在與開始沒加,默認用的Qt::AutoConnection。
- //簡單介紹一下QT信號與槽的連接方式:
- //Qt::AutoConnection表示系統自動選擇相應的連接方式,如果信號與槽在同一線程,就採用Qt::DirectConnection,
- 如果信號與槽不在同一線程,將採用Qt::QueuedConnection的連接方式。
- //Qt::DirectConnection表示一旦信號產生,立即執行槽函數。
- //Qt::QueuedConnection表示信號產生後,將發送Event給你的receiver所在的線程,postEvent(QEvent::MetaCall,...),
- slot函數會在receiver所在的線程的event loop中進行處理。
- //Qt::BlockingQueuedConnection表示信號產生後調用sendEvent(QEvent::MetaCall,...),
- 在receiver所在的線程處理完成後纔會返回;只能當sender,receiver不在同一線程時纔可以。
- //Qt::UniqueConnection表示只有它不是一個重複連接,連接纔會成功。www.linuxidc.com如果之前已經有了一個鏈接(相同的信號連接到同一對象的同一個槽上),那麼連接將會失敗並將返回false。
- //Qt::AutoCompatConnection與QT3保持兼容性
- //說明一下,對於任何的QThread來說,其線程只存在於run()函數內,其它的函數都不在線程內,所以此處要採用Qt::BlockingQueuedConnection,
- //因爲當SOCKET有數據到達時就會發出readyRead()信號,但是此時可能之前的receiveFile()還未執行完畢,之前使用的Qt::AutoConnection,
- //結果傳輸大文件的時候就會出錯,原因就在於只要有數據到達的時候,就會連接信號,但是數據接收還沒處理完畢,而Qt::BlockingQueuedConnection會阻塞
- //此連接,直到receiveFile()處理完畢並返回後才發送信號。
- 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); //必須的,因爲發送端爲LINUX系統
- 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 ))
- {
- }
- }
- }
- //如果讀取的文件小於文件大小就繼續讀
- 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();
- }
- }
代碼中已經有很詳細的註釋,需要再說明的一點就是在多線程的編寫中,信號/槽的連接方式一定要根據實際情況來進行選擇!
“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("迴環"),
- tr("無法開始測試: %1.")
- .arg(tcpServer.errorString()),
- QMessageBox::Retry
- | QMessageBox::Cancel);
- if (ret == QMessageBox::Cancel)
- return;
- }
- ui->statuslabel->setText(tr("監聽端口:%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("現在連接的socket描述符:%1").arg(socketDescriptor));
- }
完成服務器的監聽,和進度條的更新。
點擊開始後,處於監聽狀態。
傳輸文件時: