Qt基础 | TCP通信 | TCP服务端与客户端程序设计 | QTcpServer | QTcpSocket

文章目录

  • [一、TCP 通信](#一、TCP 通信)
    • [1.TCP 通信概述](#1.TCP 通信概述)
    • [2.TCP 服务器端程序设计](#2.TCP 服务器端程序设计)
      • [2.1 主窗口定义与构造函数](#2.1 主窗口定义与构造函数)
      • [2.2 网络监听与 socket 连接的建立](#2.2 网络监听与 socket 连接的建立)
      • [2.3 与 TCP 客户端进行数据通信](#2.3 与 TCP 客户端进行数据通信)
    • [3.TCP 客户端程序设计](#3.TCP 客户端程序设计)
      • [3.1 主窗口定义与构造函数](#3.1 主窗口定义与构造函数)
      • [3.2 与服务器端建立 socket 连接](#3.2 与服务器端建立 socket 连接)
      • [3.3 与 TCPServer 的数据收发](#3.3 与 TCPServer 的数据收发)
    • 4.小结

Qt 网络模块:


一、TCP 通信

1.TCP 通信概述

   TCP 是一种被大多数 Internet 网络协议(如 HTTP 和 FTP) 用于数据传输的低级网络协议,它是可靠的、面向流 、面向连接的传输协议,特别适合用于连续数据传输。

  TCP 通信必须先建立 TCP 连接,通信端分为客户端和服务器端。Qt 提供 QTcpSocket 类和 QTcpServer 类用于建立 TCP 通信应用程序。服务器端程序必须使用 QTcpServer 用于端口监听,建立服务器;QTcpSocket 用于建立连接后使用套接字(Socket)进行通信。

  QTcpServer 是从 QObject 继承的类,它主要用于服务器端建立网络监听,创建网络 Socket 连接。

  服务器端程序首先需要用 QTcpServer::listen() 开始服务器端监听,可以指定监听的 IP 地址和端口,一般一个服务程序只监听某个端口的网络连接。当有新的客户端接入时,QTcpServer内部的 incomingConnection() 函数会创建一个与客户端连接的 QTcpSocket 对象,并添加到内部可用新连接列表,然后发射 newConnection() 信号。在 该信号的槽函数中,可以用 nextPendingConnection() 函数接受客户端的连接,然后使用 QTcpSocket 与客户端通信。在客户端与服务器建立 TCP 连接后,具体的数据通信是通过 QTcpSocket 完成的。

QTcpServer 类的主要接口函数

  QTcpSocket 类是从 QIODevice 间接继承的类,所以具有流数据读写的功能。一个 QTcpSocket 实例既可以接收数据也可以发送数据,且接收与发射是异步工作的,有各自的缓冲区。QTcpSocket 类除了构造函数和析构函数,其他函数都是从 QAbstractSocket 继承或重定义的。

  TCP 客户端使用 QTcpSocket 与 TCP 服务器建立连接并通信。客户端的 QTcpSocket 实例首先通过 connectToHost() 尝试连接到服务器,需要指定服务器的 IP 地址和端口。connectToHost() 以异步方式连接服务器,即以非阻塞程序运行,connectToHost() 成功连接到服务器发射 connected() 信号。如果需要使用阻塞方式连接服务器,则使用 waitForConnected() 函数阻塞程序运行,直到建立 socket 连接。与服务器端建立 socket 连接后,就可以向缓冲区写数据或从接收缓冲区读取数据,实现数据的通信。当缓冲区有新数据进入时 会发射 readyRead() 信号,一般在此信号的槽函数里读取缓冲区数据。

QAbstractSocket 用于 TCP 通信的主要接口函数

2.TCP 服务器端程序设计

TCP 服务器端程序应具有以下功能

  • 根据指定 IP 地址和端口打开网络监听,有客户端连接时创建 socket 连接
  • 采用基于行的数据通信协议,可以接收客户端发来的消息,也可以向客户端发送消息
  • 在状态栏显示服务器监听状态和 socket 的状态

2.1 主窗口定义与构造函数

   TCPServer 是一个基于 QMainWindow 的应用程序,界面是由 UI 设计器设计,设计如下:

  主窗口类 MainWindow 定义如下:

mainwindow.h

cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

#include    <QTcpServer>
#include    <QLabel>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

private:
    QLabel  *LabListen;//状态栏标签
    QLabel  *LabSocketState;//状态栏标签
    QTcpServer *tcpServer; //TCP服务器
    QTcpSocket *tcpSocket;//TCP通讯的Socket

    QString getLocalIP();//获取本机IP地址

protected:
    void    closeEvent(QCloseEvent *event);

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
//自定义槽函数
    void    onNewConnection();//QTcpServer的newConnection()信号

    void    onSocketStateChange(QAbstractSocket::SocketState socketState);
    void    onClientConnected(); //Client Socket connected
    void    onClientDisconnected();//Client Socket disconnected
    void    onSocketReadyRead();//读取socket传入的数据
//UI生成的
    void on_actStart_triggered();
    void on_actStop_triggered();
    void on_actClear_triggered();
    void on_btnSend_clicked();
    void on_actHostInfo_triggered();

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

私有变量:

  • tcpServer:用于建立 TCP 服务器
  • tcpSocket:用于与客户端进行 socket 连接和通信。
  • LabListen:用于显示服务器监听状态
  • LabSocketState:用于显示 socket 的状态

槽函数:

  • onNewConnection()函数:

    当有新客户端接入时,会发射 newConnection()信号,连接到该槽函数,在该槽函数中可以调用 nextPendingConnection() 函数获取与接入连接进行通信的 socket,然后将 socket 的几个信号与相应的槽函数连接起来。

  • onClientConnected()函数:

    当 tcpSocket 与服务器连接建立时,发射 connected() 信号,连接到该槽函数。

  • onClientDisconnected()函数:

    当 tcpSocket 与服务器连接断开时,发射 disconnected() 信号,连接到该槽函数。

  • onSocketStateChange()函数:

    当 tcpSocket 状态发生变化时,发射 stateChanged() 信号,连接到该槽函数。

  • onSocketReadyRead()函数:

    当 tcpSocket 发射 readyRead() 信号时,连接到该槽函数。

构造函数

  构造函数创建状态栏上的标签用于信息显示,调用自定义函数 getLocalIP()获取本机 IP 地址,并显示到标题栏上。创建 QTcpServer 实例 tcpServer,并将其 newConnection() 信号与 onNewConnection() 槽函数关联。

cpp 复制代码
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    LabListen=new QLabel("监听状态:");
    LabListen->setMinimumWidth(150);
    ui->statusBar->addWidget(LabListen);

    LabSocketState=new QLabel("Socket状态:");//
    LabSocketState->setMinimumWidth(200);
    ui->statusBar->addWidget(LabSocketState);

    QString localIP=getLocalIP();//本机IP
    this->setWindowTitle(this->windowTitle()+"----本机IP:"+localIP);
    ui->comboIP->addItem(localIP);

    tcpServer=new QTcpServer(this);
    connect(tcpServer,SIGNAL(newConnection()),this,SLOT(onNewConnection()));
}

QString MainWindow::getLocalIP()
{//获取本机IPv4地址
    QString hostName=QHostInfo::localHostName();//本地主机名
    QHostInfo   hostInfo=QHostInfo::fromName(hostName);
    QString   localIP="";

    QList<QHostAddress> addList=hostInfo.addresses();//

    if (!addList.isEmpty())
    for (int i=0;i<addList.count();i++)
    {
        QHostAddress aHost=addList.at(i);
        if (QAbstractSocket::IPv4Protocol==aHost.protocol())
        {
            localIP=aHost.toString();
            break;
        }
    }
    return localIP;
}

2.2 网络监听与 socket 连接的建立

网络监听

  作为 TCP 服务器,QTcpServer 类需要调用 listen() 函数在本机某个 IP 地址和端口上开始 TCP 监听,以等待 TCP 客户端的接入。单击 "开始监听" 按钮,会触发槽函数,代码如下:

cpp 复制代码
void MainWindow::on_actStart_triggered()
{//开始监听
    QString     IP=ui->comboIP->currentText();//IP地址
    quint16     port=ui->spinPort->value();//端口
    QHostAddress    addr(IP);
    tcpServer->listen(addr,port);//
//    tcpServer->listen(QHostAddress::LocalHost,port);// Equivalent to QHostAddress("127.0.0.1").
    ui->plainTextEdit->appendPlainText("**开始监听...");
    ui->plainTextEdit->appendPlainText("**服务器地址:"
                       +tcpServer->serverAddress().toString());
    ui->plainTextEdit->appendPlainText("**服务器端口:"
                       +QString::number(tcpServer->serverPort()));

    ui->actStart->setEnabled(false);
    ui->actStop->setEnabled(true);

    LabListen->setText("监听状态:正在监听");
}
  • IP 地址可以是表示本机的 "127.0.0.1",或是本机的实际 IP 地址。

socket 连接的建立

  tcpServer 开始监听后,TCPClient 就可以通过 IP 地址和端口连接到此服务器。当有客户端接入时, tcpServer 会发射 newConnection() 信号,此信号关联到 onNewConnection()槽函数。在该槽函数中可以调用 nextPendingConnection() 函数获取与接入连接进行通信的 socket,然后将 socket 的几个信号与相应的槽函数连接起来。代码如下:

cpp 复制代码
void MainWindow::onNewConnection()
{
//    ui->plainTextEdit->appendPlainText("有新连接");
    tcpSocket = tcpServer->nextPendingConnection(); //创建socket

    connect(tcpSocket, SIGNAL(connected()),
            this, SLOT(onClientConnected()));
    onClientConnected();//

    connect(tcpSocket, SIGNAL(disconnected()),
            this, SLOT(onClientDisconnected()));

    connect(tcpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
            this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
    onSocketStateChange(tcpSocket->state());

    connect(tcpSocket,SIGNAL(readyRead()),
            this,SLOT(onSocketReadyRead()));
}

  QTcpSocket 的这几个信号的作用是:

  • connected()信号,客户端 socket 连接建立时发射此信号
  • disconnected() 信号,客户端 socket 连接断开时发射此信号
  • stateChanged() 信号,socket 状态变化时发射此信号
  • readyRead() 信号,socket 的读取缓冲区有新数据时发射此信号

这几个信号对应的槽函数如下:

cpp 复制代码
void MainWindow::onClientConnected()
{//客户端接入时
    ui->plainTextEdit->appendPlainText("**client socket connected");
    ui->plainTextEdit->appendPlainText("**peer address:"+
                                   tcpSocket->peerAddress().toString());
    ui->plainTextEdit->appendPlainText("**peer port:"+
                                   QString::number(tcpSocket->peerPort()));
}

void MainWindow::onClientDisconnected()
{//客户端断开连接时
    ui->plainTextEdit->appendPlainText("**client socket disconnected");
    tcpSocket->deleteLater();
    //    deleteLater();//QObject::deleteLater();
}

void MainWindow::onSocketStateChange(QAbstractSocket::SocketState socketState)
{//socket状态变化时
    switch(socketState)
    {
    case QAbstractSocket::UnconnectedState:
        LabSocketState->setText("scoket状态:UnconnectedState");
        break;
    case QAbstractSocket::HostLookupState:
        LabSocketState->setText("scoket状态:HostLookupState");
        break;
    case QAbstractSocket::ConnectingState:
        LabSocketState->setText("scoket状态:ConnectingState");
        break;

    case QAbstractSocket::ConnectedState:
        LabSocketState->setText("scoket状态:ConnectedState");
        break;

    case QAbstractSocket::BoundState:
        LabSocketState->setText("scoket状态:BoundState");
        break;

    case QAbstractSocket::ClosingState:
        LabSocketState->setText("scoket状态:ClosingState");
        break;

    case QAbstractSocket::ListeningState:
        LabSocketState->setText("scoket状态:ListeningState");
    }
}

void MainWindow::onSocketReadyRead()
{//读取缓冲区行文本
    while(tcpSocket->canReadLine())
        ui->plainTextEdit->appendPlainText("[in] "+tcpSocket->readLine());
}

停止监听

  TCP 服务器停止监听,只需调用 QTcpServer 的 close () 函数即可。单击 "停止监听" 按钮,其响应槽函数代码如下:

cpp 复制代码
void MainWindow::on_actStop_triggered()
{//停止监听
    if (tcpServer->isListening()) //tcpServer正在监听
    {
        tcpServer->close();//停止监听
        ui->actStart->setEnabled(true);
        ui->actStop->setEnabled(false);
        LabListen->setText("监听状态:已停止监听");
    }
}

2.3 与 TCP 客户端进行数据通信

  TCP 服务器端和客户端之间通过 QTcpSocket 通信时,需要规定两者之间的通信协议,即传输的数据内容如何解析。 QTcpSocket 间接继承于 QIODevice,所以支待流读写功能。

  Socket 之间的数据通信协议一般有两种方式:基于行的或基于数据块的

  • 基于行的数据通信协议一般用于纯文本数据的通信,每一行数据以一个换行符结束。一般是通过调用 canReadLine() 函数判断是否有新的一行数据需要读取,再调用 readLine() 函数读取一行数据,例如:

    cpp 复制代码
    //读取缓冲区行文本
    while(tcpSocket->canReadLine())
         ui->plainTextEdit->appendPlainText("[in] "+tcpSocket->readLine());
  • 基于块的数据通信协议用于一般的二进制数据的传输,需要自定义具体的格式。

发送数据

  将输入文本框中的字符串转换为 QByteArray 类型的字节数组,然后调用 QIODevice 的 write() 函数写入缓冲区,这样就完成向客户端发送一行文字。单击 "发送信息" 按钮,会触发上述内容,代码如下:

cpp 复制代码
void MainWindow::on_btnSend_clicked()
{//发送一行字符串,以换行符结束
    QString  msg=ui->editMsg->text();
    ui->plainTextEdit->appendPlainText("[out] "+msg);
    ui->editMsg->clear();
    ui->editMsg->setFocus();

    QByteArray  str=msg.toUtf8();
    str.append('\n');//添加一个换行符
    tcpSocket->write(str);
}

读取数据

  QTcpSocket 接收到来自客户端的数据后,会发射 readyRead() 信号,在其槽函数中需要读取行缓冲区的数据。代码如下:

cpp 复制代码
void MainWindow::onSocketReadyRead()
{//读取缓冲区行文本
    while(tcpSocket->canReadLine())
        ui->plainTextEdit->appendPlainText("[in] "+tcpSocket->readLine());
}

这样,TCPServer 就可以与 TCPClient 之间进行双向通信了,且这个连接将一直存在,直到某一方的 QTcpSocket 对象调用 disconnectFromHost() 函数断开 socket 连接。

运行效果图:

3.TCP 客户端程序设计

  客户端程序 TCPClient 只需要使用一个 QTcpSocket 对象,就可以和服务器端程序 TCPServer 进行通信。

TCP 客户端程序应具有以下功能

  • 通过 IP 地址和端口号连接到服务器
  • 采用基于行的数据通信协议,与服务器端收发消息
  • 处理 QTcpSocket 的 StateChange() 信号,在状态栏显示 socket 的状态

3.1 主窗口定义与构造函数

   TCPClient 是一个基于 QMainWindow 的应用程序,界面是由 UI 设计器设计,设计如下:

  主窗口定义如下:

mainwindow.h

cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include    <QTcpSocket>
#include    <QLabel>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT
private:
    QTcpSocket  *tcpClient;  //socket
    QLabel  *LabSocketState;  //状态栏显示标签

    QString getLocalIP();//获取本机IP地址
protected:
    void    closeEvent(QCloseEvent *event);
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
//自定义槽函数
    void    onConnected();
    void    onDisconnected();
    void    onSocketStateChange(QAbstractSocket::SocketState socketState);
    void    onSocketReadyRead();//读取socket传入的数据
//
    void on_actConnect_triggered();
    void on_actDisconnect_triggered();
    void on_actClear_triggered();
    void on_btnSend_clicked();

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

私有变量:

  • tcpClient:用于socket 连接与通信
  • LabSocketState:用于显示 socket 状态

槽函数:自定义了几个槽函数,用于与 tcpClient 的相关信号关联。

构造函数

  构造函数主要功能是创建 tcpClient,设置状态栏标签,往下拉列表组件中添加本机 IP,最后建立信号与槽函数的关联。

cpp 复制代码
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    tcpClient=new QTcpSocket(this); //创建socket变量

    LabSocketState=new QLabel("Socket状态:");//状态栏标签
    LabSocketState->setMinimumWidth(250);
    ui->statusBar->addWidget(LabSocketState);

    QString localIP=getLocalIP();//本机IP
    this->setWindowTitle(this->windowTitle()+"----本机IP:"+localIP);
    ui->comboServer->addItem(localIP);


    connect(tcpClient,SIGNAL(connected()),this,SLOT(onConnected()));
    connect(tcpClient,SIGNAL(disconnected()),this,SLOT(onDisconnected()));

    connect(tcpClient,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
            this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
    connect(tcpClient,SIGNAL(readyRead()),
            this,SLOT(onSocketReadyRead()));
}

3.2 与服务器端建立 socket 连接

  通过调用 QTcpSocket 的函数 connectToHost() 连接到服务器,也可以使用 disconnectFromHost() 函数断开与服务器的连接。其代码如下:

cpp 复制代码
void MainWindow::on_actConnect_triggered()
{//连接到服务器
    QString     addr=ui->comboServer->currentText();
    quint16     port=ui->spinPort->value();
    tcpClient->connectToHost(addr,port);
//    tcpClient->connectToHost(QHostAddress::LocalHost,port);
}

void MainWindow::on_actDisconnect_triggered()
{//断开与服务器的连接
    if (tcpClient->state()==QAbstractSocket::ConnectedState)
        tcpClient->disconnectFromHost();
}

void MainWindow::onConnected()
{ //connected()信号槽函数
    ui->plainTextEdit->appendPlainText("**已连接到服务器");
    ui->plainTextEdit->appendPlainText("**peer address:"+
                                   tcpClient->peerAddress().toString());
    ui->plainTextEdit->appendPlainText("**peer port:"+
                                   QString::number(tcpClient->peerPort()));
    ui->actConnect->setEnabled(false);
    ui->actDisconnect->setEnabled(true);
}

void MainWindow::onDisconnected()
{//disConnected()信号槽函数
    ui->plainTextEdit->appendPlainText("**已断开与服务器的连接");
    ui->actConnect->setEnabled(true);
    ui->actDisconnect->setEnabled(false);
}

3.3 与 TCPServer 的数据收发

  TCPClient 与 TCPServer 之间采用基于行的数据通信协议。

发送数据

  将输入文本框中的字符串转换为 QByteArray 类型的字节数组,然后调用 QIODevice 的 write() 函数写入缓冲区,这样就完成向服务器端发送一行文字。单击 "发送信息" 按钮,会触发上述内容,代码如下:

cpp 复制代码
void MainWindow::on_btnSend_clicked()
{//发送数据
    QString  msg=ui->editMsg->text();
    ui->plainTextEdit->appendPlainText("[out] "+msg);
    ui->editMsg->clear();
    ui->editMsg->setFocus();

    QByteArray  str=msg.toUtf8();
    str.append('\n');
    tcpClient->write(str);
}

接收数据

  QTcpSocket 接收到来自服务器端的数据后,会发射 readyRead() 信号,在其槽函数中需要读取行缓冲区的数据。代码如下:

cpp 复制代码
void MainWindow::onSocketReadyRead()
{//readyRead()信号槽函数
    while(tcpClient->canReadLine())
        ui->plainTextEdit->appendPlainText("[in] "+tcpClient->readLine());
}

运行结果:

4.小结

  上述客户端与服务端程序只是为了演示 TCP 通信的基本原理,TCPServer 只允许一个TCPClient 客户端接入。而一般的 TCP 服务器程序允许多个客户端接入,为了使每个 socket 连接独立通信互不影响,一般采用多线程 ,即为一个 socket 连接创建一个线程。

  上述的数据通信采用基于行的通信协议,这种 socket 通信方式只能传输字符串数据。QTcpSocket 间接继承于 QIODevice,可以使用数据流的方式传输二进制数据流,例如传输图片、任意格式文件等,但是这涉及到服务器端和客户端之间通信协议的定义。

相关推荐
初阳78510 小时前
【Qt】系统相关(2)——文件
开发语言·qt
尘中远1 天前
【Qwt 7.0 系列】坐标轴与刻度系统 —— 刻度引擎、网格、图例与刻度朝内
qt·数据可视化·qcustomplot·qwt·工业软件·科学绘图
sycmancia1 天前
Qt——多线程间的互斥
开发语言·qt
尘中远1 天前
【Qwt 7.0 系列】常用图表类型实战 —— 柱状图、散点图、箱线图与直方图
qt·qwt·工业软件·科学绘图
尘中远1 天前
【Qwt 7.0 系列】交互功能详解 —— 平移、缩放、坐标轴交互与数据拾取
qt·数据可视化·绘图·qcustomplot·qwt·科学绘图
sycmancia1 天前
Qt——进程与线程的概念
qt
郝学胜-神的一滴1 天前
Qt 高级编程 034:深耕QWidget底层内核—彻底吃透无边框窗口设计核心原理
开发语言·c++·qt·程序人生·软件开发·用户界面
尘中远1 天前
【Qwt 7.0 系列】3D 数据可视化 —— OpenGL 高性能三维绘图
qt·3d·qcustomplot·qwt·科学绘图·高性能绘图
满天星83035771 天前
【Qt】控件(二) (geometry及与frameGeometry的区别)
开发语言·qt
大气的小蜜蜂1 天前
基于Python+PyQt5+SQLite的药房管理系统实现:事务一致性与界面解耦全流程解析
python·qt·sqlite