Qt网络编程
前言
Qt为了支持跨平台,对系统网络编程的API(socket API)也进行了重新分装。
实际Qt中进行网络编程也不一定使用Qt封装的网络API,也有可能使用的是系统原生API或者其他第三方框架的API。
若要使用Qt中的网络编程的API,则要在项目中的.pro文件中添加network模块。
我们学过的Qt的各种控件等常用的功能都是包含在QtCore模块中的,系统默认添加了,为啥Qt要和划分出这些模块呢?Qt本身就是一个非常庞大的框架,如果把所有的Qt功能都放在一起时,即使我们的写的代码非常简单,此时生成的可执行程序也很庞大,因为包含了大量没有使用的功能
进行网络编程的时候,本质上是在编写应用层代码,需要传输层(操作系统内核实现好的)提供支持,socket API就是传输层提供给应用层的接口,传输层最核心的协议有UDP(无连接,不可靠传输,面向数据报,全双工)和TCP(有连接,可靠传输,面向字节流,全双工),并且这两协议差别还挺大,就导致在编写代码的时候也是有所差别的,因此操作系统就提供了两套socket API,所以Qt也提供了两套API,来完成UDP和TCP的开发。
一、UDP Socket
1、核心API
主要的类有两个:QUdpSocket 和 QNetworkDatagram,其中QUdpSocket表示一个UDP的文件,QNetworkDatagram:由于UDP是面向数据报的,每次传输的时候,发送和接受都是一个完整的NetworkDatagram。
QUdpSocket:
当socket收到请求后,QUdpSocket就会触发这个readyRead信号,就可以在信号槽里完成读取请求的操作了。
QNetworkDategram:
2、事例
写一个带有界面的UDP回显服务器
服务器端
//Widget.h#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>
#include<QUdpSocket>QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();private:Ui::Widget *ui;//先创建UdpSocket成员QUdpSocket* socket;//服务器的核心逻辑void processRequest();//服务器业务QString process(const QString& request);
};
#endif // WIDGET_H
//Widget.cpp#include "widget.h"
#include "ui_widget.h"#include<QMessageBox>
#include<QNetworkDatagram>Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);//创建出这个对象socket = new QUdpSocket(this);this->setWindowTitle("服务器");//连接信号槽connect(socket,&QUdpSocket::readyRead,this,&Widget::processRequest);//绑定端口号,必须在绑定端口号之前链接信号槽//如果在绑定完成之后,连接信号槽之前的话,若有客户端把请求发过来的,//此时就可能读不到这样的请求//QHostAddress::Any表示不管有多少网卡都可绑定上去,//端口号可自己写但要在范围内(0~65536)bool ret = socket->bind(QHostAddress::Any,9090);//一个端口号只能被一个socket绑定,若9090被别人绑定的话就会失败if(!ret){//绑定失败QMessageBox::critical(this,"服务器启动出错",socket->errorString());return;}}Widget::~Widget()
{delete ui;
}//服务器的核心逻辑
void Widget::processRequest()
{//1、读取数据const QNetworkDatagram& requestDatagram = socket->receiveDatagram();//2、解析QString request = requestDatagram.data();//虽然它返回的是QBetyArry,其是可以赋值给QString//3、根据请求计算响应(回显服务器,响应不需要计算,就是请求本身)const QString& response = process(request);//4、把响应发送给客户端QNetworkDatagram responseDategram(response.toUtf8(), requestDatagram.senderAddress(), requestDatagram.senderPort());socket->writeDatagram(responseDategram);// 把这次交互的信息, 显示到界面上.QString log = "[" + requestDatagram.senderAddress().toString() + ":" + QString::number(requestDatagram.senderPort())+ "] req: " + request + ", resp: " + response;ui->listWidget->addItem(log);
}QString Widget::process(const QString &request)
{return request;
}
客户端
//Widget.h#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>
#include<QUdpSocket>QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();private slots:void on_pushButton_clicked();private:Ui::Widget *ui;QUdpSocket* socket;void processResponse();
};
#endif // WIDGET_H
//Widget.cpp#include "widget.h"
#include "ui_widget.h"#include<QNetworkDatagram>const QString& SERVER_IP = "127.0.0.1";
const quint16 SERVER_PORT = 9090;Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);socket = new QUdpSocket(this);this->setWindowTitle("客户端");//连接槽函数connect(socket,&QUdpSocket::readyRead,this,&Widget::processResponse);}Widget::~Widget()
{delete ui;
}void Widget::processResponse()
{// 通过这个函数来处理收到的响应.// 1. 读取到响应数据const QNetworkDatagram& responseDatagram = socket->receiveDatagram();QString response = responseDatagram.data();// 2. 把响应数据显示到界面上.ui->listWidget->addItem("服务器说: " + response);
}void Widget::on_pushButton_clicked()
{//1、获取到输入框的内容const QString& text = ui->lineEdit->text();//2、构造UDP的请求数据QNetworkDatagram requestDatagram(text.toUtf8(),QHostAddress(SERVER_IP), SERVER_PORT);//3发送请求数据socket->writeDatagram(requestDatagram);// 4. 把发送的请求也添加到列表框中.ui->listWidget->addItem("客户端说: " + text);// 5. 把输入框的内容也清空一下.ui->lineEdit->setText("");}
二、TCP Socket
1、核心API
核心类有:QTcpServer 和 QTcpSocket。
QTcpServer用于监听端口和获取客户端连接。
QTcpSocket用于客户端和服务端之间的数据交互 。
peerAddress和peerPort用于获取对端的地址和端口,
connectToHost("地址",端口号);和服务器建立连接,
waitForConnected(),等待连接建立的结果,确认是否连接成功,返回bool类型
QByteArry用于表示一个字节数组,可以很方便的和QString进行相互转换,使用QString的构造函数可以把QByteArray转成QString,使用QString的toUtf8 函数可以把QString转成QByteArray
2、事例(写一个TCP回显服务器)
在学习liunx时写的TCP的回显服务器的时候,遇到了一个问题就是多个客户端访问的时候就只有一个生效,引入多线程给每个客户端安排一个单独的线程问题才解决
服务端
//Widget.h#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>
#include<QTcpServer>QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();void processConnection();QString process(const QString );private:Ui::Widget *ui;//QTcpServer* tcpServer;};
#endif // WIDGET_H
//Widget.cpp#include "widget.h"
#include "ui_widget.h"#include<QTcpSocket>
#include<QMessageBox>Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);//修改窗口标题this->setWindowTitle("服务器");//实例化QUdpsocket对象tcpServer = new QTcpServer(this);//绑定槽函数,当有新的客户端建立好连接后connect(tcpServer,&QTcpServer::newConnection,this,&Widget::processConnection);//绑定+监听//这和操作得是初始化的最后一步,都是需要把如何处理连接,如何处理请求等都准备好后才能真正绑定端口并监听bool ret = tcpServer->listen(QHostAddress::Any,9090);if(!ret){QMessageBox::critical(this,"服务器启动失败",tcpServer->errorString());return;}
}Widget::~Widget()
{delete ui;
}void Widget::processConnection()
{//通过tcpServer拿到一个socket对象,通过这个对象来和客户端进行通信QTcpSocket* clientSocket = tcpServer->nextPendingConnection();//显示用户上线 //对端端口号QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "]" + "客户端上线了";ui->listWidget->addItem(log);//如果有数据到达并准备就绪时,通过信号槽来处理客户端发送请求的情况connect(clientSocket,&QTcpSocket::readyRead,this,[=](){//读取请求数据QString req = clientSocket->readAll();//根据请求进行响应QString resp = process(req);//把响应写回客户端clientSocket->write(resp.toUtf8());//上述操作记录到日志中QString log = "[" + clientSocket->peerAddress().toString() + QString::number(clientSocket->peerPort()) + "]"+ "res:" + req + "resp:" + resp;ui->listWidget->addItem(log);});//当客户端断开时connect(clientSocket,&QTcpSocket::disconnected,this,[=](){//显示日志QString log = "[" + clientSocket->peerAddress().toString() + ":" +QString::number(clientSocket->peerPort()) + "]" + "客户端下线";ui->listWidget->addItem(log);//手动释放clientSocket//在下一轮事件循环在进行销毁操作clientSocket->deleteLater();});
}QString Widget::process(const QString req)
{return req;
}
客户端
//Widget.h#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>
#include <QTcpSocket>QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();private slots:void on_pushButton_clicked();private:Ui::Widget *ui;QTcpSocket* tcpSocket;
};
#endif // WIDGET_H
//Widget.cpp#include "widget.h"
#include "ui_widget.h"#include<QMessageBox>Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);//设置窗口标题this->setWindowTitle("客户端");//创建socket实例对象tcpSocket = new QTcpSocket(this);//和服务器建立连接tcpSocket->connectToHost("127.0.0.1",9090);//连接信号槽处理响应connect(tcpSocket,&QTcpSocket::readyRead,this,[=](){//读取响应内容QString resp = tcpSocket->readAll();//把响应内容显示到界面上ui->listWidget->addItem("服务器说:" + resp);});//等待连接建立的结果,确认是否连接成功bool ret = tcpSocket->waitForConnected();if(!ret){QMessageBox::critical(this,"连接服务器出错",tcpSocket->errorString());exit(1);}
}Widget::~Widget()
{delete ui;
}void Widget::on_pushButton_clicked()
{QString text = ui->lineEdit->text();ui->lineEdit->clear();//把请求发送到服务端tcpSocket->write(text.toUtf8());//把发送的消息显示到界面上ui->listWidget->addItem("客户端说:" + text);
}
三、HTTP client
HTTP的使用相比于TCP/UDP更多一些,TCP/UDP是传输层的协议,所以还要在应用层搭配程序员自定义的协议完成工作,而HTTP是一个现成的应用协议,本身就提供了一些方便的拓展能力以及特殊功能,所以更多的时候用HTTP完成数据的交互。
HTTP协议本质上就是基于TCP协议实现的,所以实现一个HTTP客户端/服务器也就是基于TCP Socket进行了封装,Qt中只是提供了HTTP客户端,而没有提供HTTP服务器的库。
1、核心API
关键类主要有三个:QNetworkAccessManager、QNetworkRequest、QNetworkReply
QNetworkAccessManger提供了HTTP的核心操作
QNetworkRequest表示一个HTTP请求(不含body),如果需要发送带有body的请求(比如post)会在QNetworkAccessManger的post方法中通过单独的参数来传入body
QVariant表示类型可变的值,类似于C语言中的void*
其中的QNetworkRequest::KnownHeaders是一个枚举类型,常用取值:
QNetworkReply表示一个HTTP 响应.这个类同时也是QIODevice的子类。
此外,QNetworkReply还有一个重要的信号finished会在客户端收到完整的响应数据之后触发。
2、事例(给服务器发送一个get请求)
//Widget.h#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>
#include <QNetworkAccessManager>QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();private slots:void on_pushButton_clicked();private:Ui::Widget *ui;QNetworkAccessManager* manager;
};
#endif // WIDGET_H
//Widget.cpp#include "widget.h"
#include "ui_widget.h"#include<QUrl>
#include<QNetworkRequest>
#include<QNetworkReply>Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);this->setWindowTitle("客户端");manager = new QNetworkAccessManager(this);}Widget::~Widget()
{delete ui;
}void Widget::on_pushButton_clicked()
{// 1. 获取到输入框中的 url.QUrl url(ui->lineEdit->text());// 2. 构造一个 HTTP 请求对象QNetworkRequest request(url);// 3. 发送请求QNetworkReply* response = manager->get(request);// 4. 通过信号槽, 来处理响应connect(response, &QNetworkReply::finished, this, [=]() {if (response->error() == QNetworkReply::NoError) {// 响应正确获取到了.QString html = response->readAll();ui->plainTextEdit->setPlainText(html);} else {// 响应出错了ui->plainTextEdit->setPlainText(response->errorString());}// 还需要对 response 进行释放.response->deleteLater();});
}