前言
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();});
}