Files
boost_beast/doc/design/http_message.qbk
Vinnie Falco 75b0571e83 Refactor type_traits (API Change):
fix #373

* concepts.hpp is renamed to type_traits.hpp

* Body reader and writer concepts are renamed
2017-07-20 08:12:17 -07:00

285 lines
9.6 KiB
Plaintext

[/
Copyright (c) 2013-2017 Vinnie Falco (vinnie dot falco at gmail dot com)
Distributed under the Boost Software License, Version 1.0. (See accompanying
file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
]
[section:http_message HTTP Message Container]
In this section we describe the problem of modeling HTTP messages and explain
how the library arrived at its solution, with a discussion of the benefits
and drawbacks of the design choices. The goal for creating a message model
is to create a container with value semantics, possibly movable and/or
copyable, that completely describes the message. More formally, the container
has all of the information necessary for the serialization algorithms to
represent the entire message as a series of octets, and that the parsing
algorithms store all of the captured information about an entire message
in the container.
In addition to capturing the entirety of a message, we would like the message
container to contain enough customization points to permit the following:
allocator support, user-defined containers to represent header fields, and
user-defined containers to represent the body. And finally, because requests
and responses have slightly different information in the ['start-line], we
would like the containers for requests and responses to be represented by
different types.
Here is our first attempt at declaring some message containers:
[table
[[
```
/// An HTTP request
template<class Fields, class Body>
struct request
{
int version;
std::string method;
std::string target;
Fields fields;
typename Body::value_type body;
};
```
][
```
/// An HTTP response
template<class Fields, class Body>
struct response
{
int version;
int status;
std::string reason;
Fields fields;
typename Body::value_type body;
};
```
]]
]
Here we have accomplished a few things. Request and response objects are
different types. The user can choose the container used to represent the
fields. And the user can choose the [*Body] type, which is a concept
defining not only the type of `body` member but also the algorithms used
to transfer information in and out of that member when performing
serialization and parsing.
However, a problem arises. How do we write a function which can accept
an object that is either a request or a response? As written, the only
ovious solution is to make the message a template type. Additional traits
classes would then be needed to make sure that the passed object has a
valid type which meets the requirements. We can avoid those complexities
by renaming the containers and making them partial specializations of a
single class, like this:
```
/// An HTTP message
template<bool isRequest, class Fields, class Body>
struct message;
/// An HTTP request
template<class Fields, class Body>
struct message<true, Fields, Body>
{
int version;
std::string method;
std::string target;
Fields fields;
typename Body::value_type body;
};
/// An HTTP response
template<bool isRequest, class Fields, class Body>
struct response<false, Fields, Body>
{
int version;
int status;
std::string reason;
Fields fields;
typename Body::value_type body;
};
```
Now we can declare a function which takes any message as a parameter:
```
template<bool isRequest, class Fields, class Body>
void f(message<isRequest, Fields, Body>& msg);
```
This function can manipulate the fields common to requests and responses.
If it needs to access the other fields, it can do so using tag dispatch
with an object of type `std::integral_constant<bool, isRequest>`.
Often, in non-trivial HTTP applications, we want to read the HTTP header
and examine its contents before choosing a type for [*Body]. To accomplish
this, there needs to be a way to model the header portion of a message.
And we'd like to do this in a way that allows functions which take the
header as a parameter, to also accept a type representing the whole
message (the function will see just the header part). This suggests
inheritance:
```
/// An HTTP message header
template<bool isRequest, class Fields>
struct header;
/// An HTTP request header
template<class Fields>
struct header<true, Fields>
{
int version;
std::string method;
std::string target;
Fields fields;
};
/// An HTTP response header
template<class Fields>
struct header<false, Fields>
{
int version;
int status;
std::string reason;
Fields fields;
};
/// An HTTP message
template<bool isRequest, class Fields, class Body>
struct message : header<isRequest, Fields>
{
typename Body::value_type body;
/// Construct from a `header`
message(header<isRequest, Fields>&& h);
};
```
Note that the `message` class now has a constructor allowing messages
to be constructed from a similarly typed `header`. This handles the case
where the user already has the header and wants to make a commitment to the
type for [*Body]. This also lets us declare a function accepting any header:
```
template<bool isRequest, class Fields>
void f(header<isRequest, Fields>& msg);
```
Until now we have not given significant consideration to the constructors
of the `message` class. But to achieve all our goals we will need to make
sure that there are enough constructor overloads to not only provide for
the special copy and move members if the instantiated types support it,
but also allow the fields container and body container to be constructed
with arbitrary variadic lists of parameters. This allows those members
to properly support allocators.
The solution used in the library is to treat the message like a `std::pair`
for the purposes of construction, except that instead of `first` and `last`
we have `fields` and `body`. This means that single-argument constructors
for those fields should be accessible as they are with `std::pair`, and
that a mechanism identical to the pair's use of `std::piecewise_construct`
should be provided. Those constructors are too complex to repeat here, but
interested readers can view the declarations in the corresponding header
file.
There is now significant progress with our message container but a stumbling
block remains. The presence of `std::string` members is an obstacle to our
goal of making messages allocator-aware. One obvious solution is to add an
allocator to the template parameter list of the header and message classes,
and allow the string based members to construct with instances of those
allocators. This is unsatisfying because of the combinatorial explosion of
constructor variations needed to support the scheme. It also means that
request messages could have [*four] different allocators: two for the fields
and body, and two for the method and target strings. A better solution is
needed.
To make allocators workable, we make a simple change to the interface and
then engineer a clever concession. First, the interface change:
```
/// An HTTP request header
template<class Fields>
struct header<true, Fields>
{
int version;
string_view method() const;
void method(string_view);
string_view target(); const;
void target(string_view);
Fields fields;
};
/// An HTTP response header
template<class Fields>
struct header<false, Fields>
{
int version;
int status;
string_view reason() const;
void reason(string_view);
Fields fields;
};
```
This approach replaces public data members with traditional accessors
using non-owning references to string buffers. Now we make a concession:
management of the corresponding string is delegated to the [*Fields]
container, which can already be allocator aware and constructed with the
necessary allocator parameter via the provided constructor overloads for
`message`. The delegation implementation looks like this (only the
response header specialization is shown):
```
/// An HTTP response header
template<class Fields>
struct header<false, Fields>
{
int version;
int status;
auto
reason() const -> decltype(std::declval<Fields>().reason()) const
{
return fields.reason();
}
template<class Value>
void
reason(Value&& value)
{
fields.reason(std::forward<Value>(value));
}
Fields fields;
```
An advantage of this technique is that user-provided implementations of
[*Fields] may use arbitrary representation strategies. For example, instead
of explicitly storing the method as a string, store it as an enumerated
integer with a lookup table of static strings for known message types.
Now that we've accomplished our initial goals and more, there is one small
quality of life improvement to make. Users will choose different types for
`Body` far more often than they will for `Fields`. Thus, we swap the order
of these types and provide a default:
```
/// An HTTP header
template<bool isRequest, class Body, class Fields = fields>
struct header;
/// An HTTP message
template<bool isRequest, class Body, class Fields = fields>
struct message;
```
This container is also capable of representing complete HTTP/2 messages.
Not because it was explicitly designed for, but because the IETF wanted to
preserve message compatibility with HTTP/1. Aside from version specific
fields such as Connection, the contents of HTTP/1 and HTTP/2 messages are
identical even though their serialized representation is considerably
different. The message model presented in this library is ready for HTTP/2.
In conclusion, this representation for the message container is well thought
out, provides comprehensive flexibility, and avoids the necessity of defining
additional traits classes. User declarations of functions that accept headers
or messages as parameters are easy to write in a variety of ways to accomplish
different results, without forcing cumbersome SFINAE declarations everywhere.
[endsect]