Upgrade an HTTP connection to a bidirectional message stream. More...

Topics

 Client Sockets
 Handshake through an HTTP client and send messages on the upgraded stream.
 Server Sessions
 Accept an upgrade and keep application state on the session.

Classes

class  Channel
 Binds one channel to one HTTP stream. More...
class  WebSocketMessage
 Payload of one text or binary WebSocket message. More...

Detailed Description

WebSocket is a protocol that begins as HTTP and then uses the same connection as a bidirectional message channel. HTTP is a sequence of request and reply exchanges, and the client always starts the next exchange. After a WebSocket handshake the two sides send complete payloads independently, for as long as the connection remains open. That is the reason to upgrade: a live feed, a remote editor, or any conversation that cannot wait for the next request.

The handshake is still HTTP. The client sends a GET whose Connection field contains Upgrade, whose Upgrade field is websocket, and whose Sec-WebSocket-Key is sixteen random bytes encoded as Base64. Sec-WebSocket-Version is 13, the version of RFC 6455. An Origin field is common when a browser is the client. The server answers with status 101 Switching Protocols, repeats the Upgrade, and returns Sec-WebSocket-Accept, which is the Base64 encoding of the SHA-1 of that key concatenated with the RFC 6455 GUID. Sec-WebSocket-Protocol offers subprotocol names. The server selects at most one of those names and writes that one name back. No echo means no subprotocol was agreed. After that 101 the bytes on the connection are WebSocket frames, not HTTP messages. A later HTTP request on the same connection would be a protocol error.

On the wire a frame has an opcode, a FIN bit, a payload length, and, on every frame a client writes, a four-byte masking key. Opcode 1 is a text message, opcode 2 is a binary message, and opcode 0 continues the data message that is already open. Opcode 8 is close, 9 is ping, and 10 is pong. FIN false means more frames of the same message follow. The application unit is the message: every frame from the first data opcode through the frame that has FIN set. The engine may split one payload into several frames. The application does not read an opcode, a FIN bit, or a continuation.

Masking is a client-to-server rule of the protocol. Every frame a client writes is XOR-masked with a 32-bit key so that a misbehaving client cannot inject a crafted frame that looks like an HTTP request to an intervening proxy. A server writes unmasked frames. The mode is fixed when the upgraded stream opens. The engine applies it. The application does not supply a mask.

A text message is UTF-8 over the whole message, not per frame. A binary message is an uninterpreted payload. Ping, pong, and close are control frames. They are not messages. A ping asks the peer to answer with a pong of the same payload, at most 125 bytes. A close frame carries a status code and an optional UTF-8 reason, at most 123 bytes of reason text, and starts the close handshake. After a local close, no more data frames are sent. Receive continues until the peer close arrives or the idle timeout fires.

This API keeps the HTTP split between the message and the connection. The HTTP owner keeps the TCP connection and the upgraded stream. The framing layer writes frames into that stream. Callers never construct a payload object. Each side of the conversation holds two payloads for the life of the stream: one that a receive fills, and one that a send writes. Those two payloads may move at the same time. One send or one receive is outstanding until it completes. Starting a second operation on the same direction while the first is still outstanding is an error.

The example writes a text payload, completes a send step, and reads an incoming payload after a receive step.

void onOutputReady(Pt::Http::WebSocket& socket)
{
Pt::Http::MessageProgress progress = socket.endSend();
if( ! progress.finished() )
socket.beginSend();
}
void onInputReady(Pt::Http::WebSocket& socket)
{
if(progress.body())
{
std::string text;
text.resize( socket.incoming().available() );
if( ! text.empty() )
socket.incoming().body().read(&text[0], text.size());
}
if( ! progress.finished() )
{
socket.incoming().discard();
socket.beginReceive();
return;
}
socket.incoming().clear();
socket.beginReceive();
}
socket.outgoing().body() << "hello";
socket.beginSend();
socket.beginReceive();
Progress of an asynchronous HTTP send or receive.
Definition Message.h:337
bool body() const
Returns true if the body was processed.
Definition Message.h:363
bool finished() const
Returns true if message is complete.
Definition Message.h:373
@ Text
UTF-8 text.
Definition WebSocketMessage.h:127
void discard()
Drops the buffered body and leaves the type.
void setType(Type type)
Sets the data type of an outgoing message.
std::iostream & body()
Returns the payload stream.
Definition WebSocketMessage.h:144
void clear()
Drops the buffered body and sets the type to Unknown.
std::size_t available() const
Returns how many payload bytes can be read.
Client handshake and messages on an upgraded stream.
Definition WebSocket.h:164
void beginSend(bool finished=true)
Begins sending outgoing().
void beginReceive()
Begins receiving into incoming().
WebSocketMessage & outgoing()
Returns the outgoing message.
MessageProgress endReceive()
Completes the receive started by beginReceive().
MessageProgress endSend()
Completes the send started by beginSend().
WebSocketMessage & incoming()
Returns the incoming message.

The outgoing payload is outgoing() and the incoming payload is incoming(). Both are WebSocketMessage objects. Set the type to Text or Binary, write the body, and call beginSend(). Call beginSend(false) while more body will be written, then beginSend(true) for the last part. outputReady() reports that data bytes were sent. endSend() returns MessageProgress. If the send step is not finished, beginSend() continues it with the same completion flag. beginReceive() fills incoming(). inputReady() reports that data bytes were received. endReceive() returns progress. header() means the data type is known, which is the first data opcode of that message. body() means payload bytes were processed on this step. finished() means the data message is complete. If the receive is not finished, discard the consumed body and call beginReceive() again. After finished(), clear() drops the body so the same object can carry the next message. The server session exposes the same begin and end methods on the session object; the data callbacks there take the place of the ready signals.

Heartbeats, shutdown and close are socket operations, not payloads.

socket.ping("are you there", 13);
socket.shutdown(1000, "done");
socket.close();
void close()
Releases the stream and writes no frame.

ping() enqueues a ping frame. A received ping is answered by the engine. A received pong is consumed. Neither is delivered through incoming(), and neither reports data-ready. shutdown() enqueues a close frame with a status code and a reason. Codes 1005, 1006 and 1015 cannot be sent; they are reserved for "no status received", abnormal closure, and a failed TLS handshake. A received close is answered by the engine. After a local shutdown, no more data frames are sent. close() releases the stream and writes no frame. It does not block. The destructor calls it. A pending transfer is cancelled and the buffer is discarded. The framed stream is not resumed. beginSend() and beginReceive() then throw. When the engine ends the protocol, closed() runs on the client and onClosed() on the session. The application calls close() there, or after endShutdown(). closeCode() and closeReason() report the handshake that ended the stream. No close frame leaves the code at 1006. The handshake uses the HTTP timeout of the client or the server. A finished 101 stops it on the connection. After that, a data message larger than the configured limit, inbound silence, an unanswered ping, or an unanswered close handshake closes the stream. Zero disables idle, the ping interval and ping. Close defaults to ten seconds. Incoming frames restart idle and the ping interval. After interval silence the engine writes a keepalive ping while a receive is outstanding. A pong or an incoming ping stops a ping wait. Sending does not restart idle or the ping interval.

The HTTP owner still owns the stream. Opening a client socket uses the HTTP user agent of the client chapter. Accepting a server session uses the service mapping of the server chapter. The client and server sections of this chapter take those two reader tasks from the handshake onward.