简易版QQ?Qt也可以实现!(一)

2022-07-24 15:22:51 浏览数 (1)

引言

客户端与服务器之间的数据传送在很多案例场景里都会有应用。这里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之间的都行。
代码语言:javascript复制
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():同上面的比喻,这个方法的作用是向管道里塞数据
代码语言:javascript复制
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分别使用两个不同的端口。在服务端里也是这样,待会儿不再解释了。

客户端的剩余部分设计见下一篇文章

0 人点赞