Skip to main content

QtHttpServer路由API

Comments

本文翻译自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上提交您的反馈。

谢谢您的阅读!

 

Comments

Subscribe to our blog

Try Qt 6.11 Now!

Download the latest release here: www.qt.io/download

Qt 6.11 is now available, with new features and improvements for application developers and device creators.

We're Hiring

Check out all our open positions here and follow us on Instagram to see what it's like to be #QtPeople.