引言:所谓http协议,本质上也是基于TCP/IP上服务器与客户端请求和应答的标准,web开发中常用的http server有apache和nginx。Qt程序作为http client可以使用QNetworkAccessManager很方便的进行http相关的操作。Qt本身并没有http server相关的库
,也许是因为很少有这种需求吧。但是实际开发中也会有做简单的http server的需求。实际上QT6.4里面引入了基于http的服务器
,但是看了下,也是非常的鸡肋。
大概有以下几个基于QT/C++的http服务器的库。
一、qthttpserver
前面说到,Qt官方在发行版中并没有提供相应的类来实现HTTP 服务端,但是Qt-lab中提供了一个qthttpserver, Github地址在这里,不知道基于什么原因Qt官方没有将其纳入正式的Qt发行版中,如果要使用qthttpserver的话,就得自己下载源码进行编译了,不过编译可没那么容易,会遇到各种错误,而且还没有提供说明文档,这就不太友好了。
关于编译或简单使用参考以下文章:
Qt开发的轻量级http服务器-QtHttpServer编译
关于Qt HttpServer的一些测试(Qt6.4.0rc)
QtHttpServer编译使用,亲测可用
QT基于qhttp-server搭建http服务器
二、cpp-httplib
这是Github上一个开源的C++实现http server的项目,使用方法非常简单,就只有一个头文件,引入到自己的项目中就可以使用了,不过这是一个多线程"阻塞"HTTP 库,如果您正在寻找"非阻塞"库,那就要换别的了。具体使用可以参考Github里面的文档介绍,很详细。
三、 QtWebApp
QtWepApp 是 C++ 中的 HTTP 服务器库,适用于 Linux、Windows、Mac OS 和Qt 框架支持的许多其他操作系统。QtWebApp要求Qt版本至少是 Qt5.15 及其以上
。
这个库使用也非常简单,并且很明显它是用QT写的,所以与Qt的项目可以无缝衔接。
但并不是QT官方写的,不过它的名气比Qt官方qthhttpserver库要大。
有趣的是,Qt制造商多年来一直在开发标准HTTP服务器,但到2022年,它仍然不包括在Qt库中。这也许可以解释为什么很多人使用的库。
实际上QT6.4里面引入了基于http的服务器
,但是看了下,也是非常的鸡肋。
本次选择更为实用的QtWebApp来作为http服务器。
源码下载:http://www.stefanfrings.de/qtwebapp/index-en.html
Project homepage:http://stefanfrings.de/qtwebapp/index-en.html
Tutorial:http://stefanfrings.de/qtwebapp/tutorial/index.html
API doc:http://stefanfrings.de/qtwebapp/api/index.html
关于QtWebApp的使用,可以直接阅读源码提供的demo1,结合日志可以很快的理解。
更好的是直接参考QtWebApp的使用网站:Tutorial,真的是非常详细,,不过是英文版的,需要具备一定的英语阅读能力。网站如下图所示。
可以直接把源码模块直接引入到工程里面使用,源码里面就3个模块{httpserver、logging、templateengine},按需引入即可,,,httpserver模块是最核心的,肯定是要引入的。。。
也可以将源码编译成库,,然后再通过头文件+库的形式去倒腾,,,,
推荐直接引入源码,首先QtWebApp的模块并不大,,而且可以随时调试源码或修改源码,如下文陈述,就涉及到了修改源码。
四、双向认证
之前,使用nginx作为https服务器,与客户端进行了双向认证和单向认证。
参考以下两篇文章,本篇是在以下两篇文章的基础上继续开发的,,证书、私钥等的制作及流程验证在以下文章中均作了详细的说明。所以,阅读此篇文章前务必首先阅读以下两篇文章。
ssl单向证书和双向证书校验测试及搭建流程
QT充当客户端模拟浏览器等第三方客户端对https进行双向验证
此处,采用QtWebApp来手动开发https服务器,来完成与客户端之间基于ssl的双向认证
。
客户端:win10 + Qt5.9.9,192.168.64.1
服务端:win10 + Qt6.4, 192.168.64.176
此时直接将QtWebApp的demo1拷过来,并在配置文件中指定证书及秘钥,证书和秘钥在之前的文章中都倒腾过,并且直接使用之前做好的,可以参考ssl单向证书和双向证书校验测试及搭建流程一文。
并将配置文件的端口改为443,这是https的默认端口,,(当然也可以设置为其他端口,写为443时在客户端向服务器发起请求时无需手动指定端口号,否则必须手动指定端口号)
此时运行https服务,发现报错如下,,,,,,,说白了,要想支持https,Qt的版本至少是5.15。。
好,此时去虚拟机中安装Qt6.4。。
安装完毕后,,运行程序,此时报错缺少 core5compat模块。。。
于是,去Qt里添加 Qt 5 Compatibility Module组件,,
倒腾完成后,此时运行,一切OK。。接下来首先确保主机和虚拟机互ping通(注意关闭防火墙)。
结果访问 https://192.168.64.176 时,无任何响应。。
wireshark抓包观察,结果可以明显看出ssl并没有建立成功,client发出第一次握手clienthello时,服务端压根没有任何回应
。。。。服务端连最基本的serverhello都没有回应。。
好,再通过curl工具(可以看作是客户端)看看,,可以看出curl访问时是一直在那儿等着服务端的回应。。
但服务端那边迟迟无响应,,最终握手失败。
bash
curl -k --cert ./client.crt --key ./client.key --show-error https://192.168.64.176 -v
ssl握手失败的情况比较多,,可以参考下两篇文章:https://zhuanlan.zhihu.com/p/572516140
https://www.jiyik.com/tm/xwzj/network_287.html ,说实话,此时你压根就不知道是哪个环节出了问题。。这两篇文章只能是大概看看,有点印象罢了。。。
好,我们来看看服务端的日志,,,
可以发现,实际上没有任何有用的信息。。
此时只能去阅读QtWebApp的源码。。= > 柳暗花明又一村
。。。
因为https服务器的创建肯定是要加载服务端证书和私钥的,,那就只好去看看它是怎么加载的,,在httpconnectionhandlerpool.cpp
源码的140行处是加载服务端私钥的地方。因为之前在通过openssl创建服务端的私钥时指定了私钥的密码123456,而源码中通过QSSLKey创建秘钥时却没有指定密码,,,所以导致出现了客户端发起握手请求clienthello时,服务端无动于衷,,,实际上就是因为服务器加载私钥有误导致的。。。
这里吐槽下,QtWebApp这地方处理的不好,,这种错误应该是要以日志的形式呈现给调试者的,,,,,最终导致握手失败。。。
修改完源码后,此时重新启动服务端程序,,,先用curl先测试一把,,
果然,此时握手已经成功了,客户端也接受了来自服务端的响应。
从此处我们也可以看出,,之前我们一开始通过curl工具握手失败时,分析查看了curl的相关提示,,,以及服务端QtWebApp的日志,都没有看出有价值的提示,,,所以这种情况下,还是需要去通过分析以及修改源码大法来倒腾的,,,
【注意】
此处使用curl命令之前需要首先导入客户端p12格式的证书,可阅读此篇文章:ssl单向证书和双向证书校验测试及搭建流程 中的双向证书校验
小节内容,此处不再赘述。。。
【值得注意的是】
:在 [ssl单向证书和双向证书校验测试及搭建流程]
一文中使用curl工具验证时,并没有事先导入客户端p12格式的证书就直接验证了,也是可以的,当然了,那时的https服务器是基于linux下的nginx的
。此处验证时发现必须首先导入客户端p12格式的证书
,否则直接响应连接失败:schannel: failed to receive handshake, SSL/TLS connection failed
,具体原因未知,,待后倒腾。。
好,抓包看看,客户端和服务器之间是否向彼此发送了证书。
答案是肯定的,,因为只有双向认证的情况下,客户端才会向服务端发送证书。
书归正传,此时,我们已经通过QtWebApp以代码的形式创建了https服务器
,,
接下来,我们的客户端
就要通过我们自己编写的代码
去替代刚才的curl工具或浏览器这种集成的第三方客户端去向服务器发起https请求,,,
css
void MainWindow::testSsl()
{
// 加载客户端证书
QFile crtFile("C:\\Users\\XingWei\\Documents\\untitled19\\client.crt");
crtFile.open(QIODevice::ReadOnly);
QSslCertificate certificate(&crtFile, QSsl::Pem);
crtFile.close();
// 加载客户端私钥
QByteArray passPhrase("123456"); // 私钥的密码
QFile keyFile("C:\\Users\\XingWei\\Documents\\untitled19\\client.key");
keyFile.open(QIODevice::ReadOnly);
QSslKey privateKey(&keyFile, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, passPhrase);
keyFile.close();
// ssl配置
QSslConfiguration conf;
conf.setPeerVerifyMode(QSslSocket::QueryPeer);
conf.setProtocol(QSsl::TlsV1SslV3);
conf.setPrivateKey(privateKey);
conf.setLocalCertificate(certificate);
QNetworkRequest request;
request.setUrl(QUrl("https://192.168.64.176"));
request.setSslConfiguration(conf);
QEventLoop loop;
QNetworkReply* reply = m_manager.post(request, QByteArray("AAAAAAAAAAAAAAAAAAAAA"));
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::finished, this, [=] {
qDebug() << "####################### get data finished...";
QByteArray data = reply->readAll();
qDebug() << data;
});
loop.exec();
qDebug() << "#############> " << reply->errorString() << reply->error();
}
运行效果及抓包如下所示:
客户端和服务端建立起ssl通道后就可以将数据加密交互了。。
好,接下来,我们在https服务器中拦截来自客户端的不同请求路径,进而做出相应的回应。
好,在客户端通过向服务器发送不同的请求路径,看看响应效果如何。。。
因为demo1中提供了几个请求路径,此处就以demo1提供的请求路径为基点来测试。。。
并在服务端新增一个/hello的请求路径,,该路径向客户端响应给以HELLO,SSL.
字符串。
修改上述的客户端代码如下:
javascript
// ...
QNetworkRequest request;
request.setUrl(QUrl("https://192.168.64.176/template"));
// request.setUrl(QUrl("https://192.168.64.176/dump"));
// request.setUrl(QUrl("https://192.168.64.176/hello"));
request.setSslConfiguration(conf);
QEventLoop loop;
QNetworkReply* reply = m_manager.get(request);
// ...
服务端新增HelloController请求处理类相关代码:
cpp
void HelloController::service(HttpRequest &request, HttpResponse &response)
{
response.setHeader("Content-Type", "text/html; charset=UTF-8");
response.write("<html><body>");
response.write("<h1>");
response.write("HELLO, SSL.");
response.write("</h1>");
response.write("</body></html>",true);
}
访问/template
路径时https服务器的响应:
javascript
"<html><body>\r\n\r\nHello,<br>\r\nyou requested the path: /template\r\n<p>\r\nAnd your web browser provided the following headers:\r\n<p>\r\n\r\n <b>accept-encoding:</b> gzip, deflate<br>\r\n\r\n <b>accept-language:</b> zh-CN,en,*<br>\r\n\r\n <b>connection:</b> Keep-Alive<br>\r\n\r\n <b>host:</b> 192.168.64.176<br>\r\n\r\n <b>user-agent:</b> Mozilla/5.0<br>\r\n\r\n\r\n</html></body>\r\n"
访问/dump
时的响应:
kotlin
"<html><body><b>Request:</b><br>Method: GET<br>Path: /dump<br>Version: HTTP/1.1<p><b>Headers:</b><br>accept-encoding=gzip, deflate<br>accept-language=zh-CN,en,*<br>connection=Keep-Alive<br>host=192.168.64.176<br>user-agent=Mozilla/5.0<p><b>Parameters:</b><p><b>Cookies:</b><p><b>Body:</b><br></body></html>"
访问/hello
时的响应:
markup
"<html><body><h1>HELLO, SSL.</h1></body></html>"
用浏览器来验证一下我们的请求,更加直观,如下所示:
【注意】:此处使用浏览器验证时,必须首先导入客户端p12格式的证书,否则是无法访问的。
关于客户端证书的导入,阅读此篇文章:ssl单向证书和双向证书校验测试及搭建流程中的双向证书校验
小节内容,此处不再赘述。。。
由此可见,一切正常。
当https建立后,之前基于http写的请求路径以及对应的处理函数都不用作任何改变,直接向访问http那样访问就可以了,只不过此时需要变成https了
。。
由此可见,https只不过是在http的请求基础上加了一层加密。
关于http的明文传输
和https的加密传输
,可以通过抓包来看。
可进一步阅读 QtWebApp同时开启http服务和https服务,接受来自客户端的不同请求并进行相应的处理 一文中 测试效果小节 所对应的内容。