(2019-7-27写的笔记,2021-7-21更新部分代码)
0.前言
本文是Qt中HTTP相关接口一个简单总结,主要是get/post请求以及表单提交等应用,HTTP相关知识自行百度。
下一篇,文件上传下载:https://gongjianbo1992.blog.csdn.net/article/details/119490369
1.简介
从Qt4.4开始,引入了QNetworkRequest、QNetworkReply 和 QNetworkAccessManager等类来进行HTTP、FTP的操作,替代之前的QFtp和QHttp。要使用这些类,先在pro文件中引入network模块:
QT += network
2.认识QNetworkAccessManager
网络访问API围绕一个QNetworkAccessManager对象构建,该对象包含其发送的请求的通用配置和设置。它包含代理和缓存配置,以及与此类问题相关的信号,以及可用于监控网络操作进度的回复信号。由于QNetworkAccessManager基于QObject,因此只能从它所属的线程中使用它。
创建QNetworkAccessManager对象后,应用程序就可以使用它通过网络发送请求。 该类提供了一组标准函数,它们接收请求和可选数据,每个函数都返回一个QNetworkReply对象。返回的对象用于获取响应相应请求而返回的任何数据。
//构建一个manager对象
QNetworkAccessManager *manager = new QNetworkAccessManager(this);
//manager具有异步API,当http请求完成后,会通过finished信号进行通知
connect(manager,&QNetworkAccessManager::finished,this,&MyClass::replyFinished);
//发送异步get请求
manager->get(QNetworkRequest(QUrl("http://qt-project.org")));
//这里也可以用一个QEventLoop来等待请求完成,但是我更爱用槽函数
//QNetworkReply *reply=manager->get(request);
//QEventLoop eventLoop;
//connect(reply, &QNetworkReply ::finished, &eventLoop, &QEventLoop::quit);
//eventLoop.exec();
//QByteArray reply_data=reply->readAll();
QNetworkAccessManager将收到的请求排队。并行执行的请求数取决于协议。 目前,对于桌面平台上的HTTP协议,一个主机/端口组合并行执行6个请求。请求完成后,用户有责任在适当的时间删除QNetworkReply对象。不要在连接到finished()的插槽内直接删除它,可以使用deleteLater()函数。
通过Operation枚举可以看支持的请求方式:
可以看到,HTTP的常用请求方式都有对应的方法,并且提供了自定义的接口。
也可以通过supportedSchemes()方法查看支持的协议:
QNetworkAccessManager *manager=new QNetworkAccessManager(this);
//查看支持的协议
qDebug()<<manager->supportedSchemes();
//安装了openssl,会有https
//("ftp", "file", "qrc", "http", "data")
3.服务请求QNetworkRequest
要发起一个get/post请求,首先要构建一个QNetworkRequest对象作为参数,它包含一个URL和一些可用于修改请求的辅助信息。
//构建请求对象
QNetworkRequest request;
request.setUrl(QUrl("http://httpbin.org/get"));
request.setRawHeader("Content-Type","application/json");
//发送请求
//manager->get(request);
//manager->post(request, QByteArray());
//manager->put(request, QByteArray());
有时需要在url中携带参数,如果手动进行字符串拼接不是很方便。Qt5.0提供了 QUrlQuery类,可以很方便的拼接和解析url中的参数。
QUrl url("http://httpbin.org/get");
//拼接
QUrlQuery query;
query.addQueryItem("ie","utf-8");
url.setQuery(query);
qDebug()<<url;
//QUrl("http://httpbin.org/get?ie=utf-8")
//解析
QUrlQuery query2(url);
if(query2.hasQueryItem("ie"))
qDebug()<<query2.queryItemValue("ie");
//"utf-8"
4.服务响应QNetworkReply
调用manager的get、post等接口后会返回一个reply对象,在manager的finished信号种也会传递该reply对象。如果要同步处理就在get、post后用事件循环等finished信号,如果是异步处理直接关联finished信号到槽即可。
//同步处理,可以开启一个局部的事件循环,等待响应,不会阻塞线程
QNetworkReply *reply=manager->get(request);
QEventLoop eventLoop;
connect(reply, &QNetworkReply ::finished, &eventLoop, &QEventLoop::quit);
eventLoop.exec();
//处理reply信息
receiveReply(reply);
这里我使用的是连接manager的finished信号到槽函数的方式来异步处理:
//connect(manager,&QNetworkAccessManager::finished,this,&MyClass::replyFinished);
//槽函数
void MyClass::replyFinished(QNetworkReply *reply)
{
if(reply->error()!=QNetworkReply::NoError){
//处理中的错误信息
qDebug()<<"reply error:"<<reply->errorString();
}else{
//请求方式
qDebug()<<"operation:"<<reply->operation();
//状态码
qDebug()<<"status code:"<<reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qDebug()<<"url:"<<reply->url();
//qDebug()<<"raw header:"<<reply->rawHeaderList();
//获取响应信息
const QByteArray reply_data=reply->readAll();
qDebug()<<"read all:"<<reply_data;
//解析json
QJsonParseError json_error;
QJsonDocument document=QJsonDocument::fromJson(reply_data, &json_error);
if(json_error.error==QJsonParseError::NoError){
if(document.isObject()){
const QJsonObject obj=document.object();
qDebug()<<obj;
if(obj.contains("args")){
QJsonValue value=obj.value("args");
qDebug()<<value;
//QJsonValue(object, QJsonObject({"ie":"utf-8"}))
}
}
}else{
qDebug()<<"json error:"<<json_error.errorString();
}
}
reply->deleteLater();
}
访问有些网页会返回301/302状态码,需要重新请求重定向的地址。
if (status_code == 301 || status_code == 302){
// Or the target URL if it was a redirect:
QVariant redirectionTargetUrl =reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
//qDebug() << "redirection Url is " << redirectionTargetUrl.toString();
QUrl url(redirectionTargetUrl.toString());
manager->get(QNetworkRequest(url));
}
也可以用定时器定时来调用reply的abort或者close,会提前finished终止当前任务。Qt 5.15 新增了 setTransferTimeout 接口,可对 QNetworkRequest 或是 QNetworkAccessManager 设置。
//超时处理,可以使用定时器调用abort、close来终止当前的请求
QNetworkReply *reply=manager->get(request);
if(reply->isRunning()){
QTimer *timer=new QTimer(reply);//对象树关联释放,也可以在finish进行释放
timer->setSingleShot(true);
//超时就结束,abort后状态码为0,错误信息为"Operation canceled"
connect(timer,&QTimer::timeout,reply,&QNetworkReply::abort);
//结束就关定时器
connect(reply,&QNetworkReply::finished,timer,&QTimer::stop);
//定时
timer->start(5000);
}
5.表单数据QHttpMultiPart与QHttpPart
HTTP表单提交需要借助QHttpMultiPart与QHttpPart两个类,官方文档有个小示例(直接F1查看QHttpMultiPart),我这里稍加修改使之可以正常使用。
示例中图片的ContentDispositionHeader没有添加filename,导致测试时提交失败。示例中也没有设置Boundary分隔符,导致我提交到我们公司服务器时失败,加上就好了。这些应该都和服务器的设置有关。
void test(){
QNetworkAccessManager *manager=new QNetworkAccessManager(this);
//构建一个multiPart用于提交表单
//注意,multiPart请在请求完成后再删除
QHttpMultiPart *multiPart=new QHttpMultiPart(QHttpMultiPart::FormDataType);
//文本内容
QHttpPart namePart;
//Content-Type对照表详情百度http://tool.oschina.net/commons/
namePart.setHeader(QNetworkRequest::ContentTypeHeader,QVariant("text/plain"));
namePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"thename\";"));
namePart.setBody("gongjianbo");
QHttpPart agePart;
agePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"theage\";"));
agePart.setBody("27");
multiPart->append(namePart);
multiPart->append(agePart);
//文件内容
QHttpPart filePart;
filePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("text/plain")); //貌似我们公司我用application也行
//示例里没有filename,导致提交不成功
filePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"thefile\";filename=\"file.txt\";"));
QFile *textFile = new QFile("F:/Src/file.txt");
textFile->setParent(multiPart); //在删除reply时一并释放
if(textFile->open(QIODevice::ReadOnly)){
//要读取小块数据,请使用setBody(); 对于像图像这样的较大数据块,请使用setBodyDevice()。
filePart.setBodyDevice(textFile);
multiPart->append(filePart);
}
//图片内容
QHttpPart imagePart;
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png"));
//示例里没有filename,导致提交不成功
imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"theimage\";filename=\"image.png\";"));
QFile *imageFile = new QFile("F:/Src/image.png");
imageFile->setParent(multiPart); //在删除reply时一并释放
if(imageFile->open(QIODevice::ReadOnly)){
imagePart.setBodyDevice(imageFile);
multiPart->append(imagePart);
}
//在我们公司里使用的时候,没有Boundary也会导致提交不成功
multiPart->setBoundary("qtdata");
QNetworkRequest request(QUrl("http://httpbin.org/post"));
request.setRawHeader("Content-Type","multipart/form-data;boundary=qtdata");
//提交表单
QNetworkReply *reply=manager->post(request,multiPart);
multiPart->setParent(reply); //在删除reply时一并释放
QEventLoop eventLoop;
connect(reply, &QNetworkReply ::finished, &eventLoop, &QEventLoop::quit);
eventLoop.exec();
qDebug()<<reply->readAll();
reply->deleteLater();
}
6.访问HTTPS
支持HTTPS请求需要配置OpenSSL环境,Qt默认是不带SSl认证的,直接访问HTTPS会有错误信号。
//默认链接的ssl库版本
qDebug()<<QSslSocket::sslLibraryBuildVersionString();
//支持的协议
qDebug()<<manager->supportedSchemes();
//正常的reply接收
connect(manager,&QNetworkAccessManager::finished,
this,&MainWindow::slotFinish);
//ssl错误
connect(manager,&QNetworkAccessManager::sslErrors,
this,[=](QNetworkReply *reply,const QList<QSslError> &erros){
qDebug()<<erros;
});
(2019-8-28修改并完善https部分的内容)
以下是我测试的一些配置和测试(测试地址https://httpbin.org/post):
Qt5.9.8+MSVC2015-32bit+openssl1.0.x,将编译出来的libeay32.dll和ssleay32.dll放到exe同级目录能正常访问https(如果是在QtCreator中运行的话,放到安装环境的bin目录下也能使用)。
Qt5.9.8+MinGW-32bit+openssl1.0.x,复制Qt Tools目录下的libeay32.dll和ssleay32.dll放到exe同级目录能正常访问https(我的在D:\Qt\Qt5.9.8\Tools\QtCreator\bin目录)。
Qt5.12.4+MSVC2015-64bit,将编译出来的libcrypto-1_1-x64.dll和libssl-1_1-x64.dll放到exe同级目录就能正常访问https了(我没放openssl的库貌似也能访问,我在家又测试了一遍,发现也是msvc 64bit的不用dll也能访问https,但是32bit的就不行,不知道是系统里有这个库还是啥原因)。
Qt5.13.0+MSVC2019-32bit,将编译出来的libcrypto-1_1.dll和libssl-1_1.dll放到exe同级目录就能正常访问https了。
我的openssl编译记录:https://blog.csdn.net/gongjianbo1992/article/details/100115710
我生成的dll百度云连接:https://pan.baidu.com/s/1ALvI7FStr9YIx942QQTv1Q
提取码:0q96
7.操作实例(代码链接)
这里借助httpbin.org网站写一个简单的例子。httpbin.org是用 Python + Flask编写的一个开源项目,这个网站能测试 HTTP 请求和响应的各种信息,比如 cookie、ip、headers 和登录验证等,且支持 GET、POST 等多种方法,对开发和测试很有帮助。
我的示例链接:https://github.com/gongjianbo/MyTestCode/tree/master/Qt/Qt5HttpDemo
8.参考
参考文档:https://doc.qt.io/qt-5/qnetworkaccessmanager.html
参考博客(HTTP):https://blog.csdn.net/liang19890820/article/details/52535755
参考博客(表单):https://blog.csdn.net/liang19890820/article/details/52548717
参考博客(表单):https://blog.csdn.net/u011728480/article/details/76851172?locationNum=1&fps=1
参考博客(HTTPS):https://www.cnblogs.com/jk-Huan/p/8953541.html
OpenSSL编译:https://blog.csdn.net/gongjianbo1992/article/details/100115710