赞
踩
客户端与服务器之间的数据传送在很多案例场景里都会有应用。这里Jungle用Qt来简单设计实现一个场景,即:
①两端:服务器QtServer和客户端QtClient
②功能:服务端连接客户端,两者能够互相发送消息,传送文件,并且显示文件传送进度。
环境:VS2008+Qt4.8.6+Qt设计师
客户端与服务器的基本概念不说了,关于TCP通信的三次握手等等,在经典教材谢希仁的《计算机网络》里都有详细介绍。这里说下两者是如何建立起通信连接的。
①IP地址:首先服务器和每一个客户端都有一个地址,即IP地址。(底层的MAC地址,不关心,因为TCP通信以及IP,是七层架构里面的网络层、传输层了,底层透明)。对于服务器来说,客户端的数量及地址是未知的,除非建立了连接。但是对于客户端来说,必须知道服务器的地址,因为两者之间的连接是由客户端主动发起的。
②端口号:软件层面的端口号,指的是“应用层的各种协议进程与运输实体进行层间交互的一种地址”。简而言之,每一个TCP连接都是一个进程,操作系统需要为每个进程分配一个协议端口(即每一个客户端与服务端的连接,不是两台主机的连接,而是两个端口的连接)。但一台主机通常会有很多服务,很多进程,单靠一个IP地址不能标识某个具体的进程或者连接。所以用端口号来标识访问的目标服务器以及服务器的目标服务类型。端口号也有分类,但这不是本文的重点,详见教材。
③TCP连接:总的来说,TCP的连接管理分为单个阶段:建立连接->数据传送->连接释放。在②里说到,每个TCP连接的是具体IP地址的主机的两个端口,即TCP连接的两个端点由IP地址和端口号组成,这即是套接字的概念:
套接字socket=IP:端口号
因此,我们要通过建立套接字来建立服务端与客户端的通信连接。
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连接;
②监听:监听①中指定的端口是否有连接的请求。
客户端:
服务端:
类设计如下:
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(); } }
类的设计:
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(); } }
这里发送了几条消息,并从客户端将《Windows网络编程技术.pdf》传到服务端,在服务端的ClientData文件夹里,该文件存在,证明程序可行!
程序包:https://download.csdn.net/download/sinat_21107433/10823712
Github更新到了vs2013+Qt5,源代码上传到Git了,地址:https://github.com/FengJungle/QtSocket.git
欢迎关注知乎专栏:Jungle是一个用Qt的工业Robot
欢迎关注Jungle的微信公众号:Jungle笔记
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。