本文介绍的是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表示网络连接出了问题,停止读取。
- 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; //主机地址与名称信息结构
- 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();
- //获取打开的文件的大小,并将文件整个读入内存中
- 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';
- }
- //结束通讯,关闭套接字,关闭连接
- 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)的定义。
#p#
“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的建立,和文件的接收。
“tcpthread.cpp”
- #include "tcpthread.h"
- #include <QtGui>
- #include <QtNetwork>
- //构造函数完成简单的赋值/
- TcpThread::TcpThread(int socketDescriptor, QObject *parent):
- QThread(parent),socketDescriptor(socketDescriptor)
- {
- bytesReceived = 0;
- }
- //因为QT的线程的执行都是从run()开始,
- //所以在此函数里完成tcpsocket的创建,相关信号的绑定
- 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表示只有它不是一个重复连接,连接才会成功。如果之前已经有了一个链接(相同的信号连接到同一对象的同一个槽上),那么连接将会失败并将返回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));
- }
完成服务器的监听,和进度条的更新。
点击开始后,处于监听状态。
传输文件时:
小结:关于详解 QT 多线程 TCP 文件接收服务器实例的内容介绍完了,关于QT 网络的内容,如果你有兴趣的话,请参考编辑推荐。