网络编程也是操作系统提供的一组API(Socket API)。
进行网络编程的时候,本质上是在编写应用层代码,需要传输层提供支持,传输层最核心的协议有UDP和TCP,并且这俩协议,差别还很大,Qt也就提供了两套API
要想使用网络编程的API,需要在项目中的 .pro 文件 中添加network 模块。添加之后要手动编译一下项目,使Qt Creator能够加载对应模块的头文件。之前学的Qt的各种控件,各种内容,都是包含在QtCore模块中的(默认就添加好的)。
UDP Socket
核心API概览
主要的类有两个,QUdpSocket 和 QNetworkDatagram
QUdpSocket表示一个UDP的socket文件
bind(const QHostAddress&, quint16) 绑定指定的端⼝号, 对应bind
receiveDatagram()返回QNetworkDatagram 读取一个UDP数据报,对应recvfrom
writeDatagram(const QNetworkDatagram&)发送一个UDP数据报。对应sendto
readyRead 是一个信号,在收到数据并准备就绪后触发(类似于IO多路复用的通知机制)
QNetworkDatagram表示一个UDP数据报
QNetworkDatagram(const QByteArray&,const QHostAddress& , quint16)
构造函数,通过QByteArray ,目标IP地址, 目标端口号构造一个UDP数据报. 通常用于发送数据时
data() 方法,获取数据报内部持有的数据.返回QByteArray
senderAddress() 方法,获取数据报中包含的对端的IP地址
senderPort() 方法,获取数据报中包含的对端的端口号
UDP回显服务器
服务器的.h
cpp
#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;
void processRequest();
QString process(const QString& request);
};
#endif // WIDGET_H
.cpp
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("服务器");
//连接信号槽,设置solt函数处理信号
connect(socket,&QUdpSocket::readyRead,this,&Widget::processRequest);
//要先连接信号槽,再绑定端口号;一旦绑定端口了,意味着请求可以被收到了
//如果再完成绑定之后,连接信号之前有请求过来了,此时可能读不到请求
bool ret = socket->bind(QHostAddress::Any,9099);
if(!ret)//绑定失败的情况
{
QMessageBox::critical(this,"服务器启动出错",socket->errorString());
return;
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::processRequest()
{
//读取请求并解析
const QNetworkDatagram& requestData = socket->receiveDatagram();
QString request = requestData.data();
//根据请求计算响应(回显服务器,请求啥返回啥)
const QString& response = process(request);
//把响应写回给客户端
QNetworkDatagram responseData(response.toUtf8(),requestData.senderAddress(),requestData.senderPort());
socket->writeDatagram(responseData);
//交互信息显示到界面
QString log = "[" + requestData.senderAddress().toString() + ":" + QString::number(requestData.senderPort()) + "]req"
+ request + ",resp:" + response;
ui->listWidget->addItem(log);
}
QString Widget::process(const QString &request)
{
return request;
}
客户端

qt中不建议同时这样搞,如果打开的两个项目有着大量同名文件的话就非常容易混淆
.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include<QUdpSocket>
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_clicked();
private:
Ui::Widget *ui;
QUdpSocket* socket;
void processResponse();
};
#endif // WIDGET_H
.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
#include<QNetworkDatagram>
const QString& SERVER_IP = "127.0.0.1";
const quint16 S_PORT = 9099;
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::on_pushButton_clicked()
{
//获取输入框的内容
const QString& text = ui->lineEdit->text();
//构造udp的请求数据QNetworkDatagram
QNetworkDatagram requestDatagram(text.toUtf8(),QHostAddress(SERVER_IP),S_PORT);
//发送请求数据
socket->writeDatagram(requestDatagram);
//发送的请求到那个大的列表框中
ui->listWidget->addItem("客户端:"+text);
ui->lineEdit->setText("");
}
void Widget::processResponse()
{
//处理收到的响应
//读取响应数据
const QNetworkDatagram& responseData =socket->receiveDatagram();
QString response = responseData.data();
//显示
ui->listWidget->addItem("服务器:"+ response);
}

TcpSocket
核心API概览
核心类是两个:QTcpServer和QTcpSocket
QTcpServer用于监听端口,和获取客户端连接
listen(const QHostAddress&, quint16 port) 一个方法
绑定指定的地址和端口号,并开始监听 对应的原生API:bind和listen
nextPendingConnection() 方法
从系统中获取到一个已经建立好的Tcp连接;返回一个QTcpSocket,表示这个客户端的连接
通过这个 socket 对象 完成和客户端之间的通信 对应的原生API:accept
newConnection 信号
有新的客户端建立连接好之后触发 accept中的等待操作
QTcpSocket 用户客户端和服务器之间的数据交互
readAll() 方法
读取当前接收缓冲区中的所有数据 返回QByteArray对象,对应 read
write(const QByteArray&) 方法 把数据写入socket中,对应write
deleteLater 方法
暂时把socket对象标记为无效。Qt会在下个事件循环中析构释放该对象
类似于"半自动化的垃圾回收"
readyRead 信号
有数据到达并准备就绪时触发
类似于IO多路复用中的通知机制
disconnected 信号
连接断开时触发
TCP回显服务器
服务器
.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include<QTcpServer>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
void processConnection();
~Widget();
private:
Ui::Widget *ui;
QTcpServer* tcpServer;
QString process(const QString request);
};
#endif // WIDGET_H
.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
#include<QMessageBox>
#include<QTcpSocket>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("服务器");
//实例化tcpserver
tcpServer = new QTcpServer(this);
//通过信号槽------处理客户端的连接
connect(tcpServer,&QTcpServer::newConnection,this,&Widget::processConnection);
//绑定窗口,监听
bool ret = tcpServer->listen(QHostAddress::Any,9099);
if(!ret)
{
//绑定失败
QMessageBox::critical(this,"服务器绑定失败",tcpServer->errorString());
exit(1);
}
}
void Widget::processConnection()
{
//连接后处理信息的接受(然后打印)和发送
//先通过tcpserver获取socket对象,通过这个对象来和客户端进行通信
QTcpSocket* clientsocket = tcpServer->nextPendingConnection();
QString log = "["+ clientsocket->peerAddress().toString() + ":" + QString::number(clientsocket->peerPort()) + "] 客户端上线";
ui->listWidget->addItem(log);
//通过信号槽,处理客户端的请求 这里就不去定义slot函数了,直接用lambda表达式去完成
//之前完成这一步是通过循环来完成的,Qt这里是slot函数,客户端每发一次消息就触发一次信号
//每触发一次信号就调用一次槽函数,就不用写循环了
connect(clientsocket,&QTcpSocket::readyRead,this,[=]() {
//先读取数据
QString request =clientsocket->readAll();
//处理响应
const QString& response = process(request);
//返回客户端
clientsocket->write(response.toUtf8());
//打印
QString log = "[" + clientsocket->peerAddress().toString() + ": " + QString::number(clientsocket->peerPort())
+ "]req:" + request + ",resp:" + response;
ui->listWidget->addItem(log);
});
//通过处理客户端断开连接的情况
connect(clientsocket,&QTcpSocket::disconnected,this,[=](){
//log日志显示一下
QString log = "["+ clientsocket->peerAddress().toString() + ":" + QString::number(clientsocket->peerPort()) + "] 客户端下线";
ui->listWidget->addItem(log);
//手动释放socket(tcpsocket每个客户端都有一个这样的对象,也就是说会存在N个,不释放会导致内存泄漏和文件描述符泄漏;不像udpsocket只有一份)
clientsocket->deleteLater();//不是立即销毁,在下一轮事件循环中再进行销毁操作
});
}
Widget::~Widget()
{
delete ui;
}
QString Widget::process(const QString request)
{
return request;
}
当然上述的代码不够严谨,实际使用tcp的过程中,由于是面向字节流的,一个完整的请求可能会分成多断字节数组进行传输,无法处理粘包问题。更严谨的做法是每次收到的数据都给放到一个大的字节数组缓冲区中,并且提前约定好应用层协议的格式,再按照协议格式对缓冲区数据进行解析处理。
Tcp回显客户端
和udp差不多,只不过部分调用函数不同

.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include<QTcpSocket>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_clicked();
private:
Ui::Widget *ui;
QTcpSocket* socket;
};
#endif // WIDGET_H
.cpp
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
socket=new QTcpSocket(this);
//建立连接
socket->connectToHost("127.0.0.1",9099);//这个函数不会阻塞等待三次握手完毕
//处理服务器响应
connect(socket,&QTcpSocket::readyRead,this,[=](){
QString response = socket->readAll();
ui->listWidget->addItem("服务器:"+response);
});
//等待连接结果
bool ret = socket->waitForConnected();//这个函数就是阻塞等待
if(!ret)
{
QMessageBox::critical(this,"连接服务器出错",socket->errorString());
exit(1);
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_clicked()
{
//获取输入框的内容
const QString& text = ui->lineEdit->text();
//发送数据给服务器
socket->write(text.toUtf8());
//打印到界面上
ui->listWidget->addItem("客户端:"+text);
ui->lineEdit->setText("");
}
HTTPClient
在进行Qt开发时,和服务器之间的通信很多时候也会用到HTTP协议。实际上HTTP的使用比TCP/UDP更多一些,Qt中也提供了HTTP的客户端,HTTP协议本质上也就是基于TCP实现的。
核心API
关键类QNetworkAccessManager、 QNetworkRequest、QNetworkReply、QNetworkAccessManager提供了HTTP的核心操作
get(const QNetworkRequest& )
发起一个HTTP GET请求。返回QNetworkReply对象
post(const QNetworkRequest&,const QByteArray& )
发起一个HTTP POST请求。返回QNetworkReply对象.
QNetworkRequest
表示一个HTTP请求(不含body)
如果需要发送一个带有body的请求(比如post),会在QNetworkAccessManager 的post方法 中通过单独的参数来传入body
QNetworkRequest(const QUrl&)
通过URL构造⼀个HTTP请求
setHeader(QNetworkRequest::KnownHeaders header, const QVariant&value)
设置请求头
其中的QNetworkRequest::KnownHeaders 是⼀个枚举类型,常用取值:
ContentTypeHeader 描述body的类型
ContentLengthHeader 描述body的长度
LocationHeader ⽤于重定向报文中指定重定向地址(响应中使用,请求用不到)
CookieHeader 设置cookie
UserAgentHeader 设置User-Agent
QNetworkReply表示一个HTTP响应,这个类同时也是QIODevice 的子类
error() 获取出错状态
errorString() 获取出错原因的⽂本
readAll() 读取响应body
header(QNetworkRequest::KnownHeaders header)
读取响应指定header的值
此外,QNetworkReply 还有⼀个重要的信号finished 会在客户端收到完整的响应数据之后触发.
代码示例:
客户端的界面参考之前的udp、tcp客户端

.h引入QNetworkAccessManager

然后再.cpp中完成实例化对象

处理发送的槽函数

.h代码
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include<QNetworkAccessManager>
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_clicked();
private:
Ui::Widget *ui;
QNetworkAccessManager* manager;
};
#endif // WIDGET_H
.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
#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()
{
//获取输入框中的url
QUrl url(ui->lineEdit->text());
//构造一个HTTP请求对象(用url去构建)
QNetworkRequest request(url);
//发送请求
QNetworkReply* response=manager->get(request);//get后会返回一个响应
//get本身不是阻塞函数并不负责等待响应回来
//等待是由QNetworkReply去完成的,这里边有个finished信号,当我们的响应被收回到客户端之后finished信号就会被触发
//通过信号槽来处理响应
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->deleteLater();//自动释放
});
}
