客户端与服务器之间的数据传送在很多案例场景里都会有应用。这里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()<seek(0);
sendout<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
#include
#include
#include
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()<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<seek(0);
sendout<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