QT -- 网络编程

目录

Qt 网络

Qt 为了支持跨平台, 对网络编程的 API 也进行了重新封装.Qt Network 是一个用于C++应用程序网络编程的模块,它提供了一套从底层套接字操 作到高层网络协议(如HTTP)的完整API。其核心特点是将复杂的网络通信功能封装为易于使用的类,并深度融入Qt的事件驱动信号槽机制,让开发者能高效地开发客户端和服务器程序。

注意:实际 Qt 开发中进行网络编程, 也不⼀定使用 Qt 封装的网络 API, 也有⼀定可能使用的是系统原生 API 或者其他第三方框架的 API。
在进行网络编程之前, 需要在项目中的 .pro 文件中添加 network 模块,QT中有很多模块,需要使用哪些模块,需要在.pro 文件中添加,这样在编译的时候才能把相关模块加载进来。

UDP Socket

核心 API 概览

主要的类有两个QUdpSocketQNetworkDatagram

QUdpSocket 表示⼀个 UDP 的 socket 文件.

名称 类型 说明 对标原生 API
bind(const QHostAddress&, quint16) 方法 绑定指定的端口号。 bind
receiveDatagram() 方法 返回 QNetworkDatagram,读取一个 UDP 数据报。 recvfrom
writeDatagram(const QNetworkDatagram&) 方法 发送一个 UDP 数据报。 sendto
readyRead 信号 在收到数据并准备就绪后触发。 无(类似于 IO 多路复用的通知机制)

QNetworkDatagram 表示⼀个 UDP 数据报

名称 类型 说明 对标原生 API
QNetworkDatagram(const QByteArray&, const QHostAddress&, quint16) 构造函数 通过 QByteArray、目标 IP 地址、目标端口号构造一个 UDP 数据报。通常用于发送数据时。
data() 方法 获取数据报内部持有的数据。返回 QByteArray
senderAddress() 方法 获取数据报中包含的对端的 IP 地址。 无,recvfrom 包含了该功能。
senderPort() 方法 获取数据报中包含的对端的端口号。 无,recvfrom 包含了该功能。

回显服务器

1.创建一个QListWidget页面来显示信息

2.在Widget.h中添加QUdpSocket成员

c 复制代码
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include<QUdpSocket>  //添加头文件
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private:
    Ui::Widget *ui;
    QUdpSocket* socket; //添加QUdpSocket成员
};
#endif // WIDGET_H

3.修改 widget.cpp, 完成 socket 后续的初始化

注意:⼀般来说, 要先连接信号槽, 再绑定端口.如果顺序反过来, 可能会出现端⼝绑定好了之后, 请求就过来了. 此时还没来得及连接信号槽. 那么这个请求就有可能错过了.

c 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    //创建QUdpSocket对象
    socket = new QUdpSocket(this);
    //连接信号槽
    connect(socket,&QUdpSocket::readyRead,this,&Widget::processRequest);
    //绑定IP和端口号
    bool ret = socket->bind(QHostAddress::Any,8080);
    if(!ret)
    {
        QMessageBox::critical(this,"服务器启动出错",socket->errorString()); //errorString就相当于Linux中的perror,本质上也是对error的封装。
        return;
    }
    
}
//处理客户端请求
void Widget::processRequest()
{
    //1.读取请求并解析
    QNetworkDatagram requestDatagram = socket->receiveDatagram();
    QString request = requestDatagram.data();//返回值是QByteArray类型,可以直接赋值给QString
    //2.根据请求计算响应
    QString response = process(request);
    //3.把响应写回到客户端
    QNetworkDatagram responseDatagram(response.toUtf8(),requestDatagram.senderAddress(),
                          requestDatagram.senderPort());//toUtf8返回QString中的字节数组QByteArray,senderAddress和senderPort指明发送给谁
    socket->writeDatagram(responseDatagram);
    //4.打印日志
    QString log = "["+requestDatagram.senderAddress().toString()+":"+
            QString::number(requestDatagram.senderPort())+"],request:"+request
            +" response:"+response;
    ui->listWidget->addItem(log);
}

4.实现 process 函数

由于我们此处是实现回显服务器. 所以 process 方法中并没有包含实质性的内容.

c 复制代码
const QString& Widget::process(const QString &text)
{
    return text;
}

注意: "根据请求处理响应" 是服务器开发中的最核心的步骤.⼀个商业服务器程序, 这里的逻辑可能是几万行几十万行代码量级的。

此时, 服务器程序编写完毕.

但是直接运行还看不出效果. 还需要搭配客户端来使用。


回显客户端

  1. 创建界⾯. 包含⼀个 QLineEdit , QPushButton , QListWidget
  • 先使用⽔平布局把 QLineEdit 和 QPushButton 放好, 并设置这两个控件的垂直方向的
    sizePolicy 为 Expanding
  • 再使用垂直布局把 QListWidget 和上面的水平布局放好.
  • 设置垂直布局的 layoutStretch 为 5, 1 (自己课根据个人喜好微调)

2.在 widget.cpp 中, 先创建两个全局常量, 表示服务器的 IP 和 端口

c 复制代码
const QString SERVER_IP = "127.0.0.1";
const quint16 SERVER_PORT = 8080;
  1. 创建 QUdpSocket 成员,修改 widget.h, 定义成员
c 复制代码
class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();
void process();
private slots:
void on_pushButton_clicked();

private:
    Ui::Widget *ui;
    QUdpSocket* socket;
};

4.修改 widget.cpp, 初始化 socket

c 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    //设置窗口标题
    this->setWindowTitle("client");
    //创建QUdpSocket实例
    socket = new QUdpSocket(this);
    //连接信号槽
    connect(socket,&QUdpSocket::readyRead,this,&Widget::process);
}

5.发送按钮 slot 函数, 实现发送请求.

c 复制代码
void Widget::on_pushButton_clicked()
{
    //获取输入数据
    QString text = ui->lineEdit->text();
    //封装数据报
    QNetworkDatagram requestDategram(text.toUtf8(),QHostAddress(SERVER_IP),
                                     SERVER_PORT);
    //发送请求
    socket->writeDatagram(requestDategram);
    ui->listWidget->addItem("客户端说:"+text);
    ui->lineEdit->setText("");

}

6.通过信号槽, 来处理服务器的响应.

c 复制代码
//处理响应
void Widget::process()
{
    //收到响应
    QNetworkDatagram responseDatagram = socket->receiveDatagram();
    QString response = responseDatagram.data();
    ui->listWidget->addItem("服务器说:"+response);
}

结果展示,启动多个客户端都可以正常工作

TCP Socket

TCP三次握手,建立流程的过程是操作系统在内核中完成的,应用层的代码只能告诉内核,我要发起一个连接(客户端),或者是我要获取一个建立好的连接(服务器)。

  • QTcpServer 是"接待员":它的唯一工作就是监听网络端口,等待并接受新的连接请求。它不参与具体的数据交换。
  • QTcpSocket 是"通讯员":它的核心工作是建立一个连接通道,负责所有数据的发送和接收。无论是客户端发起的连接,还是服务器接受的连接,最终进行数据通信的都是 QTcpSocket 对象。

核心 API 概览

核心类是两个: QTcpServerQTcpSocket
QTcpServer 用于监听端口, 和获取客户端连接。

名称 类型 说明 对标原生 API
listen(const QHostAddress&, quint16 port) 方法 绑定指定的地址和端口号,并开始监听。 bindlisten
nextPendingConnection() 方法 从系统中获取一个已经建立好的 TCP 连接。返回一个 QTcpSocket,表示这个客户端的连接。通过这个 socket 对象完成和客户端之间的通信。 accept
newConnection 信号 有新的客户端建立连接好之后触发。 无(类似于 IO 多路复用中的通知机制)

QTcpSocket 用户客户端和服务器之间的数据交互。

名称 类型 说明 对标原生 API
readAll() 方法 读取当前接收缓冲区中的所有数据,返回 QByteArray 对象。 read
write(const QByteArray&) 方法 把数据写入 socket 中。 write
deleteLater 方法 暂时把 socket 对象标记为无效,Qt 会在下个事件循环中析构释放该对象。 无(类似于"半自动化的垃圾回收")
readyRead 信号 有数据到达并准备就绪时触发。 无(类似于 IO 多路复用中的通知机制)
disconnected 信号 连接断开时触发。 无(类似于 IO 多路复用中的通知机制)

QByteArray 用于表示⼀个字节数组. 可以很方便的和 QString 进行相互转换.

例如:

• 使用 QString 的构造函数即可把 QByteArray 转成 QString.

• 使用 QString 的 toUtf8 函数即可把 QString 转成 QByteArray

回显服务器

  1. 创建界⾯. 包含⼀个 QListWidget , 用于显示收到的数据

2.创建 QTcpServer 并初始化

修改 widget.h, 添加 QTcpServer 指针成员.

c 复制代码
class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();
public slots:
    void processConnection();
    QString process(const QString& response);
private:
    Ui::Widget *ui;
    QTcpServer* server;
};

3.修改 widget.cpp, 实例化 QTcpServer 并进行后续初始化操作

• 设置窗口标题

• 实例化 TCP server. (父元素设为当前控件, 会在父元素销毁时被⼀起销毁).

• 通过信号槽, 处理客户端建立的新连接.

• 监听端口

c 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    //设置窗口标题
    this->setWindowTitle("server");
    //创建QTcpServer实例对象
    server = new QTcpServer(this);//绑定对象树
    //绑定信号槽,获取新连接时触发信号
    connect(server,&QTcpServer::newConnection,this,&Widget::processConnection);
    //绑定端口号并监听
    bool ret = server->listen(QHostAddress::Any,8080);
    if(!ret)
    {
     QMessageBox::critical(this,"服务器绑定失败",server->errorString());
     return;
    }
}

4.继续修改 widget.cpp, 实现处理连接的具体方法 processConnection

• 获取到新的连接对应的 socket.

• 通过信号槽, 处理收到请求的情况

• 通过信号槽, 处理断开连接的情况

c 复制代码
//处理新连接
void Widget::processConnection()
{
    //获取新连接
    QTcpSocket* socket = server->nextPendingConnection();
    QString log = "[" + socket->peerAddress().toString()+":"+
            QString::number(socket->peerPort()) + "],client上线!";
    ui->listWidget->addItem(log);
    //添加信号槽
    connect(socket,&QTcpSocket::readyRead,this,[=](){
        //获取请求
        QString request = socket->readAll();
        //处理请求
        QString response = process(request);
        //发送响应
        socket->write(response.toUtf8());
        QString log = "["+socket->peerAddress().toString()+":"+
                QString::number(socket->peerPort())+"] request:"+request+
        ",response"+response;
        ui->listWidget->addItem(log);
    });
    //处理断开连接的情况
    connect(socket,&QTcpSocket::disconnected,this,[=](){
        QString log = "[" + socket->peerAddress().toString()+":"+
                QString::number(socket->peerPort()) + "],client下线!";
        ui->listWidget->addItem(log);
        socket->deleteLater();
    });

}

5.实现 process 方法, 实现根据请求处理响应.

由于我们此处是实现回显服务器. 所以 process 方法中并没有包含实质性的内容

c 复制代码
QString Widget::process(const QString &response)
{
    return response;
}

此时, 服务器程序编写完毕.

但是直接运行还看不出效果. 还需要搭配客户端来使用。

回显客户端

  1. 创建界⾯. 包含⼀个 QLineEdit , QPushButton , QListWidget
  • 先使用⽔平布局把 QLineEdit 和 QPushButton 放好, 并设置这两个控件的垂直方向的
    sizePolicy 为 Expanding
  • 再使用垂直布局把 QListWidget 和上面的水平布局放好.
  • 设置垂直布局的 layoutStretch 为 5, 1 (自己课根据个人喜好微调)

2.在 widget.cpp 中, 先创建两个全局常量, 表示服务器的 IP 和 端口

c 复制代码
const QString SERVER_IP = "127.0.0.1";
const quint16 SERVER_PORT = 8080;
  1. 创建 QUdpSocket 成员,修改 widget.h, 定义成员
c 复制代码
class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();


private slots:
    void on_pushButton_clicked();
    void process();
private:
    Ui::Widget *ui;
    QTcpSocket* socket;
};

3.修改 widget.cpp, 对 QTcpSocket 进行实例化.

• 设置窗口标题

• 实例化 socket 对象 (父元素设为当前控件, 会在父元素销毁时被⼀起销毁).

• 和服务器建立连接.

• 等待并确认连接是否出错

c 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    //设置窗口标题
    this->setWindowTitle("client");
    //实例化QTcpSocket对象
    socket = new QTcpSocket(this);
    //连接信号槽
    connect(socket,&QTcpSocket::readyRead,this,&Widget::process);
    //和服务器建立连接
    socket->connectToHost(SERVER_IP,SERVER_PORT);
    //等待并确认连接是否出错
    if(!socket->waitForConnected())
    {
        QMessageBox::critical(this,"服务器建立连接失败",socket->errorString());
        return;
    }
}

4.修改 widget.cpp, 给按钮增加点击的 slot 函数, 实现发送请求给服务器.

c 复制代码
void Widget::on_pushButton_clicked()
{
    //获取输入框中的内容
    QString text = ui->lineEdit->text();
    //发送请求
    socket->write(text.toUtf8());
    //打印日志
    QString log = "客户端说:"+text;
    ui->listWidget->addItem(log);
    ui->lineEdit->setText("");
}

5.通过信号槽, 处理收到的服务器的响应.

c 复制代码
//处理获取的内容
void Widget::process()
{
    //获取响应
    QString response = socket->readAll();
    //打印日志
    QString log = "服务器说:"+response;
    ui->listWidget->addItem(log);
}

先启动服务器, 再启动客户端(可以启动多个), 最终执行效果:

由于我们使用信号槽处理同⼀个客户端的多个请求, 不涉及到循环, 也就不会使客户端之间相互影响了。

相关推荐
墨月白1 小时前
[QT]QList 相关接口
qt
Felven1 小时前
盛科工业千兆网交换机端口计数查看
运维·网络·盛科交换机
闻缺陷则喜何志丹1 小时前
【计算几何 矢量】2280. 表示一个折线图的最少线段数|1681
c++·数学·计算几何·矢量
Unlyrical1 小时前
为什么moduo库要进行线程检查
linux·服务器·开发语言·c++·unix·muduo
GIS阵地1 小时前
Qt实现简易仪表盘
开发语言·c++·qt·pyqt·qgis·qt5·地理信息系统
天天摸鱼的小学生1 小时前
【Java Enum枚举】
java·开发语言
崇山峻岭之间1 小时前
C++ Prime Plus 学习笔记028
c++·笔记·学习
阿猿收手吧!1 小时前
【C++】cpp虚函数和纯虚函数的声明和定义
开发语言·c++
q_30238195561 小时前
Python实现基于多模态知识图谱的中医智能辅助诊疗系统:迈向智慧中医的新篇章
开发语言·python·知识图谱