...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
A WebSocket session begins when a client sends the HTTP/1.1 Upgrade request for WebSocket on an established connection, and the server sends an appropriate response indicating that the request was accepted and that the connection has been upgraded. The Upgrade request must include the Host field, and the target of the resource to request. A typical HTTP Upgrade request created and sent by the implementation will look like this:
Table 1.31. WebSocket HTTP Upgrade Request
Wire Format |
Description |
---|---|
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 |
The host and target parameters become part of the Host field and request-target in the resulting HTTP request. The key is generated by the implementation. Callers who wish to add, modify, or inspect fields may set the decorator option on the stream (described later). |
The websocket::stream
member functions handshake
and async_handshake
are used to send
the request with the required host and target strings. This code connects
to the IP address returned from a hostname lookup, then performs the WebSocket
handshake in the client role.
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 );
When a client receives an HTTP Upgrade response from the server indicating
a successful upgrade, the caller may wish to perform additional validation
on the received HTTP response message. For example, to check that the response
to a basic authentication challenge is valid. To achieve this, overloads
of the handshake member function allow the caller to store the received HTTP
message in an output reference argument of type response_type
as follows:
// 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 );
For servers accepting incoming connections, the websocket::stream
can read the incoming upgrade
request and automatically reply. If the handshake meets the requirements,
the stream sends back the upgrade response with a 101
Switching Protocols status code. If the handshake does
not meet the requirements, or falls outside the range of allowed parameters
specified by stream options set previously by the caller, the stream sends
back an HTTP response with a status code indicating an error. Depending on
the keep alive setting, the connection may remain open for a subsequent handshake
attempt. A typical HTTP Upgrade response created and sent by the implementation
upon receiving an upgrade request handshake will look like this:
Table 1.32. WebSocket Upgrade HTTP Response
Wire Format |
Description |
---|---|
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Server: Boost.Beast |
The Sec-WebSocket-Accept field value is generated from the request in a fashion specified by the WebSocket protocol. |
The stream
member functions accept
and async_accept
are used to read the
WebSocket HTTP Upgrade request handshake from a stream already connected
to an incoming peer, and then send the WebSocket HTTP Upgrade response, as
shown:
// Perform the websocket handshake in the server role. // The stream must already be connected to the peer. ws.accept();
It is possible for servers to read data from the stream and decide later
that the buffered bytes should be interpreted as a WebSocket upgrade request.
To address this usage, overloads of accept
and async_accept
which accept an additional
buffer sequence parameter are provided.
In this example, the server reads the initial HTTP request header into a dynamic buffer, then later uses the buffered data to attempt a websocket upgrade.
// 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));
When implementing an HTTP server that also supports WebSocket, the server
usually reads the HTTP request from the client. To detect when the incoming
HTTP request is a WebSocket Upgrade request, the function is_upgrade
may be used.
Once the caller determines that the HTTP request is a WebSocket Upgrade,
additional overloads of accept
and async_accept
are provided which
receive the entire HTTP request header as an object to perform the handshake.
By reading the request manually, the program can handle normal HTTP requests
as well as upgrades. The program may also enforce policies based on the HTTP
fields, such as Basic Authentication. In this example, the request is first
read in using the HTTP algorithms, and then passed to a newly constructed
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. }
The WebSocket protocol understands the concept of subprotocols. If the client is requesting one of a set of subprotocols it will set the header Sec-WebSocket-Protocol in the initial WebSocket Upgrade HTTP request. It is up to the server to parse the header and select one of the protocols to accept. The server indicates the selected protocol by setting the Sec-WebSocket-Protocol header in the accept header.
This is accomplished with a decorator
.
The code that follows demonstrates how a server reads an HTTP request, identifies it as a WebSocket Upgrade, and then checks for a preferred matching subprotocol before performing the WebSocket handshake:
// 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. }