跳到主要内容

Boost.Beast

介绍

Beast是header-only的C++库,通过使用Boost.Asio一致的异步模型提供底层HTTP/1,WebSocket和网络协议的术语和算法,可作为编写通用网络库的基础库。

该库设计用于:

  • 对称性:算法与角色无关;构建客户端和/或服务器。
  • 易于使用:Boost.Asio用户将立即了解如何使用Beast。
  • 灵活性:用户可以做出重要的决定,例如缓冲区或线程管理。
  • 性能:构建处理数千个或更多连接的应用程序。
  • 进一步抽象的基础。组件非常适合在其上进行构建。

该库不是客户端或服务器,但是可以用来构建这些东西。提供了许多示例,包括客户端和服务器,它们可以用作编写您自己的程序的起点。

动机

Beast允许用户使用HTTP/1和WebSocket创建自己的库,客户端和服务器。因为Beast会处理底层协议的详细信息,所以代码将更易于实现,理解和维护。 HTTP和WebSocket协议驱动了大多数万维网。每个Web浏览器都实现这些协议以加载网页并允许客户端程序(通常以JavaScript编写)进行交互通信。C++得益于这些协议的标准化实现。

要求

重要 : 该库供熟悉Boost.Asio的程序员使用。希望使用异步接口的用户应该已经知道如何使用回调或协程创建并发网络程序。

Beast要求:

  • C++11:对大多数语言功能的强大支持。
  • Boost:Beast仅适用于Boost,不适用于独立的Asio
  • OpenSSL:生成测试,示例以及使用TLS/Secure套接字是必需的。

源码只有头文件。除以下情况外,通常不需要在程序的链接步骤中添加其他库以使用Beast:

  • 使用通过调用boost::asio::spawn创建的协程时,需要将Boost.Coroutine库添加到程序中。
  • 使用boost::asio::ssl::stream时,您需要将OpenSSL库添加到程序中。

请访问Boost文档以获取有关如何为特定环境系统构建和链接Boost库的说明。

快速浏览

这些完整的程序旨在使读者快速印象深刻。 它们的源代码和构建脚本位于example目录中。

简单的HTTP客户端

使用HTTP向网站发出GET请求并打印响应:

//HttpSyncClient.cpp

#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <cstdlib>
#include <iostream>
#include <string>

namespace beast = boost::beast;
namespace http = beast::http;
using tcp = boost::asio::ip::tcp;

// Performs an HTTP GET and prints the response
int HttpSyncClient(int argc, char **argv) {
try {
auto const host = "www.baidu.com";
auto const port = "80";
auto const target = "/";
int version = argc == 5 && !std::strcmp("1.0", argv[4]) ? 10 : 11;

// The io_context is required for all I/O
boost::asio::io_context io_context;

// These objects perform our I/O
tcp::resolver resolver(io_context);
beast::tcp_stream stream(io_context);

// Look up the domain name
auto const results = resolver.resolve(host, port);

// Make the connection on the IP address we get from a lookup
stream.connect(results);

// Set up an HTTP GET request message
http::request<http::string_body> req{http::verb::get, target, version};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);

// Send the HTTP request to the remote host
http::write(stream, req);

// This buffer is used for reading and must be persisted
beast::flat_buffer buffer;

// Declare a container to hold the response
http::response<http::dynamic_body> res;

// Receive the HTTP response
http::read(stream, buffer, res);

// Write the message to standard out
std::cout << res << std::endl;

// Gracefully close the socket
beast::error_code ec;
stream.socket().shutdown(tcp::socket::shutdown_both, ec);

// not_connected happens sometimes
// so don't bother reporting it.
//
if (ec && ec != beast::errc::not_connected)
throw beast::system_error{ec};

// If we get here then the connection is closed gracefully
} catch (std::exception const &e) {
std::cerr << "Error: " << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

简单的WebSocket客户端

建立WebSocket连接,发送消息并接收回复:

//WebsocketSyncClient.cpp

#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <cstdlib>
#include <iostream>
#include <string>

namespace beast = boost::beast; // from <boost/beast.hpp>
namespace http = beast::http; // from <boost/beast/http.hpp>
namespace websocket = beast::websocket; // from <boost/beast/websocket.hpp>
namespace net = boost::asio; // from <boost/asio.hpp>
using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>

// Sends a WebSocket message and prints the response
int WebsocketSyncClient(int /*argc*/, char ** /*argv*/) {
try {
auto const host = "127.0.0.1";
auto const port = "8080";
auto const text = "Hello World!1111";

// The io_context is required for all I/O
net::io_context ioc;

// These objects perform our I/O
tcp::resolver resolver{ioc};
websocket::stream<tcp::socket> ws{ioc};

// Look up the domain name
auto const results = resolver.resolve(host, port);

// Make the connection on the IP address we get from a lookup
net::connect(ws.next_layer(), results.begin(), results.end());

// Set a decorator to change the User-Agent of the handshake
ws.set_option(websocket::stream_base::decorator([](websocket::request_type &req) {
req.set(http::field::user_agent,
std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro");
}));

// Perform the websocket handshake
ws.handshake(host, "/");

// Send the message
ws.write(net::buffer(std::string(text)));

// This buffer will hold the incoming message
beast::flat_buffer buffer;

// Read a message into our buffer
ws.read(buffer);

// Close the WebSocket connection
ws.close(websocket::close_code::normal);

// If we get here then the connection is closed gracefully

// The make_printable() function helps print a ConstBufferSequence
std::cout << beast::make_printable(buffer.data()) << std::endl;
} catch (std::exception const &e) {
std::cerr << "Error: " << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

安全审查(Bishop Fox)

自2005年以来,毕晓普·福克斯(Bishop Fox)为全球财富1000强,高科技初创企业和金融机构提供了安全咨询服务。 Beast与Bishop Fox合作,评估了Boost C++ Beast HTTP/S网络库的安全性。以下报告详细介绍了从2017年9月11日开始的参与过程中发现的发现。

评估团队对Beast库进行了混合应用评估。 Bishop Fox的混合应用程序评估方法利用了应用程序渗透测试的实际攻击技术,结合有针对性的源代码审查,以彻底识别应用程序安全漏洞。这些全面知识评估始于对已部署的应用程序和源代码的自动扫描。接下来,将扫描结果的分析与手动检查结合起来,以彻底识别潜在的应用程序安全漏洞。此外,该团队还对应用程序体系结构和业务逻辑进行了审查,以查找所有设计级别的问题。最后,团队对这些问题进行手动开发和审查,以验证结果。

WebSocket(Autobahn|测试套件)

Autobahn WebSockets Testsuite提供了一个全自动测试套件,可以验证WebSocket协议的客户端和服务器实现,以确保规范的一致性和实现的鲁棒性。 该测试套件将通过进行基本的WebSocket对话,广泛的协议符合性验证以及性能和限制测试来检查实现。 Autobahn|Testsuite在整个行业中使用,包含500多个测试用例。

Autobahn|Testsuite WebSocket Results

示例

客户端

这些HTTP客户端将GET请求提交到命令行上指定的服务器,并打印结果响应。 客户端异步爬取10,000个顶级域的文档根,这可用于评估健壮性。 所有异步客户端均支持超时。

服务端

服务端(高级)

聊天服务器

网络

刷新器

为了有效地使用Beast,需要对网络有事先的了解。本节将对这些概念进行回顾,以提醒您并进一步学习。

通过建立连接,网络允许位于任何地方的程序在加入通信后交换信息。数据可以双向(全双工)可靠地通过连接传输,字节的发送顺序与接收顺序相同。这些连接以及用于表示它们的对象和类型统称为流。连接到网络的计算机或设备称为主机,建立的连接另一端的程序称为peer。

互联网是由相互连接的计算机组成的全球网络,这些计算机使用各种标准化的通信协议来交换信息。最受欢迎的协议是TCP/IP,该库仅依赖该协议。该协议负责处理底层细节,因此应用程序可以看到流(stream),这是一种可靠的全双工连接,带有上述字节的有序集合。服务器是功能强大且始终在线的主机,位于提供数据服务的知名网络名称或网络地址上。客户端是连接到服务器以交换数据然后下线的peer端。

供应商提供了一个称为设备驱动程序的程序,该程序使网络硬件(例如以太网适配器)能够与操作系统进行通讯。这又允许运行的程序使用各种接口(例如Berkeley套接字或Windows Sockets 2(Winsock)与网络交互。

Boost.AsioAsioNetworking TS为代表的C++网络库提供了一层抽象层,可与操作系统设备进行可移植的交互,不仅用于网络,而且用于常规输入/输出(I/O)。

缓冲区

缓冲区保存执行I/O时使用的连续字节序列。net::const_buffernet::mutable_buffer类型将这些内存区域表示为类型安全的pair<指针,大小>

net::const_buffer cb("Hello, world!", 13);
assert(string_view(reinterpret_cast<char const*>(
cb.data()), cb.size()) == "Hello, world!");

char storage[13];
net::mutable_buffer mb(storage, sizeof(storage));
std::memcpy(mb.data(), cb.data(), mb.size());
assert(string_view(reinterpret_cast<char const*>(
mb.data()), mb.size()) == "Hello, world!");

const_buffermutable_buffer优于std::span<byte>span<byte const>,因为std::span的功能太多。 它不仅对原始指针进行类型擦除,而且将其转化为指向字节的指针。 操作系统并不关心这一点,但是如果用户想要发送和接收其他类型的数组,则不需要将其显示为支持位操作的字节数组。 自定义缓冲区类型还使实现能够提供目标功能,例如缓冲区调试,而无需更改更通用的词汇表类型。

ConstBufferSequenceMutableBufferSequence概念描述了双向范围,其值类型分别可转换为const_buffermutable_buffer。 这些序列允许在单个函数调用中与多个缓冲区进行事务处理,这种技术称为分散/聚集 I/O。 缓冲区和缓冲区序列是非所有者的; 副本产生浅引用,而不是基础内存的副本。 这些语句中的每一个都声明一个缓冲区序列:

net::const_buffer b1;                   // a ConstBufferSequence by definition
net::mutable_buffer b2; // a MutableBufferSequence by definition
std::array<net::const_buffer, 3> b3; // A ConstBufferSequence by named requirements

函数net::buffer_sizenet::buffer_copy确定缓冲区序列中的字节总数,并将部分或全部字节分别从一个缓冲区序列传输到另一缓冲区序列。 函数buffer_size是一个自定义点:外部命名空间中用户定义的重载是可能的,并且调用者应在没有命名空间限定的情况下调用buffer_size。 函数net::buffer_sequence_beginnet::buffer_sequence_end用于获得一对遍历序列的迭代器。 Beast提供了一组缓冲区序列类型和算法,例如buffers_catbuffers_frontbuffers_prefixbuffers_rangebuffers_suffix。 本示例以字符串形式返回缓冲区序列中的字节:

template <class ConstBufferSequence>
std::string string_from_buffers (ConstBufferSequence const& buffers) {
// check that the type meets the requirements using the provided type traits
static_assert(
net::is_const_buffer_sequence<ConstBufferSequence>::value,
"ConstBufferSequence type requirements not met");

// optimization: reserve all the space for the string first
std::string result;
result.reserve(beast::buffer_bytes(buffers));// beast version of net::buffer_size

// iterate over each buffer in the sequence and append it to the string
// returns an iterator to beginning of the sequence
for(auto it = net::buffer_sequence_begin(buffers);
// returns a past-the-end iterator to the sequence
it != net::buffer_sequence_end(buffers);) {
// A buffer sequence iterator's value_type is always
// convertible to net::const_buffer
net::const_buffer buffer = *it++;

// A cast is always required to out-out of type-safety
result.append(static_cast<char const*>(buffer.data()), buffer.size());
}
return result;
}

DynamicBuffer概念定义了可调整大小的缓冲区序列接口。 当不知道内存大小时(例如,从流中读取HTTP消息时),可以用动态缓冲区来表示算法。 Beast提供了全面的动态缓冲区类型集合,例如buffers_adaptorflat_buffermulti_bufferstatic_buffer。 以下函数使用net::buffers_iterator将缓冲区的内容视为一个字符范围,将数据从tcp_stream读取到动态缓冲区中,直到遇到换行符为止:

// Read a line ending in '\n' from a socket, returning
// the number of characters up to but not including the newline
template <class DynamicBuffer>
std::size_t read_line(net::ip::tcp::socket& sock, DynamicBuffer& buffer) {
// this alias keeps things readable
using range = net::buffers_iterator<
typename DynamicBuffer::const_buffers_type>;

for(;;) {
// get iterators representing the range of characters in the buffer
auto begin = range::begin(buffer.data());
auto end = range::end(buffer.data());

// search for "\n" and return if found
auto pos = std::find(begin, end, '\n');
if(pos != range::end(buffer.data()))
return std::distance(begin, end);

// Determine the number of bytes to read,
// using available capacity in the buffer first.
std::size_t bytes_to_read = std::min<std::size_t>(
std::max<std::size_t>(512, // under 512 is too little,
buffer.capacity() - buffer.size()),
std::min<std::size_t>(65536, // and over 65536 is too much.
buffer.max_size() - buffer.size()));

// Read up to bytes_to_read bytes into the dynamic buffer
buffer.commit(sock.read_some(buffer.prepare(bytes_to_read)));
}
}

同步I/O

同步I/O是通过阻塞函数调用来完成的,这些函数返回操作结果。 这些操作通常无法取消,并且没有设置超时的方法。 SyncReadStreamSyncWriteStream概念定义了同步流的要求:一种可移植的I/O抽象,它使用缓冲区序列表示字节并使用error_code或异常报告任何故障来传输数据。 net::basic_stream_socket是通常用于形成TCP/IP连接的同步流。 可以满足要求的用户定义类型:

// Meets the requirements of SyncReadStream
struct sync_read_stream {
// Returns the number of bytes read upon success, otherwise throws an exception
template <class MutableBufferSequence>
std::size_t read_some(MutableBufferSequence const& buffers);

// Returns the number of bytes read successfully, sets the error code
// if a failure occurs
template <class MutableBufferSequence>
std::size_t read_some(MutableBufferSequence const& buffers, error_code& ec);
};

// Meets the requirements of SyncWriteStream
struct sync_write_stream {
// Returns the number of bytes written upon success,
// otherwise throws an exception
template <class ConstBufferSequence>
std::size_t write_some(ConstBufferSequence const& buffers);

// Returns the number of bytes written successfully, sets the error code
// if a failure occurs
template <class ConstBufferSequence>
std::size_t write_some(ConstBufferSequence const& buffers, error_code& ec);
};

同步流算法被编写为函数模板,该函数接受满足同步读取,写入或两者的命名要求的流对象。 此示例显示了一种算法,该算法可以编写文本并使用异常来指示错误:

template <class SyncWriteStream>
void hello (SyncWriteStream& stream) {
net::const_buffer cb("Hello, world!", 13);
do {
auto bytes_transferred = stream.write_some(cb); // may throw
cb += bytes_transferred; // adjust the pointer and size
}
while (cb.size() > 0);
}

可以使用错误代码代替异常来表示相同的算法:

template <class SyncWriteStream>
void hello (SyncWriteStream& stream, error_code& ec) {
net::const_buffer cb("Hello, world!", 13);
do {
auto bytes_transferred = stream.write_some(cb, ec);
cb += bytes_transferred; // adjust the pointer and size
}
while (cb.size() > 0 && ! ec);
}

异步I/O

异步操作从对initiating(启动)函数的调用开始,该调用启动该函数并立即返回到调用方。 这种出色的异步操作可以同时进行,而不会阻塞调用方。 当观察者一方建立完成后时,将在启动函数调用中提供的称为完成处理程序(completion handler)的可移动函数对象进入队列,以便与结果一起执行,该结果可能包括错误代码和其他特定信息。 在完成处理程序队列后,据说异步操作已完成。 以下代码显示了如何将某些文本异步写入套接字,并在操作完成后调用lambda:

// initiate an asynchronous write operation
net::async_write(sock, net::const_buffer("Hello, world!", 13),
[](error_code ec, std::size_t bytes_transferred) {
// this lambda is invoked when the write operation completes
if(! ec)
assert(bytes_transferred == 13);
else
std::cerr << "Error: " << ec.message() << "\n";
});
// meanwhile, the operation is outstanding and execution continues from here

每个完成处理程序也称为 continuation(延续),都具有net::get_associated_allocator返回的关联分配器和net::get_associated_executor返回的关联执行器。 这些关联可能被侵入地指定:

// The following is a completion handler expressed
// as a function object, with a nested associated
// allocator and a nested associated executor.
struct handler {
using allocator_type = std::allocator<char>;
allocator_type get_allocator() const noexcept;

using executor_type = boost::asio::io_context::executor_type;
executor_type get_executor() const noexcept;

void operator()(boost::beast::error_code, std::size_t);
};

或者可以通过专门化类模板net::associated_allocatornet::associated_executor来非侵入地指定这些关联:

namespace boost {
namespace asio {

template<class Allocator>
struct associated_allocator<handler, Allocator> {
using type = std::allocator<void>;

static
type
get(handler const& h,
Allocator const& alloc = Allocator{}) noexcept;
};

template<class Executor>
struct associated_executor<handler, Executor> {
using type = boost::asio::executor;

static
type
get(handler const& h,
Executor const& ex = Executor{}) noexcept;
};

} // boost
} // asio

当调用者想要更改完成处理程序的执行程序时,可以使用函数net::bind_executor

该实现使用分配器来获取执行该操作所需的任何临时存储。在调用完成处理程序之前,总是会释放临时分配。执行程序是可复制耗时成本低的对象,它提供用于调用完成处理程序的算法。除非由调用者定制,否则完成处理程序默认使用std::allocator<void>和相应I/O对象的执行程序。

网络规定了确定处理程序运行环境的工具。每个I/O对象都引用一个ExecutionContext,以获取用于调用完成处理程序的Executor实例。执行程序确定在何处以及如何调用完成处理程序。从net::io_context实例获得的执行程序提供了基本保证:处理程序将仅从当前正在调用net::io_context::run的线程中调用。

AsyncReadStreamAsyncWriteStream概念定义了异步流的要求:一种可移植的I/O抽象,使用缓冲区序列表示字节并使用error_code异步报告数据以报告任何故障。异步流算法被编写为模板化启动函数模板,该模板接受满足异步读取,写入或两者的命名要求的流对象。此示例显示了一种将一些文本写入异步流的算法:

template <class AsyncWriteStream, class WriteHandler>
void async_hello (AsyncWriteStream& stream, WriteHandler&& handler) {
net::async_write (stream,
net::buffer("Hello, world!", 13),
std::forward<WriteHandler>(handler));
}

并发

I/O对象(例如套接字和流)不是线程安全的。尽管可能有多个未完成的操作(例如,同时异步读取和异步写入),但流对象本身一次只能从一个线程访问。这意味着不得同时调用成员函数,例如移动构造函数,析构函数或启动函数。通常,这是通过同步原语(例如互斥锁)来完成的,但是并发网络程序需要一种更好的方式来访问共享资源,因为获取互斥锁的所有权可能会阻止线程执行无人参与的工作。为了提高效率,网络采用了一种在不需要显式锁定的情况下使用线程的模型,因为该模型要求所有对I/O对象的访问都必须在一个strand中执行。

通用模型

因为完成处理程序导致控制流的反转,所以有时需要其他附加延续的方法。网络为异步操作提供了通用模型,它提供了一种可自定义的方式来转换启动函数的签名,以使用其他类型的对象和方法来代替完成处理程序回调。例如,使用std::future接收异步传输的字符串,从而接收到传输的字节数,如下所示:

std::future<std::size_t> f = net::async_write(sock,
net::const_buffer("Hello, world!", 13), net::use_future);

通过传递变量net::use_future(类型为net::use_future_t<>)来代替完成处理程序,可以启用此功能。 相同的async_write函数重载可以与使用asio :: spawn启动的协程一起使用:

asio::spawn(
[&sock](net::yield_context yield)
{
std::size_t bytes_transferred = net::async_write(sock,
net::const_buffer("Hello, world!", 13), yield);
(void)bytes_transferred;
});

在这两种情况下,都使用具有特定类型的对象代替CompletionHandler,并且将初始化函数的返回值从void转换为std::future<std::size_t>std::size_t。 在此上下文中使用时,该处理程序有时称为CompletionToken。 返回类型转换由启动函数签名中的定制点支持。 这是net::async_write的签名:

return net::async_initiate<
CompletionToken,
void(error_code, std::size_t)>(
run_async_write{}, // The "initiation" object.
token, // Token must come before other arguments.
&stream, // Additional captured arguments are
buffers); // forwarded to the initiation object.

这个经过转换的内部处理程序负责完成步骤,该步骤将操作结果传递给调用方。 例如,当使用net::use_future时,内部处理程序将通过在启动函数返回的promise对象上调用std::promise::set_value来传递结果。

使用Networking

大多数库流算法需要一个tcp::socketnet::ssl::stream或其他已经与远程对等方建立通信的Stream对象。 提供此示例是为了提醒您如何使用套接字:

// The resolver is used to look up IP addresses and port numbers from a domain and service name pair
tcp::resolver r{ioc};

// A socket represents the local end of a connection between two peers
tcp::socket stream{ioc};

// Establish a connection before sending and receiving data
net::connect(stream, r.resolve("www.example.com", "http"));

// At this point `stream` is a connected to a remote
// host and may be used to perform stream operations.

在本文档中,具有以下名称的标识符具有特殊含义:

  • ioc

    net::io_context类型的变量,它在一个单独的线程上运行,并在其上构造了net::executor_work_guard对象。

  • sock

    类型为tcp::socket的变量,该变量已经连接到远程主机。

  • ssl_sock

    net::ssl::stream<tcp::socket>类型的变量,该变量已经连接并且已与远程主机握手。

  • ws

    类型为websocket::stream<tcp::socket>的变量,该变量已与远程主机连接。

Stream是一种通信通道,在该通道中,数据作为字节的有序序列被可靠地传输。 流可以是同步的也可以是异步的,并且可以允许读,写或两者兼而有之。 请注意,特定类型可以为多个概念建模。 例如,网络类型tcp::socketnet::ssl::stream支持SyncStream和AsyncStream。 使用以下概念将Beast中的所有流算法声明为模板函数:

  • SyncReadStream

    支持面向缓冲区的阻塞读取。

  • SyncWriteStream

    支持面向缓冲区的阻塞写入。

  • SyncStream

    流支持面向缓冲区的阻塞读取和写入。

  • AsyncReadStream

    支持面向缓冲区的异步读取。

  • AsyncWriteStream

    支持面向缓冲区的异步写入。

  • AsyncStream

    流支持面向缓冲区的异步读写。

这些模板元函数检查给定类型是否满足各种流概念的要求以及一些其他有用的实用程序。 该库在内部使用这些类型检查,还提供它们作为公共接口,因此用户可以使用相同的技术来扩展自己的代码。 这些类型检查的使用有助于在编译期间提供更简洁的错误:

  • executor_type

    get_executor返回的对象类型的别名。

  • has_get_executor

    确定get_executor成员函数是否存在。

  • is_async_read_stream

    确定类型是否满足AsyncReadStream的要求。

  • is_async_stream

    确定类型是否同时满足AsyncReadStream和AsyncWriteStream的要求。

  • is_async_write_stream

    确定类型是否满足AsyncWriteStream的要求。

  • is_sync_read_stream

    确定类型是否符合SyncReadStream的要求。

  • is_sync_stream

    确定类型是否同时满足SyncReadStream和SyncWriteStream的要求。

  • is_sync_write_stream

    确定类型是否符合SyncWriteStream的要求。

对函数或类模板类型使用带有static_assert的类型检查,将为用户提供有用的错误消息并防止未定义的行为。 此示例说明了写入同步流的模板函数如何检查其参数:

template<class SyncWriteStream>
void write_string(SyncWriteStream& stream, string_view s) {
static_assert(is_sync_write_stream<SyncWriteStream>::value,
"SyncWriteStream type requirements not met");
net::write(stream, net::const_buffer(s.data(), s.size()));
}

超时

限速

分层流

计数流

缓冲区类型

为了便于处理Boost.Asio中引入的ConstBufferSequence和MutableBufferSequence概念的实例,Beast将这些序列视为一种特殊的range。 提供了以下算法和包装器,可以使用惰性求值计算有效地转换这些range。 转换中不使用任何内存分配。 相反,它们在现有未修改的内存缓冲区上创建轻量级迭代器。 缓冲区的控制权由调用方保留; 所有权不转移。

  • buffer_bytes

    这是net::buffer_size的更可靠版本,它更易于使用,并且还适用于可转换为net::const_buffernet::mutable_buffer的类型。

  • buffers_cat

    此函数返回一个新的缓冲区序列,该缓冲区序列在进行迭代时会遍历如果所有输入缓冲区序列都串联在一起将形成的序列。使用此例程,可以将对流的write_some函数的多个调用组合为一个,从而消除了昂贵的系统调用。

  • buffers_cat_view

    此类表示通过串联两个或多个缓冲区序列形成的缓冲区序列。这是buffers_cat返回的对象的类型。

  • buffers_front

    此函数返回缓冲区序列中的第一个缓冲区,如果缓冲区序列中没有元素,则返回大小为零的缓冲区。

  • buffers_prefix

    该函数返回一个新的缓冲区或缓冲区序列,它代表原始缓冲区的前缀。

  • buffers_prefix_view

    此类表示由现有缓冲区序列的前缀形成的缓冲区序列。这是buffers_prefix返回的缓冲区的类型。

  • buffers_range buffers_range_ref

    此函数返回表示传递的缓冲区序列的可迭代范围。迭代范围时获得的值将始终是常量缓冲区,除非基础缓冲区序列是可变的,在这种情况下,迭代时获得的值将是可变缓冲区。在缓冲区序列上编写范围语句时,其目的是为了提供符号方便。

    函数buffers_range维护缓冲区序列的副本,而buffers_range_ref维护引用(在这种情况下,调用者必须确保所引用的缓冲区序列的生存期延长,直到范围对象被破坏为止)。

  • buffers_suffix

    此类包装现有缓冲区序列的基础内存,并提供原始序列的后缀。后缀的长度可以逐渐缩短。这使调用者可以按顺序递增缓冲区序列。

  • buffers_to_string

    此函数将缓冲区序列转换为std :: string。它可以用于诊断目的和测试。

Boost.Asio中引入的DynamicBuffer概念对支持自己可调整范围的缓冲区序列进行建模。 Beast提供了动态缓冲区概念的这套附加实现:

  • buffers_adaptor

    该包装器将任何MutableBufferSequence调整为DynamicBuffer,其输入和输出区域的总大小的上限等于基础可变缓冲区序列的大小。该实现不执行堆分配。

  • flat_buffer basic_flat_buffer

    确保输入和输出区域是长度为1的缓冲区序列。在构造时,可以设置输入和输出区域的总大小的可选上限。基本容器是AllocatorAwareContainer。

  • multi_buffer basic_multi_buffer

    使用一系列大小不同的一个或多个字符数组。其他字符数组对象将附加到序列中,以适应字符序列大小的变化。基本容器是AllocatorAwareContainer。

  • flat_static_buffer flat_static_buffer_base

    确保输入和输出区域是长度为1的缓冲区序列。提供动态缓冲区的功能,但要限制由constexpr模板参数定义的输入和输出区域的总大小的上限。序列的存储保存在类中;该实现不执行堆分配。

  • static_buffer static_buffer_base

    提供循环动态缓冲区的功能。取决于由constexpr模板参数定义的输入和输出区域的总大小的上限。该实现永远不会在缓冲区操作期间移动内存。序列的存储保存在类中;该实现不执行堆分配。

这两个函数有助于缓冲区与标准输出流的互操作性。

  • make_printable

    该函数包装了ConstBufferSequence,因此可以与operator <<std::ostream一起使用。

  • ostream

  • 此函数返回一个包装动态缓冲区的std::ostream。 使用operator<<发送到流的字符存储在动态缓冲区中。

提供这些类型特征以方便编写在缓冲区上运行的编译时元函数:

buffers_iterator_type

此元函数用于确定特定缓冲区序列使用的迭代器的类型。

buffers_type

此元功能用于确定缓冲区序列列表的基础缓冲区类型。 别名的等效类型将根据模板类型参数而有所不同。

is_const_buffer_sequence

此元函数用于确定所有指定类型是否都满足ConstBufferSequence的要求。 如果每个指定的类型都符合要求,则此类型别名将为std::true_type,否则,此类型别名将为std::false_type

is_mutable_buffer_sequence

此元函数用于确定所有指定的类型是否都满足MutableBufferSequence的要求。 如果每个指定的类型都符合要求,则此类型别名将为std::true_type,否则,此类型别名将为std::false_type

文件

编写组合操作

回显

检测SSL

HTTP

协议入门

HTTP协议定义了客户端和服务器角色:客户端发送请求,服务器发送回响应。客户端和服务器建立连接后,客户端将发送一系列请求,而服务器将为每个接收到的请求按接收顺序依次发送至少一个响应。

请求或响应是具有两个部分的HTTP消息(以下称为“消息”):具有结构化元数据的头部header和包含任意数据的可选可变长度body。序列化的header是一个或多个文本行,其中每行以回车符结尾,后跟换行符(“ \ r \ n”)。空行标记header的末尾。header中的第一行称为起始行。起始行内容的内容因请求和响应而有所不同。

每个消息都包含一组零个或多个字段名称/值对,统称为“字段”。名称和值使用具有各种要求的文本字符串表示。序列化的字段包含字段名称,然后是一个冒号,后跟一个空格(“:”),最后是带有尾随CRLF的字段值。

请求

客户端发送包含方法和请求目标以及HTTP版本的请求。该方法标识要执行的操作,而目标标识应用了该操作的服务器上的对象。该版本几乎始终为1.1,但较旧的程序有时使用1.0。

  • 序列化请求

    GET / HTTP/1.1\r\n
    User-Agent: Beast\r\n
    \r\n
  • 描述

    该请求的方法为“ GET”,目标为“ /”,并指示HTTP版本1.1。 它包含一个名为“ User-Agent”的字段,其值为“ Beast”。 没有消息body。

响应

服务器发送响应,其中包含状态码,原因短语(reason-phrase)和HTTP版本。 原因短语已过时:客户端应忽略原因短语内容。 这是一个包含身体的回应。 特殊的Content-Length字段会通知远程主机随后的body大小。

  • 序列化响应

    HTTP/1.1 200 OK\r\n
    Server: Beast\r\n
    Content-Length: 13\r\n
    \r\n
    Hello, world!
  • 描述

    该响应的状态码为200,表示请求的操作已成功完成。 过时的原因短语为“OK”。 它指定HTTP版本1.1,并包含一个大小为13个八位字节的主体,文本为“ Hello,world!”。

Body

消息可以选择带有正文。 消息正文的大小由消息的语义以及特殊字段Content-Length和Transfer-Encoding决定。 rfc7230第3.3节提供了有关如何确定体长的全面说明。

特殊字段

消息中出现的某些字段是特殊的。 该库在执行序列化和解析时会理解这些字段,在消息中解析这些字段时会根据需要采取自动操作,并在调用者请求时设置这些字段。

  • Connection / Proxy-Connection

    该字段允许发送方指示当前连接的所需控制选项。 常用值包括“close”,“keep-alive”和“upgrade”。

  • Content-Length

    如果存在,则此字段通知接受者有关紧随header的body的确切大小(以字节为单位)。

  • Transfer-Encoding

    此可选字段列出了已经(或将要)应用于内容有效负载以形成消息正文的传输编码序列的名称。

    Beast了解“块式”编码方案的最新情况(最外部)应用的编码。 当在序列化过程中提前未知内容长度时,库将自动应用分块编码,并且当存在时,库将自动从已解析的消息中删除分块编码。

  • Upgrade

    Upgrade header字段提供了一种从HTTP / 1.1过渡到同一连接上的另一个协议的机制。 例如,它是WebSocket的初始HTTP握手用来建立WebSocket连接的机制。

消息容器

Beast提供了一个类模板message和一些别名,它们模拟HTTP / 1和HTTP / 2消息:

	
/// An HTTP message
template<
bool isRequest, // `true` for requests, `false` for responses
class Body, // Controls the container and algorithms used for the body
class Fields = fields> // The type of container to store the fields
class message;

/// A typical HTTP request
template<class Body, class Fields = fields>
using request = message<true, Body, Fields>;


/// A typical HTTP response
template<class Body, class Fields = fields>
using response = message<false, Body, Fields>;

容器提供值语义,包括“正文”和“字段”支持的移动和复制。 用户定义的模板函数参数可以接受任何消息,也可以使用部分专业化来仅接受请求或响应。 默认字段是使用标准分配器提供的关联容器,并支持对字段的修改和检查。 根据rfc7230,字段名称使用不区分大小写的比较。 用户定义的字段类型是可能的。 主体类型确定用于表示主体的容器的类型,以及用于将缓冲区与容器进行传输的算法。 该库带有一组常见的正文类型。 与字段一样,用户定义的正文类型也是可能的。

有时希望仅使用标头。 Beast提供了一个类模板头和一些别名来对HTTP / 1和HTTP / 2头进行建模:

	/// An HTTP header
template<
bool isRequest, // `true` for requests, `false` for responses
class Fields = fields> // The type of container to store the fields
class header;

/// A typical HTTP request header
template<class Fields>
using request_header = header<true, Fields>;


/// A typical HTTP response header
template<class Fields>
using response_header = header<false, Fields>;

请求和响应共享version,fields和body,但具有一些类型唯一的成员。 通过将标头类声明为isRequest的进行特化来实现。message继承自header,可以将message作为参数传递给以适当类型的header作为参数的函数。 此外,header是从Fields公开派生的; 一条message继承了Fields的所有成员函数。 此图显示了header和message之间的继承关系,以及每个部分特化中成员的一些显着差异:

body类型

Beast定义了Body概念,它决定message::body成员的类型(如上图所示),并且还可能包括用于将缓冲区传入和传出的算法。 这些算法在解析和序列化过程中使用。 用户可以定义自己的body类型来满足要求,或使用库自带的Body类型:

  • buffer_body

    其value_type包含指向调用者提供的缓冲区的原始指针和大小。这允许对来自外部源的正文数据进行序列化,并使用固定大小的缓冲区对邮件正文内容进行增量解析。

  • dynamic_body / basic_dynamic_body

    一个其value_type为DynamicBuffer的body。它继承了动态缓冲区基础选择的插入复杂度。具有此主体类型的消息可能会被序列化和解析。

  • empty_body

    一个特殊的正文,其value_type为空,表示该消息没有正文。具有此主体的消息可能会被序列化和解析;但是,与此主体一起解析消息时收到的主体八位位组将产生唯一错误。

  • file_body / basic_file_body

    该body由打开的用于读取或写入的文件表示。具有此body的消息可能会被序列化和解析。 HTTP算法将使用打开的文件进行读写,流式传输和增量式发送和接收。

  • span_body

    一个其value_type为span的body,是对单个字节线性缓冲区的非所有者引用。具有此主体类型的消息可能会被序列化和解析。

  • string_body / basic_string_body

    一个其value_type为std::basic_stringstd::string的body。插入复杂度按固定时间摊销,而容量按几何增长。具有此主体类型的消息可能会被序列化和解析。这是示例中使用的主体类型。

  • vector_body

    一个其value_type为std :: vector的body。插入复杂度按固定时间摊销,而容量按几何增长。具有此主体类型的消息可能会被序列化和解析。

用法

这些示例说明如何创建和填写请求和响应对象:在这里,我们构建一个带有空消息正文的HTTP GET请求:

request<empty_body> req;
req.version(11); // HTTP/1.1
req.method(verb::get);
req.target("/index.htm");
req.set(field::accept, "text/html");
req.set(field::user_agent, "Beast");

/* 序列化结果
GET /index.htm HTTP/1.1\r\n
Accept: text/html\r\n
User-Agent: Beast\r\n
\r\n
*/

在此代码中,我们创建一个HTTP响应,其中包含指示成功的状态代码。 该消息的正文长度为非零。 函数message::prepare_payload会根据正文成员的内容和类型自动设置Content-Length或Transfer-Encoding字段。 此功能的使用是可选的。 这些字段也可以显式设置。

response<string_body> res;
res.version(11); // HTTP/1.1
res.result(status::ok);
res.set(field::server, "Beast");
res.body() = "Hello, world!";
res.prepare_payload();

/*
HTTP/1.1 200 OK\r\n
Server: Beast\r\n
Content-Length: 13\r\n
\r\n
Hello, world!
*/

序列化消息时,实现将从状态代码中自动填充过时的原因短语。 或者可以使用header::reason直接设置。

消息流操作

序列化器流操作

解析器流操作

核心算法需要做的不仅仅是一次接收完整的消息,例如:

  • 首先接收header,然后接收body。
  • 使用固定大小的缓冲区接收较大的body。
  • 逐渐收到一条消息:每个I / O周期的工作量有限。
  • 将承诺推迟到Body类型,直到读取标题为止。

这些类型的操作要求调用者通过构造从basic_parser派生的类来管理关联状态的生存期。 Beast附带了派生实例解析器,该解析器使用basic_fields Fields容器创建完整的消息对象。

/// An HTTP/1 parser for producing a message.
template<
bool isRequest, // `true` to parse an HTTP request
class Body, // The Body type for the resulting message
class Allocator = std::allocator<char>> // The type of allocator for the header
class parser
: public basic_parser<...>;


/// An HTTP/1 parser for producing a request message.
template<class Body, class Allocator = std::allocator<char>>
using request_parser = parser<true, Body, Allocator>;


/// An HTTP/1 parser for producing a response message.
template<class Body, class Allocator = std::allocator<char>>
using response_parser = parser<false, Body, Allocator>;

basic_parser和从其派生的类处理以rfc7230中描述的HTTP / 1格式序列化的八位字节流。

在解析器上工作的流操作是:

  • read

    从SyncWriteStream将所有内容读取到解析器中。

  • async_read

    从AsyncWriteStream异步将所有内容读取到解析器中。

  • read_header

    从SyncWriteStream仅将header按字节大小读取到解析器中。

  • async_read_header

    从AsyncWriteStream异步将header按字节大小读取到解析器中。

  • read_some

    从SyncReadStream将一些八位字节读入解析器。

  • async_read_some

    从AsyncWriteStream异步将一些八位位组读取到解析器中。

与消息流操作一样,解析器流操作需要持久化的DynamicBuffer来保存流中未使用的八位位组。 基本解析器实现针对此动态缓冲区将其输入序列存储在单个连续内存缓冲区中的情况进行了优化。 尽管为此目的,建议使用flat_buffer,flat_static_buffer或flat_static_buffer_base的实例,尽管用户定义的DynamicBuffer实例可以生成长度为一的输入序列,但也可以使用该实例。

解析器包含内部构造的消息。 传递给解析器的构造函数的参数将转发到消息容器中。 调用者可以通过调用parser :: get访问解析器内部的消息。 如果“字段”和“正文”类型为MoveConstructible,则调用方可以通过调用parser :: release获得消息的所有权。 在此示例中,我们使用解析器读取带有字符串正文的HTTP响应,然后打印响应:

template<class SyncReadStream>
void print_response(SyncReadStream& stream) {
static_assert(is_sync_read_stream<SyncReadStream>::value,
"SyncReadStream type requirements not met");

// Declare a parser for an HTTP response
response_parser<string_body> parser;

// Read the entire message
read(stream, parser);

// Now print the message
std::cout << parser.get() << std::endl;
}

增量阅读

面向缓冲区的序列化

写入std :: ostream

面向缓冲区的解析

从std :: istream读取

Chunked编码

对于提前不知道大小的消息有效负载,HTTP 1.1版定义了分块(chunked)的传输编码。 此编码由零个或多个分块主体(chunked bodies)组成,最后加上last chunk。 每个chunked body都可以包含可选的应用程序定义的,特定于连接的分块扩展(chunk-extensions)。 最后一个chunk可能在最后一个块的称为"chunk-trailer"的部分中包含其他HTTP字段值。 字段值在标头中“承诺”为“尾随”字段值中字段名称的逗号分隔列表。 客户端通过在TE字段值中包含“ trailers”令牌来表明他们愿意接受trailers。

序列化Chunks

当消息从message::chunked返回true时,serializer将自动应用分块传输编码。serializer发出的块之间的边界是实现定义的。 Chunk extensions和trailers省略。 需要对块边界,扩展和尾部进行精确控制的应用程序可以使用一组帮助程序类,这些帮助程序类可以使用块编码来手动释放消息有效负载。

要使用这些帮助类,请首先使用标准接口序列化消息的标头部分。 然后准备缓冲区,块扩展和所需的预告片,并将其与以下帮助程序一起使用:

我们首先通过声明一个函数来演示这些对象的用法,该函数返回下一个缓冲区序列以用作chunk body:

// This function returns the buffer containing the next chunk body
net::const_buffer get_next_chunk_body();

此示例演示了手动发送完整的chunked message有效负载。 不发出chunk extensions或trailers:

// Prepare an HTTP/1.1 response with a chunked body
response<empty_body> res{status::ok, 11};
res.set(field::server, "Beast");

// Set Transfer-Encoding to "chunked".
// If a Content-Length was present, it is removed.
res.chunked(true);

// Set up the serializer
response_serializer<empty_body> sr{res};

// Write the header first
write_header(sock, sr);

// Now manually emit three chunks:
net::write(sock, make_chunk(get_next_chunk_body()));
net::write(sock, make_chunk(get_next_chunk_body()));
net::write(sock, make_chunk(get_next_chunk_body()));

// We are responsible for sending the last chunk:
net::write(sock, make_chunk_last());

以下代码发送附加chunks,并使用帮助器容器设置chunk extensions。 必要时,容器会自动在序列化输出中为值加上引号:

// Prepare a set of chunk extension to emit with the body
chunk_extensions ext;
ext.insert("mp3");
ext.insert("title", "Beale Street Blues");
ext.insert("artist", "W.C. Handy");

// Write the next chunk with the chunk extensions
// The implementation will make a copy of the extensions object,
// so the caller does not need to manage lifetime issues.
net::write(sock, make_chunk(get_next_chunk_body(), ext));

// Write the next chunk with the chunk extensions
// The implementation will make a copy of the extensions object, storing the copy
// using the custom allocator, so the caller does not need to manage lifetime issues.
net::write(sock, make_chunk(get_next_chunk_body(), ext, std::allocator<char>{}));

// Write the next chunk with the chunk extensions
// The implementation allocates memory using the default allocator and takes ownership
// of the extensions object, so the caller does not need to manage lifetime issues.
// Note: ext is moved
net::write(sock, make_chunk(get_next_chunk_body(), std::move(ext)));

调用者可以通过传递non-owning字符串来接管扩展缓冲区的生成和管理。 请注意,这要求字符串内容遵循块扩展的正确语法,包括对包含空格的值所需要的双引号:

// Manually specify the chunk extensions.
// Some of the strings contain spaces and a period and must be quoted
net::write(sock, make_chunk(get_next_chunk_body(),
";mp3"
";title=\"Danny Boy\""
";artist=\"Fred E. Weatherly\""
));

下一个代码示例发出一个chunked响应,该响应承诺两个尾部字段并将它们传递到最后一个块中。 该实现使用默认或传入的分配器分配内存,以保存序列化尾部所需的状态信息:

// Prepare a chunked HTTP/1.1 response with some trailer fields
response<empty_body> res{status::ok, 11};
res.set(field::server, "Beast");

// Inform the client of the trailer fields we will send
res.set(field::trailer, "Content-MD5, Expires");

res.chunked(true);

// Serialize the header and two chunks
response_serializer<empty_body> sr{res};
write_header(sock, sr);
net::write(sock, make_chunk(get_next_chunk_body()));
net::write(sock, make_chunk(get_next_chunk_body()));

// Prepare the trailer
fields trailer;
trailer.set(field::content_md5, "f4a5c16584f03d90");
trailer.set(field::expires, "never");

// Emit the trailer in the last chunk.
// The implementation will use the default allocator to create the storage for holding
// the serialized fields.
net::write(sock, make_chunk_last(trailer));

使用自定义分配器序列化最后一个块:

// Use a custom allocator for serializing the last chunk
fields trailer;
trailer.set(field::approved, "yes");
net::write(sock, make_chunk_last(trailer, std::allocator<char>{}));

另外,调用者可以通过传递非所有者字符串来接管序列化预告片字段的生成和生命周期管理:

// Manually emit a trailer.
// We are responsible for ensuring that the trailer format adheres to the specification.
string_view ext =
"Content-MD5: f4a5c16584f03d90\r\n"
"Expires: never\r\n"
"\r\n";
net::write(sock, make_chunk_last(net::const_buffer{ext.data(), ext.size()}));

对于最终的控制级别,调用者可以通过首先发出具有正确块主体大小的标头,然后通过在多次调用流写入函数中发出块主体来手动组成块本身。 在这种情况下,调用方还负责发出终止CRLF(“ \ r \ n”):

// Prepare a chunked HTTP/1.1 response and send the header
response<empty_body> res{status::ok, 11};
res.set(field::server, "Beast");
res.chunked(true);
response_serializer<empty_body> sr{res};
write_header(sock, sr);

// Obtain three body buffers up front
auto const cb1 = get_next_chunk_body();
auto const cb2 = get_next_chunk_body();
auto const cb3 = get_next_chunk_body();

// Manually emit a chunk by first writing the chunk-size header with the correct size
net::write(sock, chunk_header{
buffer_bytes(cb1) +
buffer_bytes(cb2) +
buffer_bytes(cb3)});

// And then output the chunk body in three pieces ("chunk the chunk")
net::write(sock, cb1);
net::write(sock, cb2);
net::write(sock, cb3);

// When we go this deep, we are also responsible for the terminating CRLF
net::write(sock, chunk_crlf{});

解析Chunks

当它是列表中的最后一个编码时,解析器会自动删除分块的传输编码。 但是,它也丢弃了块扩展,并且没有提供确定块之间边界的方法。 需要访问块扩展或读取完整单个块的高级应用程序可以使用解析器提供的回调接口:

本示例将从流中读取消息头,然后手动读取每个块。 它识别块的边界,并在每个块进入时输出其内容。任何块扩展都将被打印,每个扩展都在其自己的行上。 最后,打印头中承诺的所有预告片。

/** Read a message with a chunked body and print the chunks and extensions
*/
template<
bool isRequest,
class SyncReadStream,
class DynamicBuffer>
void
print_chunked_body(
std::ostream& os,
SyncReadStream& stream,
DynamicBuffer& buffer,
error_code& ec)
{
// Declare the parser with an empty body since
// we plan on capturing the chunks ourselves.
parser<isRequest, empty_body> p;

// First read the complete header
read_header(stream, buffer, p, ec);
if(ec)
return;

// This container will hold the extensions for each chunk
chunk_extensions ce;

// This string will hold the body of each chunk
std::string chunk;

// Declare our chunk header callback This is invoked
// after each chunk header and also after the last chunk.
auto header_cb =
[&](std::uint64_t size, // Size of the chunk, or zero for the last chunk
string_view extensions, // The raw chunk-extensions string. Already validated.
error_code& ev) // We can set this to indicate an error
{
// Parse the chunk extensions so we can access them easily
ce.parse(extensions, ev);
if(ev)
return;

// See if the chunk is too big
if(size > (std::numeric_limits<std::size_t>::max)())
{
ev = error::body_limit;
return;
}

// Make sure we have enough storage, and
// reset the container for the upcoming chunk
chunk.reserve(static_cast<std::size_t>(size));
chunk.clear();
};

// Set the callback. The function requires a non-const reference so we
// use a local variable, since temporaries can only bind to const refs.
p.on_chunk_header(header_cb);

// Declare the chunk body callback. This is called one or
// more times for each piece of a chunk body.
auto body_cb =
[&](std::uint64_t remain, // The number of bytes left in this chunk
string_view body, // A buffer holding chunk body data
error_code& ec) // We can set this to indicate an error
{
// If this is the last piece of the chunk body,
// set the error so that the call to `read` returns
// and we can process the chunk.
if(remain == body.size())
ec = error::end_of_chunk;

// Append this piece to our container
chunk.append(body.data(), body.size());

// The return value informs the parser of how much of the body we
// consumed. We will indicate that we consumed everything passed in.
return body.size();
};
p.on_chunk_body(body_cb);

while(! p.is_done())
{
// Read as much as we can. When we reach the end of the chunk, the chunk
// body callback will make the read return with the end_of_chunk error.
read(stream, buffer, p, ec);
if(! ec)
continue;
else if(ec != error::end_of_chunk)
return;
else
ec = {};

// We got a whole chunk, print the extensions:
for(auto const& extension : ce)
{
os << "Extension: " << extension.first;
if(! extension.second.empty())
os << " = " << extension.second << std::endl;
else
os << std::endl;
}

// Now print the chunk body
os << "Chunk Body: " << chunk << std::endl;
}

// Get a reference to the parsed message, this is for convenience
auto const& msg = p.get();

// Check each field promised in the "Trailer" header and output it
for(auto const& name : token_list{msg[field::trailer]})
{
// Find the trailer field
auto it = msg.find(name);
if(it == msg.end())
{
// Oops! They promised the field but failed to deliver it
os << "Missing Trailer: " << name << std::endl;
continue;
}
os << it->name() << ": " << it->value() << std::endl;
}
}

给定HTTP响应作为输入的左侧,上面显示的函数的输出显示在右侧:

自定义Body类型

文件Body

自定义解析器

HTTP示例

更改Body类型

期望100-continue(客户端)

期望100继续(服务端)

HEAD请求(客户端)

HEAD响应(服务端)

HTTP应答

发送子进程输出

有时有必要发送一条消息,该消息的主体不能由单个容器方便地描述。 例如,在实现HTTP中继功能时,可靠的实现需要在主体缓冲区从下游主机可用时单独呈现主体缓冲区。 这些缓冲区的大小应固定,否则在将完整的邮件正文转发到上游主机之前会产生不必要的,效率低下的负担,无法读取完整的邮件正文。

为了启用这些用例,提供了主体类型buffer_body。 该主体使用呼叫者提供的指针和大小,而不是拥有的容器。 要使用此主体,请实例化序列化程序的实例,并在调用流写入函数之前填写指针和大小字段。

本示例从子进程中读取数据,并将输出发送回HTTP响应。 流程的输出在可用时发送:

WebSocket

WebSocket协议允许在受控环境中运行不受信任代码的客户端与已选择从该代码进行通信的远程主机之间进行双向通信。 该协议包括一个开放的握手,随后是基于TCP层的基本消息框架。 该技术的目标是为基于浏览器的应用程序提供一种机制,该应用程序需要与服务器进行双向通信,而无需依赖于打开多个HTTP连接。

Beast使用现代C++方法为开发人员提供了基于Boost.Asio的可靠WebSocket实现,并具有一致的异步模型。

本文档假定您熟悉Boost.Asio和rfc6455中描述的协议规范。 本节中出现的示例代码和标识符的编写方式需要确保以下这些声明有效:

#include <boost/beast.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
namespace net = boost::asio;
namespace beast = boost::beast;
using namespace boost::beast;
using namespace boost::beast::websocket;

net::io_context ioc;
tcp_stream sock(ioc);
net::ssl::context ctx(net::ssl::context::tlsv12);

构造

WebSocket连接需要一个有状态的对象,在Beast中用单个类模板websocket::stream表示。 接口使用分层流模型。 一个websocket stream对象包含另一个stream对象,称为next layer,它用于执行I/O。 每个模板参数的说明如下:

namespace boost {
namespace beast {
namespace websocket {

template<
class NextLayer,
bool deflateSupported = true>
class stream;

} // websocket
} // beast
} // boost
  • NextLayer next layer的类型。 这种类型的对象将在流的生命周期中构造和维护。 所有读取和写入将通过next layer。 此类型必须满足SyncStreamAsyncStream或者两者都条件满足,具体取决于要执行的I/O的样式。

  • deflateSupported 当此值为true时,流将支持(但不是必需)permessage-deflate扩展。 流在握手期间是否实际请求或接受扩展取决于单独的可配置选项。

    值为false时,扩展被禁用。 stream永远不会在客户端角色中请求扩展,也不会在服务器角色中接受扩展请求。 禁用扩展的另一个好处是编译会更快,并且生成的程序可执行文件将包含更少的代码。

构造stream时,提供给构造函数的所有参数都将转发到next layer对象的构造函数。 这使用I/O context通过普通的TCP/IP套接字声明一个stream:

// This newly constructed WebSocket stream will use the specified
// I/O context and have support for the permessage-deflate extension.

stream<tcp_stream> ws(ioc);

Websocket stream使用其自己的特定于协议的超时功能。 将Websocket stream与tcp_stream或basic_stream类模板一起使用时,应在建立连接后在TCP或basic_stream上禁用timeouts,否则该stream的行为不确定。

与大多数I/O对象一样,websocket stream也不是线程安全的。 如果两个不同的线程同时访问对象,则会导致未定义的行为。 对于多线程程序,tcp_stream可以由executor(在这种情况下为strand)构造。 下面声明的stream将使用a strand(一个链)来调用所有完成处理程序:

// Ownership of the `tcp_stream` is transferred to the websocket stream

stream<tcp_stream> ws(std::move(sock));

可以通过调用stream::next_layer访问next layer

// Calls `close` on the underlying `beast::tcp_stream`
ws.next_layer().close();

使用SSL

要通过SSL使用WebSocket,请使用net::ssl::stream类模板的实例作为stream的模板类型。 所需的net::io_contextnet::ssl::context参数转发到被包装的stream的构造函数:

// The WebSocket stream will use SSL and a new strand
stream<ssl_stream<tcp_stream>> wss(net::make_strand(ioc), ctx);

使用Asio SSL类型声明websocket stream对象的代码必须包含文件<boost/beast/websocket/ssl.hpp>

和以前一样,可以通过调用next_layer访问底层(underlying,下一层)的SSL stream

// Perform the SSL handshake in the client role
wss.next_layer().handshake(net::ssl::stream_base::client);

对于multi-layered streams(例如上面声明的stream),当对next_layer进行链式调用时,访问单个层(layer)可能很麻烦。 函数get_lowest_layer返回layered stream中layers堆栈中的最后一个stream。 在这里,我们访问最底层的stream以取消所有未完成的I/O

非阻塞模式

请注意,websocket stream不支持非阻塞模式。

连接

在可以交换消息之前,首先需要连接Websocket stream,然后执行Websocket handshake。 该stream将建立连接的任务委托给next layers。 例如,如果next layer是可连接的stream或socket对象,则可以对其进行访问以调用用于连接的必要函数。 在这里,我们像客户端一样建立出站(outbound)连接。

stream<tcp_stream> ws(ioc);
net::ip::tcp::resolver resolver(ioc);

// Connect the socket to the IP address returned from performing a name lookup
get_lowest_layer(ws).connect(resolver.resolve("example.com", "ws"));

要接受传入连接,需要使用使用acceptor。 可以在建立传入连接时从acceptor返回的socket构造websocket stream。

net::ip::tcp::acceptor acceptor(ioc);
acceptor.bind(net::ip::tcp::endpoint(net::ip::tcp::v4(), 0));
acceptor.listen();

// The socket returned by accept() will be forwarded to the tcp_stream,
// which uses it to perform a move-construction from the net::ip::tcp::socket.

stream<tcp_stream> ws(acceptor.accept());

或者,可以使用acceptor的重载成员函数将传入的连接直接传入赋值到websocket stream所属的socket。

// The stream will use the strand for invoking all completion handlers
stream<tcp_stream> ws(net::make_strand(ioc));

// This overload of accept uses the socket provided for the new connection.
// The function `tcp_stream::socket` provides access to the low-level socket
// object contained in the tcp_stream.

acceptor.accept(get_lowest_layer(ws).socket());

握手

客户端

当客户端在已建立的连接上发送针对WebSocket的HTTP/1.1升级请求时,WebSocket会话开始,并且服务器发送适当的响应,指示该请求已被接受并且连接已升级。 Upgrade 请求必须包含Host字段和要请求的资源的target。 实现创建并发送的典型HTTP Upgrade 请求如下所示:

GET / HTTP/1.1
Host: www.example.com
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Key: 2pGeTR0DsE4dfZs2pH+8MA==
Sec-WebSocket-Version: 13
User-Agent: Boost.Beast/216

hosttarget参数成为HTTP请求中Host字段和request-target的一部分。 密钥由实现生成。 希望添加,修改或检查字段的调用者可以在stream上设置decorator 选项(稍后说明)。

websocket::stream成员函数handshakeasync_handshake用于发送带有所需hosttarget字符串的request。 此代码连接到从主机名查找返回的IP地址,然后以客户端角色执行WebSocket握手。

stream<tcp_stream> ws(ioc);
net::ip::tcp::resolver resolver(ioc);
get_lowest_layer(ws).connect(resolver.resolve("www.example.com", "ws"));

// Do the websocket handshake in the client role, on the connected stream.
// The implementation only uses the Host parameter to set the HTTP "Host" field,
// it does not perform any DNS lookup. That must be done first, as shown above.

ws.handshake(
"www.example.com", // The Host field
"/" // The request-target
);

当客户端从服务器收到指示升级成功的HTTP升级响应时,调用者可能希望对收到的HTTP响应消息执行其他验证。 例如,检查对基本身份验证质询的响应是否有效。 为此,handshake 的重载成员函数使调用者可以将接收到的HTTP消息存储在类型为response_type的输出引用参数中,如下所示:

// This variable will receive the HTTP response from the server
response_type res;

// Perform the websocket handshake in the client role.
// On success, `res` will hold the complete HTTP response received.

ws.handshake(
res, // Receives the HTTP response
"www.example.com", // The Host field
"/" // The request-target
);

服务器

对于接受传入连接的服务器,websocket::stream可以读取传入的升级请求并自动答复。 如果握手满足要求,则流将使用101交换协议状态代码发送回升级响应。 如果握手不符合要求,或者超出了调用者先前设置的stream选项所指定的允许参数范围,则stream将返回HTTP响应,并带有指示错误的状态代码。 根据 keep alive设置,连接可能会保持打开状态,以进行后续的握手尝试。 在收到升级请求握手时,实现创建并发送的典型HTTP Upgrade响应如下所示:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Server: Boost.Beast

Sec-WebSocket-Accept字段值是按照WebSocket协议指定的方式从请求中生成的。

stream成员函数acceptasync_accept用于和已连接到传入peer的stream握手,从中读取WebSocket HTTP升级请求,然后发送WebSocket HTTP升级响应,如下所示:

// Perform the websocket handshake in the server role.
// The stream must already be connected to the peer.

ws.accept();

Handshake缓存

服务器可能会从stream中读取数据,并在以后决定将缓存区的字节解释为WebSocket升级请求。 为了解决此用法,提供了accept和async_accept的重载函数,它们可以接受其他缓冲区序列参数。

在此示例中,服务器将初始HTTP请求header读取到动态缓冲区中,然后在以后使用缓冲的数据尝试进行Websocket升级。

// This buffer will hold the HTTP request as raw characters
std::string s;

// Read into our buffer until we reach the end of the HTTP request.
// No parsing takes place here, we are just accumulating data.

net::read_until(sock, net::dynamic_buffer(s), "\r\n\r\n");

// Now accept the connection, using the buffered data.
ws.accept(net::buffer(s));

检查HTTP请求

在实现还支持WebSocket的HTTP服务器时,该服务器通常会从客户端读取HTTP请求。 要检测传入的HTTP请求何时为WebSocket升级请求,可以使用函数is_upgrade。

一旦调用方确定HTTP请求是WebSocket升级,将提供accept和async_accept的其他重载函数,这些重载函数将整个HTTP请求header作为执行握手的对象。 通过手动读取请求,程序可以处理正常的HTTP请求以及升级。 该程序还可以基于HTTP字段(例如基本身份验证)强制实施策略。 在此示例中,首先使用HTTP算法读取请求,然后将其传递给新构建的stream:

// This buffer is required for reading HTTP messages
flat_buffer buffer;

// Read the HTTP request ourselves
http::request<http::string_body> req;
http::read(sock, buffer, req);

// See if its a WebSocket upgrade request
if(websocket::is_upgrade(req)) {
// Construct the stream, transferring ownership of the socket
stream<tcp_stream> ws(std::move(sock));

// Clients SHOULD NOT begin sending WebSocket
// frames until the server has provided a response.
BOOST_ASSERT(buffer.size() == 0);

// Accept the upgrade request
ws.accept(req);
} else {
// Its not a WebSocket upgrade, so
// handle it like a normal HTTP request.
}

子协议(Subprotocols)

WebSocket协议阐释了子协议的概念。 如果客户端请求一组子协议中的一个,它将在初始WebSocket升级HTTP请求中设置标头Sec-WebSocket-Protocol。 由服务器决定报头并选择要接受的协议之一。 服务器通过在接受header中设置Sec-WebSocket-Protocol header来指示所选协议。

这是用 decorator完成的。

以下代码演示了服务器如何读取HTTP请求,将其标识为WebSocket升级,然后在执行WebSocket握手之前检查首选的匹配子协议:

// a function to select the most preferred protocol from a comma-separated list
auto select_protocol = [](string_view offered_tokens) -> std::string {
// tokenize the Sec-Websocket-Protocol header offered by the client
http::token_list offered( offered_tokens );

// an array of protocols supported by this server
// in descending order of preference
static const std::array<string_view, 3> supported = {
"v3.my.chat",
"v2.my.chat",
"v1.my.chat"
};

std::string result;

for (auto proto : supported) {
auto iter = std::find(offered.begin(), offered.end(), proto);
if (iter != offered.end()) {
// we found a supported protocol in the list offered by the client
result.assign(proto.begin(), proto.end());
break;
}
}

return result;
};


// This buffer is required for reading HTTP messages
flat_buffer buffer;

// Read the HTTP request ourselves
http::request<http::string_body> req;
http::read(sock, buffer, req);

// See if it's a WebSocket upgrade request
if(websocket::is_upgrade(req)) {
// we store the selected protocol in a std::string here because
// we intend to capture it in the decorator's lambda below
std::string protocol =
select_protocol(
req[http::field::sec_websocket_protocol]);

if (protocol.empty()) {
// none of our supported protocols were offered
http::response<http::string_body> res;
res.result(http::status::bad_request);
res.body() = "No valid sub-protocol was offered."
" This server implements"
" v3.my.chat,"
" v2.my.chat"
" and v1.my.chat";
http::write(sock, res);
} else {
// Construct the stream, transferring ownership of the socket
stream<tcp_stream> ws(std::move(sock));

ws.set_option(
stream_base::decorator(
[protocol](http::response_header<> &hdr) {
hdr.set(
http::field::sec_websocket_protocol,
protocol);
}));

// Accept the upgrade request
ws.accept(req);
}
} else {
// Its not a WebSocket upgrade, so
// handle it like a normal HTTP request.
}

装饰器(Decorator)

消息

建立Websocket会话后,任何一个peer都可以随时主动发送消息。 一条消息由一个或多个消息帧组成。 每个帧的前缀是有payload的大小(以字节为单位),后跟数据。 帧还包含一个标志(称为“fin),指示它是否是消息的最后一帧。 当消息仅由一帧组成时,可以立即知道消息的大小。 否则,仅在接收到最后一帧后才能确定消息的总大小。

多帧(multi-frame)消息的帧之间的边界不视为消息的一部分。 诸如代理这样的中介程序可以转发WebSocket流量,它们可以自由地以任意方式“重组reframe”(拆分帧并组合它们)消息。 这些中介包括Beast,在某些情况下,Beast可以根据stream中设置的选项自动重组消息。

算法永远不应依赖于将传入或传出消息拆分为帧的方式。

消息可以是文本或二进制。 以文本形式发送的消息必须包含有效的utf8,而以二进制形式发送的消息可能包含任意数据。 除了消息帧外,Websocket还提供ping,pong和close消息形式的控制帧,这些消息的payload 大小上限很小。 根据消息的帧方式,控制帧之间可能会有更多的发送机会。

发送

这些stream成员函数用于写入发送Websocket消息:

  • write, async_write 发送buffer序列作为完整的消息。
  • write_some, async_write_some 发送buffer序列作为消息的一部分。

本示例说明如何将buffer序列作为完整消息发送。

net::const_buffer b("Hello, world!", 13);

// This sets all outgoing messages to be sent as text.
// Text messages must contain valid utf8, this is checked
// when reading but not when writing.

ws.text(true);

// Write the buffer as text
ws.write(b);

因此,同一消息可以在两个或更多帧中发送。

接收

  • read, async_read 将完整的消息读入DynamicBuffer。
  • read_some, async_read_some 将消息的一部分读入DynamicBuffer。
  • read_some, async_read_some 将消息的一部分读取到MutableBufferSequence中。

完成WebSocket握手后,调用方可以使用面向消息的接口发送和接收消息。 此接口要求提前知道代表消息的所有缓冲区:

// This DynamicBuffer will hold the received message
flat_buffer buffer;

// Read a complete message into the buffer's input area
ws.read(buffer);

// Set text mode if the received message was also text,
// otherwise binary mode will be set.
ws.text(ws.got_text());

// Echo the received message back to the peer. If the received
// message was in text mode, the echoed message will also be
// in text mode, otherwise it will be in binary mode.
ws.write(buffer.data());

// Discard all of the bytes stored in the dynamic buffer,
// otherwise the next call to read will append to the existing
// data instead of building a fresh message.
buffer.consume(buffer.size());

websocket::stream不是线程安全的。 对stream成员函数的调用必须全部来自同一隐式或显式strand

一些用例使提前缓冲整个消息变得不切实际或不可能:

  • 将多媒体流传输到端点。
  • 立即发送一条很大的消息(大于内存容量)。
  • 提供可用的增量结果。

对于这些情况,可以使用部分面向数据的接口。 本示例使用以下接口读取并回显完整的消息:

// This DynamicBuffer will hold the received message
multi_buffer buffer;

// Read the next message in pieces
do
{
// Append up to 512 bytes of the message into the buffer
ws.read_some(buffer, 512);
}
while(! ws.is_message_done());

// At this point we have a complete message in the buffer, now echo it

// The echoed message will be sent in binary mode if the received
// message was in binary mode, otherwise we will send in text mode.
ws.binary(ws.got_binary());

// This buffer adaptor allows us to iterate through buffer in pieces
buffers_suffix<multi_buffer::const_buffers_type> cb{buffer.data()};

// Echo the received message in pieces.
// This will cause the message to be broken up into multiple frames.
for(;;) {
if(buffer_bytes(cb) > 512) {
// There are more than 512 bytes left to send, just
// send the next 512 bytes. The value `false` informs
// the stream that the message is not complete.
ws.write_some(false, buffers_prefix(512, cb));

// This efficiently discards data from the adaptor by
// simply ignoring it, but does not actually affect the
// underlying dynamic buffer.
cb.consume(512);
} else {
// Only 512 bytes or less remain, so write the whole
// thing and inform the stream that this piece represents
// the end of the message by passing `true`.
ws.write_some(true, cb);
break;
}
}

// Discard all of the bytes stored in the dynamic buffer,
// otherwise the next call to read will append to the existing
// data instead of building a fresh message.
buffer.consume(buffer.size());

控制帧

超时时间

拆除

笔记

概念

Body

Body阅读器

身体写手

缓冲序列

动态缓冲区

领域

FieldsWriter

文件

收费政策

设计选择

HTTP消息容器

HTTP与其他库的比较

与Zaphoyd Studios WebSocket ++的比较

## 常问问题

发行说明(已移动)

参考(已移动)

指数