QtHttpServer路由API

作者:Richard Lin | May 28, 2019 9:37:28 AM

本文翻译自QtHttpServer routing API

原文作者:Mikhail Svetkin

校审:Richard Lin

大家好。首先,感谢大家对之前博文的评论。

今天,我来谈谈路由,包括它的工作原理以及我们是如何实现它的。

在开始之前,我想澄清一些事情。我们已经在GitHub上看过很多类似的项目,它们使用各种语言开发而不仅仅是C++。

我们发现大多数用C++编写的项目都过于复杂或者过于底层。

有的“Hello world”示例程序都需要20-30行代码。

用C++以外的其他语言编写的项目则非常庞大,有很大的生态系统。因此,我们无法重新实现他们已经完成的所有功能。

这就是为什么我们希望创建既简单、又易于扩展的东西。

注:非常感谢Flask项目,是它给我灵感创建了这套API。

什么是路由,它是如何工作的?

路由是负责对某个特定的请求执行对应的回调函数的进程。

一个路由是某个特定的请求URL对应的特定规则(回调函数)。

让我们来看一个例子:

http://blog.qt.io/dev // blogs for Dev loop
http://blog.qt.io/biz // blogs for Biz Circuit
http://blog.qt.io/blog/2019/ // blogs for 2019 (dev loop + Biz Circuit)

每个地址代表具有特定回调函数的某个请求。

另外,您可能已经注意到其中一个有参数(http://blog.qt.io/blog/2019/).

这个URL请求2019年的整个博客,因此回调能够获取到请求的年份。

这样的路由称为动态路由,另外两个是静态路由。

静态路由

我认为静态路理解起来非常简单,让我们来看看如何使用QHttpServer来实现它吧。

1
2
3
4
5
6
7
8
9
QHttpServer server;
server.route( "/dev/" , [] () {
     return "All dev loop blogs" ;
});
server.route( "/biz/" , [] () {
     return "All biz loop blogs" ;
});

我们只是将每个路径绑定到其回调函数上。我想动态路由会更有意思。

动态路由

首先,我建议把任务分成子任务。然后,理解我们需要实现什么。

  • 我们需要一种机制来从URL中的路径中捕获参数。
  • 将捕获的参数转换为我们希望拥有的类型。

这是最重要的两个要点。我建议把每一项都认真看一遍。

一开始,您可能会问:“需要那么复杂吗?我们能用正则表达式解决问题吗?”

为了更好地理解,我认为我们应该从上面这个例子开始,尝试用正则表达式来解决。

/blog/(\\d+)/

看起来很完美。我们可以捕捉一个参数。如何将参数转换为int类型呢?

最常见的方法是将参数保持为字符串,然后手动转换。

但这种方法有几个缺点:

  • 如果我们有几个参数,那么我们需要对所有的参数都这样做。
  • 并不是每个人都想编写正则表达式 —— 特别是对于字符串或浮点型。

我们如何改进它?让我们看看使用其他语言的同行们是怎么做的。

现代框架(Django、Flask、Ruby on Rails)都是以同样的方式实现的。

例如,它可以是这样的:

/blog/<int:year>/

看上去很清楚。int用作正则表达式的别名,现在我们也知道了类型。

year用于将捕获的参数绑定到回调参数。

这种方法允许您轻松地添加自定义类型,如下所示:

/blog/<HexInt:year>/

现在,我们对路由知道得够多了,可以开始实现它了。

QHttpServer::route

我建议这样做:

1
2
3
4
5
6
7
route( "/blog/<int:year>" , [] ( const QHttpServerRequest &request) {
     return blogs_by_year(request[ "year" ].toInt());
});
route( "/blog/<int:year>" , [] (auto year) {
     return blogs_by_year(year.toInt());
});

看起来不错,但我们还是可以继续改进的。如果您仔细观察第二种情况,您会发现参数的名称“year”在路径格式中并不是必须的。它被放在那儿是因为在C++中不能将捕获的参数绑定到回调参数中。而且我们也无法对路径格式类型和回调参数进行编译时检查。那么我们该如何改进呢?

我们可以使用静态类型。这样我们就可以将编译器用作为“控制器”,确保我们得到的类型是我们支持的类型。

要解决第二个子任务(类型转换,记得吗?),我们可以使用QVariant,它可以很容易地转换参数。

那我们如何做呢?可以把它们结合起来,就像这样:

1
2
3
4
QHttpServer server;
server.route( "/blog/" , [] ( int year) {
     return blogs_by_year(year);
});

我们能得到什么?

  • 不需要在路径格式和回调函数中重复类型名称和参数名称。
  • 类型将自动转换。
  • 额外优势:编译时检查支持的类型。

很酷,不是吗?!

此外,我们还支持多种类型:int, float, double, QString, QByteArray, QUrl,还有好多…

您还可以添加对自定义类型的支持,甚至可以更改正则表达式匹配。如果您想让我另写一篇博文解释这一点,请在评论中告诉我。

您可能会问:“如何捕获多个参数?”例如,我想展示所有在2019年2月发布的博文:

1
2
3
4
QHttpServer server;
server.route( "/blog/<arg>/<arg>" , [] ( int year, int month) {
     return blogs_by_year_and_month(year, month);
});

您可能已经发现了关键字<arg>。它的工作原理与QString::Arg类似,但不支持排序。

这就是为什么我们使用的参数与QSting::arg中的不一样。

除此以外,我们还希望为您提供创建RESTAPI的可能性。为此,我们需要拆分GET/POST/PUT/DELETE请求。

一个小例子:

1
2
3
server.route( "/blog/" , QHttpServerRequest::Method::Get, [] ( int year) {
     return blogs_by_year(year);
});

或者

1
2
3
4
5
6
server.route( "/blog/" , [] ( int year, const QHttpServerRequest &req) {
     if (req.method() == QHttpServerRequest::Method::Get)
         return blogs_by_year(year);
     return QHttpServerResponder::StatusCode::NotFound;
});

这两种方法都很好,但我更喜欢第一种 —— 它更短一些。

 

默认情况下,QHttpServer::router只适用于转自URLPath。如果想要创建特殊的查询规则,您可以继承QHttpServerRouterRule并将其作为模板参数传递给QHttpServer::router.

如果希望自定义HTTP响应的头,则可以使用底层API,QHttpServerResponder.

1
2
3
4
QHttpServer server;
server.route( "/blog/" , [] ( int year, QHttpServerResponder &&responder) {
     responder.write(blogs_by_year(year), "text/plain" );
});

备注:QHttpServerResponderQHttpServerRequest是特殊参数,只能用作回调函数的最后一个参数。

这里有一个简单的Web服务器,您可以很容易地扩展它。如果您对这个项目感兴趣,别忘了下载它。

如果您对如何进一步改进它的建议或想法,您可以考虑在QTBUG-60105上提交您的反馈。

谢谢您的阅读!