pistace是一个用现代C语言编写的web框架,它关注性能,并提供一个优雅的异步API。
#include <pistache/pistache.h>
安装
需要Git来检索源。编译源代码将需要CMake生成构建文件和支持c++ 17的最新编译器。
如果你在Ubuntu上,想要跳过编译过程,你可以添加官方的PPA提供的nightly 构建:
sudo add-apt-repository ppa:pistache+team/unstable
sudo apt update
sudo apt install libpistache-dev
否则,使用以下方法构建和安装最新版本:
git clone https://github.com/pistacheio/pistache.git
cd pistache
meson setup build
meson install -C build
或者:
cd pistache-master
mkdir build
cd build
cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release ..
make
sudo make install
另外,Pistache还不支持Windows,但应该可以在WSL下正常工作。
入门
头文件
#include <pistache/endpoint.h>
hello world
Pistache收到的请求用Http::Handler
来处理。
让我们从定义一个简单的HelloHandler开始:
using namespace Pistache;
class HelloHandler : public Http::Handler {
public:
HTTP_PROTOTYPE(HelloHandler)
void onRequest(const Http::Request& request, Http::ResponseWriter response) {
response.send(Http::Code::Ok, "Hello, World\n");
}
};
处理程序必须继承Http::Handler类,并至少定义onRequest成员函数。它们还必须定义一个clone()成员函数。简单的处理程序可以使用特殊的HTTP_PROTOTYPE宏,传入类的名称。该宏将负责为您定义clone()成员函数。
最后
定义处理程序后,现在可以启动服务器:
int main() {
Address addr(Ipv4::any(), Port(9080));
auto opts = Http::Endpoint::options().threads(1);
Http::Endpoint server(addr);
server.init(opts);
server.setHandler(Http::make_handler<HelloHandler>());
server.serve();
}
为了简单起见,你也可以使用特殊的listenAndServe
函数,它会自动创建一个端点并实例化你的处理程序:
int main() {
Http::listenAndServe<HelloHandler>("*:9080");
}
就这样,现在你可以启动你最喜欢的curl请求,并观察最终结果:
curl http://localhost:9080/
Hello, World
这个例子的完整代码可以在GitHub上找到: examples/hello_server.cc
HTTP处理程序
Pistache接收到的请求由一个名为Http::Handler
的特殊类处理。该类声明了一组虚拟方法,可以重写这些方法来处理套接字/&连接上发生的特殊事件。
onRequest()
函数必须被重写。每当Pistache接收到数据并将其作为HTTP请求正确解析时,就会调用此函数。
virtual void onRequest(const Http::Request& request, Http::ResponseWriter response);
第一个参数是Http::Request类型的对象,表示请求本身。它包含一系列信息,包括:
- 与请求相关联的资源
- 查询参数
- headers
- 请求的主体
Request对象提供了对这些信息的只读访问。您可以通过几个getter来访问它们,但不能修改它们。HTTP请求是不可变的。
发送响应
ResponseWriter
是一个对象,最终的HTTP响应将从该对象发送到客户端。onRequest()
函数不返回任何东西(void)。相反,响应通过ResponseWriter
类发送。这个类提供了一组send()
函数重载来发送响应。
Async::Promise<ssize_t> send(Code code);
你可以使用这个重载发送一个空的响应体和一个给定的HTTP代码(例如HTTP::Code::Ok)
Async::Promise<ssize_t> send(
Code code,
const std::string& body,
const Mime::MediaType &mime = Mime::MediaType()
);
此重载可用于发送带有静态、固定大小内容(正文)的响应。还可以指定MIME类型,它将通过Content-Type头发送。
template<size_t N>
Async::Promise<ssize_t> send(
Code code,
const char (&arr)[N],
const Mime::MediaType& mime = Mime::MediaType()
);
除了不需要构造字符串(不分配内存)之外,这个版本还可以用于发送带有正文的固定大小的响应。内容的大小由编译器直接推导。这个版本只适用于原始字符串。
这些函数是异步的,这意味着它们不会返回一个简单的旧ssize_t值,指示正在发送的字节数,而是返回一个稍后将实现的Promise。有关使用Pistache进行异步编程的更多细节,请参见下一节。
响应流
有时,要返回给用户的内容不能预先知道,因此不能预先确定长度。因此,HTTP规范定义了一种叫做分块编码的特殊数据传输机制,其中数据以一系列的形式发送。这种机制使用Transfer-Encoding HTTP头来代替Content-Length头。
对于流式内容,Pistache提供了一个特殊的ResponseStream类。要从ResponseWriter获取ResponseStream,可以调用stream()成员函数:
auto stream = response.stream(Http::Code::Ok);
要初始化流,必须将HTTP状态码传递给流函数(这里是HTTP:: code::Ok或HTTP 200)。ResponseStream类提供了一个类似于iostream的接口,用于重载<<操作符。
stream << "PO"
stream << "NG"
第一行将使用内容PO将大小为2的块写入流的缓冲区。第二行将使用内容NG编写大小为2的第二个块。要结束流并刷新内容,请使用特殊的ends标记:
stream << ends
end标记将写入大小为0的最后一块,并通过网络发送最终数据。要简单地刷新流缓冲区而不结束流,可以使用flush
标记:
stream << flush
headers写入:在启用流之后,头文件变得不可变。它们必须在创建ResponseStream之前写入响应:
response.headers() .add<Header::Server>("lys") .add<Header::ContentType>(MIME(Text, Plain)); auto stream = response.stream(); stream << "PO" << "NG" << ends;
静态文件服务
除了提供文本内容服务外,Pistache还通过Http::serveFile函数提供了一种提供静态文件的方法:
if (request.resource() == "/doc" && request.method == Http::Method::Get) {
Http::serveFile(response, "README.md");
}
返回值:serveFile还返回一个Promise,表示发送到网络的总字节数
控制超时
有时,您可能需要在一定时间之后超时。比如,如果您在设计一个带有软实时约束的HTTP API,那么您将有一个向客户机发送响应的时间约束。这就是为什么Pistache提供了根据每个请求控制超时的能力。要在响应上设置超时,你可以直接在ResponseWriter对象上使用timeouttaafter()
成员函数:
response.timeoutAfter(std::chrono::milliseconds(500));
如果在500毫秒内没有发送响应,这将触发超时。timeoutAfter接受任何类型的持续时间。
当一个超时触发时,处理程序中的onTimeout()函数将被调用。默认情况下,此方法不执行任何操作。如果你想正确处理超时,你应该在你自己的处理程序中重写这个函数:
void onTimeout(const Http::Request& request, Http::ResponseWriter writer) {
request.send(Http::Code::No_Content);
}
传递给onTimeout的Request对象与触发超时的请求完全相同。ResponseWriter是一个全新的写入器对象。
响应写入程序状态:由于ResponseWriter对象是一个完整的新对象,状态不会保留从onRequest()回调的ResponseWriter,这意味着您将不得不再次写入完整的响应,包括头和cookie。
异步HTTP编程
Pistache
提供的接口是异步和非阻塞的。异步编程允许代码继续执行,即使给定调用的结果还不可用。提供异步接口的调用被引用为异步调用。
这种调用的一个例子是ResponseWriter接口提供的send()函数。此函数返回写入与连接关联的套接字文件描述符的字节数。但是,它并没有直接将值返回给调用者从而阻止调用者,而是将值包装到一个称为Promise的组件中。
Promises
是Pistache对 Promises/A+ 标准的实现,在很多JavaScript实现中都可用。简单地说,在异步调用期间,Promises将异步操作的启动和结果的检索分开。当异步可能仍在运行时,一个Promise会直接返回给调用者,以便在最终结果可用时检索它。可以将所谓的延续附加到Promise上,以便在结果可用时(当Promise被解析或实现时)执行回调。
auto res = response.send(Http::Code::Ok, "Hello World");
res.then(
[](ssize_t bytes) { std::cout << bytes << " bytes have been sent\n" },
Async::NoExcept
);
then()
成员用于向Promise附加回调。第一个参数是一个可调用对象,当成功解决Promise时将调用它。如果由于某种原因,在异步操作期间发生错误,则可以拒绝Promise,然后会失败。在本例中,将调用第二个可调用对象。Async::NoExcept是一个特殊的回调函数,如果Promise失败,它将调用std::terminate()。这相当于noexcept关键字。
其他泛型回调也可以在这种情况下使用:
- Async::IgnoreException将简单地忽略该异常并让程序继续
- Async::Throw将“重新抛出”异常,直到最终的promise调用链。这与throw关键字具有相同的效果,只是它适用于promise
- promise回调中的异常通过exception_ptr传播。promise也可以链接在一起,以创建一个完整的异步管道:
auto fetchOp = fetchDatabase();
fetchOp
.then(
[](const User& user) { return fetchUserInfo(user); },
Async::Throw)
.then(
[](const UserInfo& info) { std::cout << "User name = " << info.name << '\n'; },
[](exception_ptr ptr) { std::cerr << "An exception occured during user retrieval\n";}
);
如果fetchDatabase()失败并拒绝了promise,第5行将传播异常
HEAD
概述
受Rust生态系统和Hyper的启发,HTTP头被表示为类型安全的普通对象。不再将头文件表示为(key: string, value: value),而是将它们表示为普通对象,这大大降低了使用普通旧字符串编译器无法捕捉到的输入错误的风险。
相反,对象让编译器能够在编译时直接捕获错误,因为用户不能通过名称添加或请求头文件:它必须使用整个类型。类型在编译时强制执行,这有助于减少常见的输入错误。
对于Pistache,每个HTTP Header都是一个继承自HTTP::Header基类的类,并使用NAME()宏来定义Header的名称。HTTP请求或响应中的所有头的列表存储在一个内部std::unordered_map中,包装在Header::Collection
类中。单个标头可以通过整个标头类型获取或添加到该对象中:
auto headers = request.headers();
auto ct = headers.get<Http::Header::ContentType>();
当H为H::Header
(H继承自Header)时,get< T>将返回一个std::shared_ptr< T>。如果Header不存在,get< T>将抛出异常。tryGet< T>提供了一个非抛出的替代方法,它返回一个空指针。
内置的header:
Pistache提供的头文件位于Http::Header名称空间中
定义自己的Header
HTTP RFC (RFC2616)定义的公共报头已经实现并且可用。然而,一些api可能定义了Pistache中不存在的额外头文件。为了支持你自己的头类型,你可以定义和注册你自己的HTTP头,首先声明一个继承HTTP:: header类的类:
class XProtocolVersion : public Http::Header {
};
因为每个Header都有一个名称,所以必须使用NAME()宏正确地命名头文件:
class XProtocolVersion : public Http::Header {
NAME("X-Protocol-Version")
};
Http::Header基类提供了两个虚方法,你必须在自己的实现中重写它们:
void parse(const std::string& data);
这个函数用于解析字符串表示的头。或者,为了避免为字符串表示分配内存,可以使用原始版本:
void parseRaw(const char* str, size_t len);
str将直接指向原始HTTP流中的报头缓冲区。len参数是报头值的总长度。
void write(std::ostream& stream) const
当将响应写回客户端时,write函数用于将报头序列化到网络缓冲区中。
让我们将这些函数组合在一起来完成前面声明的头文件的实现:
class XProtocolVersion : public Http::Header {
public:
NAME("X-Protocol-Version")
XProtocolVersion()
: minor(-1)
, major(-1)
{ }
void parse(const std::string& data) {
auto pos = data.find('.');
if (pos != std::string::npos) {
minor = std::stoi(data.substr(0, pos));
major = std::stoi(data.substr(pos + 1));
}
}
void write(std::ostream& os) const {
os << minor << "." << major;
}
private:
int minor;
int major;
};
就是这样。现在我们要做的就是将头文件注册到注册表系统中:
Header::Registry::registerHeader<XProtocolVersion>();
您应该始终为您的头文件提供一个默认构造函数,以便注册表系统可以实例化它
现在,可以像header::Collection类中的任何其他头文件一样检索和添加XProtocolVersion。
UNKNOWN HEADERS:注册表系统不知道的头将作为原始字符串对存储在Collection类中。getRaw()可用于检索原始头文件:
auto myHeader = request.headers().getRaw("x-raw-header"); myHeader.name() // x-raw-header myHeader.value() // returns the value of the header as a string
MIME类型
MIME类型(或媒体类型)也是全类型的。例如,在HTTP请求或响应中使用此类类型来描述消息体中包含的数据(Content-Type头,…),并且由类型、子类型和可选的后缀和参数组成。
MIME类型由MIME::MediaType类表示,在MIME .h头文件中实现。MIME类型可以直接从字符串构造:
auto mime = Http::Mime::MediaType::fromString("application/json");
然而,为了强制类型安全,通用类型都表示为枚举:
Http::Mime::MediaType m1(Http::Mime::Type::Application, Http::Mime::Subtype::Json);
为了避免这样的键入痛苦,还提供了一个MIME宏:
auto m1 = MIME(Application, Json);
对于后缀mime,使用特殊的MIME3宏:
auto m1 = MIME3(Application, Json, Zip);
如果你喜欢打字,你也可以用长格式:
Http::Mime::MediaType m1(Http::Mime::Type::Application, Http::Mime::Subtype::Json, Http::Mime::Suffix::Zip);
toString()函数可用于获取给定MIME类型的字符串表示:
auto m1 = MIME(Text, Html);
m1.toString(); // text/html