引言
客户端与服务器之间的数据传送在很多案例场景里都会有应用。这里Jungle用Qt来简单设计实现一个场景,即:
①两端:服务器QtServer和客户端QtClient
②功能:服务端连接客户端,两者能够互相发送消息,传送文件,并且显示文件传送进度。
环境:VS2008 Qt4.8.6 Qt设计师
01
基本概念
客户端与服务器的基本概念不说了,关于TCP通信的三次握手等等,在经典教材谢希仁的《计算机网络》里都有详细介绍。这里说下两者是如何建立起通信连接的。
①IP地址:首先服务器和每一个客户端都有一个地址,即IP地址。对于服务器来说,客户端的数量及地址是未知的,除非建立了连接。但是对于客户端来说,必须知道服务器的地址,因为两者之间的连接是由客户端主动发起的。
②端口号:软件层面的端口号,指的是“应用层的各种协议进程与运输实体进行层间交互的一种地址”。简而言之,每一个TCP连接都是一个进程,操作系统需要为每个进程分配一个协议端口(即每一个客户端与服务端的连接,不是两台主机的连接,而是两个端口的连接)。但一台主机通常会有很多服务,很多进程,单靠一个IP地址不能标识某个具体的进程或者连接。所以用端口号来标识访问的目标服务器以及服务器的目标服务类型。端口号也有分类,但这不是本文的重点,详见教材。
③TCP连接:总的来说,TCP的连接管理分为单个阶段:建立连接->数据传送->连接释放。在②里说到,每个TCP连接的是具体IP地址的主机的两个端口,即TCP连接的两个端点由IP地址和端口号组成,这即是**套接字(socket)**的概念:
套接字socket=IP:端口号
因此,我们要通过建立套接字来建立服务端与客户端的通信连接。
02
Qt相关类
- 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连接;
- 监听:监听指定端口是否有连接的请求。
03
UI设计
客户端:
服务端:
04
客户端实现
类设计如下:
代码语言:javascript复制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;
};
类实现如下:
代码语言:javascript复制#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分别使用两个不同的端口。在服务端里也是这样,待会儿不再解释了。
客户端的剩余部分设计见下一篇文章。