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  StreamSession
 Binds one session object 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. 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:80
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:97
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:133
void beginSend()
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(). outputReady() reports that data bytes were sent. endSend() returns MessageProgress. If the send is not finished, beginSend() continues the same message. 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 and close are socket operations, not payloads.

socket.ping("are you there", 13);
socket.close(1000, "done");
void ping(const char *payload=0, std::size_t n=0)
Enqueues a ping with an optional payload of at most 125 bytes.
void close(unsigned code=1000, const std::string &reason=std::string())
Enqueues a close frame and ends the stream after the close handshake.

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. close() 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 close, no more data frames are sent. closeCode() and closeReason() report the handshake that ended the stream. A data message larger than the configured limit, or an idle period without a finished transfer, closes the stream. Zero disables each limit. The size count is the declared payload from the first data opcode to FIN. A finished send or receive restarts the idle timeout, and a received ping or pong restarts it as well.

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.