This module is the portable HTTP layer, so a caller sends and receives HTTP messages through the same types on every supported platform. The unit of work is a message, which is a header and an optional body. A Request is the message a client sends and a server receives, and a Reply is the message a server sends and a client receives. Both are Message types that share the header-and-body model and differ in the start line.
A Client is an HTTP user agent that holds one request and one reply. There is no separate connect step, because the client opens a TCP connection to its Endpoint when a send needs one. A persistent connection is a keep-alive header that the server also permits, and pipelining reuses that connection. After a 401 reply, Authenticator prepares the request so it can be sent again.
A Server listens on an Endpoint and does not implement resources itself. A Servlet maps an incoming request to a Service and may attach an Authorizer. The service is a factory for Responder objects, and each responder handles one request and writes one reply.
HTTP I/O takes part in the System event-loop model. Asynchronous work attaches a client or a server with setActive() so an EventLoop can monitor the connection, but the loop does not own the HTTP object: the code that creates it keeps it alive while an operation is still waiting on the loop. The same objects also have blocking send and receive operations, though asynchronous operation is the usual path.
Endpoints and TCP belong to Pt::Net. This module names a host and a port with Endpoint and listens or connects through that address. HTTPS is the same client and server API after setSecure() has been given a Pt::Ssl::Context; certificate and handshake details live in the SSL module.
A protocol error that is specific to HTTP is an HttpError, which is an I/O error. A local address that is already occupied still throws AddressInUse from the listen that uses it.
An HTTP connection can be upgraded. After a finished 101 the owner keeps the TCP connection and exposes it as a Stream. The server calls Service::onUpgrade() with that stream, the request, and the reply. A client obtains the stream it already owns with Client::upgrade(). Binding a Channel accepts the upgrade. WebSocket is the framed protocol on that path: a handshake that is still HTTP, then messages on the same connection for as long as the stream lasts.
The rest of this chapter is the message model, then the client, then the server, then WebSocket.
This chapter covers:
An HTTP message is a header and an optional body. The header is a list of name and value fields together with the HTTP version, and the body is a byte stream. Message holds both. Request and Reply are messages: a request adds the method, the URL and the query string, while a reply adds the status code and the status text.
Callers do not normally construct these types. A Client owns the request it will send and the reply it receives, and a Responder is given the server-side request and reply for one exchange, so the same header and body operations apply on both sides.
MessageHeader stores fields in a fixed buffer. set() replaces a field, add() appends another value for the same name, and get() and has() look up a name. begin() and end() walk the fields. Keep-alive, chunked transfer coding, content length and Upgrade are derived from those fields rather than being a second header model.
body() is a bidirectional stream on the message. Write to it to fill a request or a reply that will be sent, and read from it when a receive step has delivered body bytes. discard() drops buffered body data. available() and pending() report how much can be read or still has to be written.
Asynchronous send and receive move a message in steps, and each completed step returns a MessageProgress value. header() is true when the header of that message is available, body() is true when body bytes were processed, and finished() is true when the message is complete. A short message often sets all three on one step, while a long body, or a send that only partly left the socket, needs another begin and end pair. Progress can also be empty, meaning the step made I/O progress without exposing a header or a body yet, in which case the next begin call continues the same message.
HttpError is the HTTP-specific I/O error. Credential is a user name and password that client authentication and server authorization both use, though they do not use the same authenticator type.
MessageHeader is the field list and HTTP version of a message. It is the header half of Message, and through that of Request and Reply. Callers do not usually construct a header on its own; they use header() on the message the client or responder already holds.
Fields are names and values. set() replaces the value for a name, add() appends another value for the same name, and remove() deletes it. get() returns the value or a null pointer, has() reports presence, and isSet() tests that a name has a particular value. begin() and end() walk the fields as Field values, each with a name and a value pointer into the header's buffer.
Keep-alive, chunked transfer coding, content length and Upgrade are not a second header model: they are derived from those fields. isKeepAlive(), isChunked(), contentLength() and isUpgrade() read them, while setKeepAlive() and setUpgrade() write the corresponding fields. HTTP version is versionMajor() and versionMinor(), set together with setVersion().
The header has a fixed size, so the caller must not rely on filling it beyond that limit. clear() empties the fields so the same header can be used for another message.
Message is the header-and-body object that Request and Reply extend. The header is header(), and the body is body(), an iostream. Write the body before sending, and read it when progress reports that body bytes are available.
Callers do not construct a Message. A Client owns the request and reply, and a Responder is passed the server-side pair. The connection that carries the message is connection(), which is the HTTP connection the client or server already opened.
available() is how many body bytes can be read without blocking, and pending() is how many body bytes still have to be written. discard() drops the buffered body so the stream can be reused for another message.
MessageProgress is the result of one begin/end step on a message. The client returns it from endSend() and endReceive(), and the same flags describe how far a request or a reply has moved.
The flags are independent and can be combined. header() means the header of that message is now available to read or has been sent. body() means body bytes were processed on this step. finished() means the message is complete, so the caller must not begin the same send or receive again. trailer() means trailer fields were processed.
A short reply often arrives in one receive step, in which case header, body and finished are all true. A large body, or a socket that accepted only part of a write, returns finished as false, and the caller calls begin again. It is also possible that I/O made progress without a header or body becoming visible yet, so none of the three is true; begin again until finished.
For a chunked send, finished means the current chunk has left the socket, not that the whole request is complete. The completion flag on the next beginSend() decides whether another chunk follows.
Request is the Message a client sends and a server receives. It adds the request line: method, URL and query string. The default method is GET. setUrl() sets the resource path, setMethod() sets the verb, and setQParams() sets the query without the leading question mark. Header fields and the body are the inherited Message operations.
On the client, the request is Client::request(). Fill it before beginSend() or beginReceive(), including keep-alive or content headers that the exchange needs. On the server, the responder receives it in onBeginRequest() and onReadRequest(), where the header is already parsed and the body arrives in chunks.
clear() resets URL, method, query and the inherited header and body so the same request object can be filled for another exchange. Send and receive on Request itself are used by the connection; the client API is Client::beginSend() and Client::beginReceive().
Reply is the Message a server sends and a client receives. It adds the status line: statusCode() and statusText(), set together with setStatus(). The default is 200 OK. The nested StatusCode enumeration names the codes the API uses in replies it generates, but setStatus() accepts any code.
On the client, the reply is Client::reply() after a receive step has made it available. Read body() when progress reports body bytes, and read the status when progress reports the header. On the server, the responder writes the reply and starts sending it with beginSend(). The completion flag is true when this is the last chunk of the body, and false when onWriteReply() should run again for more. Calling beginSend(true) finishes the reply and releases the responder.
clear() resets status, header and body so the same reply object can be used for another exchange.
Client is the HTTP user agent. It holds one Request and one Reply, so the usual work is to fill the request, send it, and read the reply. The client opens a TCP connection to its host when a send needs one; there is no separate connect method.
The host is an Endpoint passed to a constructor or to setHost(). Asynchronous work needs an EventLoop, passed to a constructor or to setActive(), but the loop does not own the client. HTTPS is the same send and receive path after setSecure() has been given a Pt::Ssl::Context.
The request is request(). Set the URL, the method, query parameters and header fields before the send starts. The default method is GET. Write the request body with request().body(). The reply is reply() after a receive step has made it available.
Asynchronous receive is beginReceive() and endReceive(), and replyReceived() is emitted when a step has completed. endReceive() returns MessageProgress; if the reply is not finished, call beginReceive() again. Asynchronous send is beginSend() and endSend(), with requestSent() as the matching signal. A single beginReceive() is enough when the request is already complete and the caller only waits for the reply. Send first when the request has a body, when the body is chunked, or when several requests are pipelined.
A keep-alive header on the request asks for a persistent connection, which the server may still close. Pipelining needs that persistent connection, because several requests are sent before the matching replies are received. close() ends the connection. Leave it open only while the next request will reuse it; a later send on a closed or timed-out connection opens a new one.
send() and receive() are the blocking forms of the same exchange, and they complete the request or the reply on the calling thread.
A finished reply with status 101 switches the connection to a Stream. upgrade() returns the stream the client already owns. The client no longer sends requests on it.
When a reply is 401, Authenticator complements the request from realm credentials so the client can send it again. Basic authentication is built in, and other Authentication methods can be added.
Client is the HTTP user agent in the client model. It holds one Request and one Reply. Fill the request, send it, and read the reply. The client opens a TCP connection to its host when a send needs one, so there is no separate connect method. The host is an Endpoint passed to a constructor or to setHost().
Asynchronous work needs an EventLoop, passed to a constructor or to setActive(). The loop does not own the client; keep the client alive while an operation is still waiting on the loop. setTimeout() bounds I/O. send() and receive() are the blocking forms of the same exchange.
The request is request(). Set the URL, the method, query parameters and header fields before the send starts, and write the body with request().body(). The default method is GET. The reply is reply() after a receive step has made it available.
Asynchronous receive is beginReceive() and endReceive(). replyReceived() is emitted when a step has completed. The slot calls endReceive(), which returns MessageProgress. If the header is available, the status can be read; if the body is available, it can be read from reply().body(); if the reply is not finished, beginReceive() continues the same reply. A short reply often completes in one step.
The example is an asynchronous GET. The client is constructed with the loop and the host, the request URL is set, and beginReceive() starts the exchange. The slot ends each receive step and exits the loop when the reply is finished.
Pipelining sends several requests before receiving the matching replies, which needs a persistent connection. Set the keep-alive header on the request, connect requestSent() as well as replyReceived(), and start with beginSend() rather than beginReceive(). The send slot calls endSend(); if that send is not finished, beginSend() continues it, and if it is finished, the next request can be filled and sent. When no further request will be pipelined, beginReceive() starts reading the replies. Identify each reply by order or by application state, because Reply does not store the request URL.
A chunked request body is sent with beginSend(false) until the last chunk, so the completion flag is false while more body data will be written. endSend() reports whether the current chunk has left the socket, not whether the whole request is complete. When a chunk has finished and more data remains, write it to request().body() and call beginSend(false) again. When no more chunks remain, beginReceive() finishes the request correctly. To pipeline another chunked request after this one, call beginSend(true) so the request body is terminated.
A keep-alive header on the request asks for a persistent connection, which pipelining needs and which the server may still close. close() ends the connection. Leave it open only while the next request will reuse it; otherwise the server keep-alive timeout may close it before the client is used again. A later send on a closed or timed-out connection opens a new one.
setSecure() assigns a Pt::Ssl::Context so further connections are HTTPS. setPeerName() sets the name expected in the peer certificate. Send and receive are otherwise unchanged. Certificate and handshake details live in the SSL module.
A finished reply with status 101 switches the connection to a Stream. Call upgrade() after endReceive() reports the reply finished. The client owns that stream and no longer sends requests on the connection. That call stops the client I/O timeout on the connection.
Authenticator prepares a request after a 401 reply so the client can send it again. It is not the server-side authorizer. Store credentials per realm with setCredential(), then call authenticate() with the rejected request and the 401 reply. The method writes the authentication headers onto the request and returns true when that is possible. It returns false when no credentials are available for the realm, or when the challenge cannot be met.
Basic authentication is registered by default. Other Authentication methods can be added with addAuthentication(). The authenticator does not send the request; the caller sends it again through the client.
The example stores a realm credential and applies it to a request that just received 401.
Server is the listening HTTP service. It binds a local Endpoint and accepts connections, but it does not implement a resource. Incoming requests are mapped by servlets. Each Servlet combines a mapping rule, a Service, and an optional Authorizer. addServlet() registers the servlet, and the first servlet that maps the request handles it.
A service is a factory: onGetResponder() creates a Responder for the request, and onReleaseResponder() destroys it after the reply has been sent or the exchange has failed. BasicService is that factory for a responder type, with an allocator. Use a custom Service when the responder type depends on the request, or when responders are pooled.
A responder handles one exchange, and the server calls it in order. onBeginRequest() runs when the request header is available, onReadRequest() runs for each chunk of the request body, onBeginReply() runs when the request is complete and the reply should start, and onWriteReply() runs when a previous beginSend(false) needs another chunk of the reply body. The responder finishes the reply with Reply::beginSend() and the completion flag set to true. It may send that finished reply from an earlier callback, in which case remaining callbacks are skipped and the rest of the request is ignored.
A finished reply with status 101 upgrades the connection. The server keeps the connection and calls Service::onUpgrade() on the server thread with the Stream it owns, the request, and the reply. Those two references last until the call returns. Binding a Channel to that stream accepts the upgrade. A service that leaves the stream unbound declines it, and the server closes the stream. Stream::protocol() is the value of the request's Upgrade header. Stream::buffer() is the byte buffer of that channel.
All server I/O is asynchronous, so the server needs an EventLoop, passed to a constructor or to setActive(). listen() binds the local endpoint, and a second listen replaces the previous binding. setSecure() restricts the server to HTTPS. setMaxThreads() bounds worker threads, and timeouts together with setMaxRequestSize() bound idle connections and request size.
MapUrl maps one exact URL, and MapAny maps every request. A custom servlet implements onRequest() and returns true when its service should run. Several servlets may share one service, so the same resource can appear under more than one name.
Authorizer is the server-side access check. It is attached to a servlet, not to the server as a whole, and the same authorizer may be shared. BasicAuthorizer implements HTTP Basic authentication and asks a derived class whether the credentials are granted. Authorization may complete immediately or through an asynchronous Authorization object.
The example is the listening server and a URL mapping. The service is a BasicService for a responder type that the next sections implement, and the loop runs the server.
Server binds a local Endpoint and routes each incoming request to the first registered servlet that maps it. The server itself does not provide a resource. Use addServlet() to register a servlet and removeServlet() to unregister it.
Server I/O is asynchronous and requires an EventLoop. Supply the loop when constructing the server or later through setActive(); the server does not take ownership of it. listen() binds an endpoint and replaces an existing binding. cancel() stops accepting connections and cancels pending work.
setSecure() configures the server with a Pt::Ssl::Context for HTTPS connections. The servlet and responder model remains the same.
The server can limit worker threads, request size, idle I/O, and the lifetime of persistent connections.
Servlet is the mapping rule in the server model. It combines a Service with an optional Authorizer and decides whether an incoming request belongs to that service. The server calls isMapped(), which forwards to onRequest(), and the first servlet that returns true handles the request.
Construct it with a service, or with a service and an authorizer. Several servlets may share one service, so the same resource can appear under more than one name, and they may share one authorizer. service() and authorizer() return those pointers.
MapUrl maps one exact URL. MapAny maps every request. A custom servlet implements onRequest() and returns true when its service should run. Do not reimplement MapUrl unless the mapping rule is actually different.
setShutdown() marks the servlet so it stops taking new work, and isIdle() is true when no exchange is using it. detach() unregisters it from the server that holds it.
The example is the usual mapping: one URL, one service. The optional authorizer is passed as a third argument.
Service is the responder factory in the server model. The server does not construct responders itself: when a servlet maps a request, the service's getResponder() calls onGetResponder(), and when the reply has been sent or the exchange has failed, releaseResponder() calls onReleaseResponder(). The service must remain alive while any responder it created is still in use.
BasicService is that factory for a single responder type, using an allocator that defaults to new and delete. Use a custom Service when the responder type depends on the request headers, or when responders are pooled.
A responder upgrades the connection by finishing the reply with status 101. The server keeps the TCP connection and calls onUpgrade() on the server thread with the Stream it owns, the request that opened the upgrade, and the reply that finished it. Those two references are valid until the call returns. The responder has already been released. Binding a Channel to that stream accepts the upgrade. The empty base implementation leaves the stream unbound, and the server closes it. Stream::protocol() is the value of the request's Upgrade header. The stream buffer is Stream::buffer(). beginInput() and beginOutput() transfer that buffer.
The example is the usual factory: a BasicService for a responder type. The equivalent hand-written service implements onGetResponder() and onReleaseResponder() in the same way.
Responder is the per-exchange handler in the server model. A Service creates it for a mapped request and releases it when the reply has been sent. Derive from it and implement the four callbacks, which the server calls in order.
onBeginRequest() runs when the request header is available, so the responder can inspect fields and prepare the reply. onReadRequest() runs for each chunk of the request body, and may be called more than once. onBeginReply() runs when the request is complete and the reply should start. onWriteReply() runs when a previous Reply::beginSend(false) needs another chunk of the reply body, which is how a large reply is written in chunked encoding.
Finish the reply with Reply::beginSend() and the completion flag set to true. That call may be made from an earlier callback, in which case the remaining callbacks are skipped and the rest of the request is ignored. service() is the service that created this responder.
The example is a responder that ignores the request body and writes a fixed reply. beginSend(true) completes the reply in onBeginReply(), so onWriteReply() stays empty.
Authorizer is the access check in the server model. It is attached to a Servlet, not to the server as a whole, and the same authorizer may be shared by several servlets. It is not the client-side Authenticator. The realm is passed to the constructor and returned by realm().
beginAuthorize() starts the check for a request and reply. When the result is already known, granted is set and a null pointer is returned. When the check needs I/O, an Authorization object is returned; endAuthorization() completes it, and cancelAuthorization() aborts it. The authorizer releases that object in onReleaseAuthorization().
BasicAuthorizer implements HTTP Basic authentication. A derived class implements onAuthorizeCredentials() and either sets granted immediately or returns an Authorization for later completion. BasicUserListAuthorizer is that check against an in-memory user list.
The example grants or denies access from credentials in one step, so it returns a null pointer and does not need to release an authorization object.
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.
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.
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.
WebSocketMessage is the payload of one WebSocket data message. It is the peer of an HTTP message, not of a request or a reply. There is no header and no URL. The only type it exposes is the data type of the payload: UTF-8 text, or an uninterpreted binary body. Callers do not construct this object. The socket holds two of them for the life of the stream, one that a receive fills and one that a send writes.
The example writes a text payload on the outgoing message and reads an incoming payload after a receive step.
type() is Unknown until a data type is set. setType() is for the outgoing message and accepts Text or Binary. The engine sets the type of the incoming message when it has parsed the first data frame of that message. A text body must be valid UTF-8 over the whole message. body() is the iostream. Write it before the send begins. After a receive step, read the bytes that step delivered. available() is how many payload bytes can be read. pending() is how many payload bytes are waiting to be sent.
discard() drops the buffered body and leaves the type. That is the call during a message that is not finished, after the application has consumed what body() holds. clear() drops the body and sets the type to Unknown, so the same object can carry the next message. After a receive that reports finished, the application calls clear().
A client WebSocket is the handshake plus the message surface on the stream that handshake produces. Host, port, event loop, timeout and TLS stay settings of the HTTP user agent. The socket stores a reference to that agent and does not own it, so the agent must outlive the socket, including a handshake that is still waiting on the loop. After a finished 101 the socket formats the stream the client already owns. The client still owns the connection. Closing the socket closes the stream. The socket does not own the stream.
The example completes the handshake and then writes a text message.
Construct WebSocket with the Client that performs the handshake. Connect connected() and call beginConnect() with a request path, or with a ws:// URL whose host and port are already the client's endpoint. The path and query of that URL become the request URL; the client does not take the host from the URL. The optional second argument is the Origin header. beginConnect() fills a GET with Connection, Upgrade, Sec-WebSocket-Version 13 and Sec-WebSocket-Key, and sends it through the client. addProtocol() offers one Sec-WebSocket-Protocol name before that send. Several calls offer several names, in order. A finished 101 becomes the stream this socket formats. endConnect() completes the handshake and throws if it failed. protocol() is the single name the server echoed, or empty when the server selected none. An echo that was not offered fails the handshake. After that success the socket no longer uses the client for messages. A client masks every frame it writes.
close() releases the stream and writes no frame. The destructor calls it. The application learns that the stream has ended from the outstanding operation: an outstanding receive completes with an error, and an outstanding shutdown completes through shutdownReady(). Without either, a peer close is not seen. Destruction of the stream detaches the channel. It ends an outstanding send or receive. The owner deletes this socket. The handshake uses the timeout of the client. After the upgrade that timeout is stopped. setIdleTimeout() bounds inbound silence, setPingInterval() a keepalive ping after inbound silence, setPingTimeout() an unanswered ping, and setCloseTimeout() a close handshake the peer never answers.
WebSocket is the client side of a WebSocket upgrade. It performs the HTTP handshake through an HTTP user agent and then sends and receives payloads on the stream that handshake produces. Host, port, event loop, timeout and TLS are settings of that agent. The handshake uses the client's timeout. The socket stores a reference and does not own it, so the agent must outlive the socket, including a handshake that is still waiting on the loop. After a finished 101 the socket formats the stream the client already owns. setIdleTimeout() bounds inbound silence, setPingInterval() a keepalive ping after inbound silence, setPingTimeout() an unanswered ping, and setCloseTimeout() a close handshake the peer never answers. The client still owns the connection. Closing the socket closes the stream. The socket does not own the stream. A client masks every frame it writes.
The example completes the handshake and then writes a text message.
Construct the socket with the Client that performs the handshake. Connect connected() and call beginConnect() with a request path, or with a ws:// URL whose host and port are already the client's endpoint. The path and query of that URL become the request URL; the client does not take the host from the URL. The optional second argument is the Origin header. endConnect() completes the handshake and throws if it failed. addProtocol() offers one Sec-WebSocket-Protocol name before beginConnect(). Several calls offer several names, in order. After a finished handshake protocol() is the single name the server echoed, or empty when the server selected none. An echo that was not offered fails the handshake. After that success the socket no longer uses the client for messages.
The socket owns two WebSocketMessage objects, incoming() and outgoing(), the way a client owns a request and a reply. Set the type, 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. After finished(), clear() drops the body so the same object can carry the next message. beginReceive() starts frame processing. Until then, incoming frames stay in the stream. Control frames are processed while a receive is outstanding. A local beginShutdown() also keeps input running until the peer replies with its close frame. The two are independent: a receive started before or during a shutdown still delivers what the peer sent before that reply.
Ping, pong, shutdown and close are socket operations, not messages.
beginPing() enqueues a ping. pingReady() reports that its frame was written; call endPing() from that signal to observe a write error. A received ping is answered by the engine. After a close frame is queued nothing further is written, so beginPing() throws and a received ping is no longer answered. A received pong is reported by pong(). Neither is delivered through incoming(). beginShutdown() enqueues a close frame with a status code and a reason, then keeps reading until the peer replies. shutdownReady() reports the completed handshake; call endShutdown() from that signal to observe an error. A peer that never starts a receive never replies, so setCloseTimeout() bounds that wait. It defaults to ten seconds. Zero leaves the wait unbounded. setPingInterval() writes a keepalive ping after inbound silence. setPingTimeout() bounds that ping and an unanswered beginPing(). Interval, ping and idle default to zero on this type. A received close is answered by the engine. When no local shutdown is outstanding, closed() reports that the protocol has ended. The stream stays bound. Call close() from that signal. An outstanding shutdown completes through shutdownReady(); call endShutdown() and then close(). beginReceive() and beginShutdown() then throw. closeCode() and closeReason() report the handshake that ended the stream. No close frame leaves the code at 1006. close() releases the stream and writes no frame. It does not block and emits nothing. The destructor calls it. Destruction of the stream detaches the channel. The owner deletes this socket.
On the server the application does not construct this type. The server facade is WebSocketSession, which exposes the same message operations without a client handshake.
A server WebSocket is an HTTP service that answers the handshake and a session object that lasts for the life of the upgraded stream. The HTTP server still owns the connection. The service is the factory. The service owns the endpoint policy and the allocator. WebSocketServlet owns the live sessions. On destruction it releases remaining sessions while the factory is still fully constructed. Derive the session to keep the state that must survive from one message to the next: a subscription, a cursor, a user, or a reference into the application domain.
The example is one session type. The constructor starts the receive. The input callback reads the payload. The mapping servlet and the session owner are different objects.
WebSocketService is an HTTP Service, mapped with a Servlet like any other service. Its handshake responder answers a WebSocket Upgrade request with 101 Switching Protocols, with 503 when no WebSocketServlet is attached or the accepted-session limit is already reached, or with 404 when the request is not a WebSocket upgrade. The responder is released before the server opens the stream. It is not the session. addProtocol() names one Sec-WebSocket-Protocol value this endpoint accepts. Several calls name several values, in preference order. An empty list accepts the upgrade and selects no name. A non-empty list selects the first offered name that is also accepted, and a request that offers none of them is answered with 400. setMaxSockets(), setIdleTimeout(), setPingInterval(), setPingTimeout(), setCloseTimeout() and setMaxMessageSize() are endpoint policy of the WebSocketService. They apply before a servlet is constructed. The handshake reads the session limit and socketCount() there. socketCount() is the number of sessions getSession() returned and releaseSession() has not yet released. A missing servlet is still 503 when maxSockets() is zero. The session base copies the idle, ping-interval, ping and close timeouts and the data-message limit onto the connection before the derived constructor runs. It also copies the single Sec-WebSocket-Protocol name from the opening reply, or leaves that name empty when the reply selected none.
After a finished 101 the HTTP server keeps the connection and delivers the upgraded stream to this service. The service asks the attached WebSocketServlet to accept it. The servlet calls getSession() with the stream, the opening request, and the opening reply, and retains a non-null result as a live session. getSession() calls onGetSession() and counts that result. A null return is not counted. Both messages are valid for that call. The session binds the stream in its constructor, which accepts the upgrade. It reads the selected subprotocol from the reply and does not store the reply. A null session leaves the stream unbound, and the HTTP server closes it. No attached servlet is a 503 from the handshake. A server does not mask the frames it writes.
WebSocketSession exposes incoming(), outgoing(), beginSend(), endSend(), beginReceive() and endReceive() directly. onInput() runs when data bytes were received. onOutput() runs when data bytes were sent. A ping or a pong does not run those callbacks. The application learns that the stream has ended from the outstanding receive or shutdown, and calls close() to release the session. close() releases the stream and writes no frame. The destructor calls it. The session does not own the stream or the connection.
WebSocketServlet owns the live sessions of one service. The service owns the endpoint policy and the allocator. Construct the servlet with the service and destroy it before the service. The destructor releases every session it still owns through releaseSession() while the derived service is fully constructed. The derived service destructor has not run yet, so the virtual release call and the allocator are still there. Constructing a second servlet for the same service throws std::logic_error.
BasicWebSocketService is this factory for one session type. Derive WebSocketService when the session type depends on the upgrade, or when the session constructor needs more than the service, the stream, and the opening reply.
getSession() calls onGetSession() and counts a non-null session. releaseSession() calls onReleaseSession() and drops that count. onGetSession() creates the session and onReleaseSession() destroys it. The request argument is the opening request. The reply argument is the opening reply. Both are valid for that call. Copy any header the session must keep. The session does not store the request or the reply. The two methods must use the same allocator, as onGetResponder() and onReleaseResponder() must match. A pool, or any other detach, lives in the derived service. Return null from onGetSession() to decline the upgrade. The servlet releases a session through releaseSession() when close() is called, and from its destructor for every remaining session it owns. The service does not keep a session list. It counts the sessions getSession() returned.
WebSocketSession is the application object of one upgraded stream. Derive from it to keep the state that must survive from one message to the next: a subscription, a cursor, a user, or a reference into the application domain. The HTTP server already owns the connection and the stream. This session formats messages on that stream. It is not a channel bind the application performs, and it is not a request responder. The handshake responder has already been released when the session begins. A server does not mask the frames it writes.
The example starts a receive in the constructor and reads each payload from the input callback. Domain state belongs in the derived session.
The service factory constructs the session with the service, with the stream of this upgrade, and with the opening reply. The reply is valid for the constructor call. The base constructor reads the selected subprotocol from that reply, binds the stream, and copies the service idle, ping-interval, ping and close timeouts and the data-message limit onto the connection. It does not store the reply. The derived constructor runs after that. Its members are initialized and the stream is open. Start the first beginReceive() or beginSend() there. There is no separate accept callback. By the time the derived constructor body runs, the base has already bound the stream.
incoming() and outgoing() are the two payloads. beginSend() writes outgoing(). Call beginSend(false) while more body will be written. beginReceive() reads into incoming(). onInput() runs when data bytes were received. Call endReceive(), read incoming() when progress reports body bytes, and start the next receive if the message is not finished. A ping does not run onInput(). A pong runs onPong() and does not change incoming(). onOutput() runs when data bytes were sent. Call endSend() and continue the send when progress is not finished. onClosed() runs when the engine has ended the protocol and no local shutdown is outstanding. Call close() from onClosed(), or from onShutdownReady() after endShutdown(), to release the session. close() from onClosed() destroys the session before that call returns. The servlet also releases remaining sessions when it is destroyed.
beginReceive() starts frame processing. Until then, incoming frames stay in the stream. Control frames are processed while a receive is outstanding. A local beginShutdown() also keeps input running until the peer replies with its close frame. The two are independent: a receive started before or during a shutdown still delivers what the peer sent before that reply.
protocol() is the single name the responder wrote as Sec-WebSocket-Protocol on the 101, or empty when none was selected. The base copies it from the opening reply before the derived constructor body runs. It is not the list the client offered. A reply that echoes more than one token, or a value that is not one protocol token, fails construction. The server then closes the stream. service() reaches state shared by every connection of this endpoint. The idle, ping-interval, ping and close timeouts and the data-message limit are copied from the WebSocketService before the derived constructor runs. Closing the session closes the stream. The session does not own the stream or the connection. The destructor closes the stream. A derived constructor that throws still runs this destructor, so a failed construction does not leave an accepted stream without an owner.
Ping, shutdown and close stay control operations on this type.
beginPing() enqueues a ping. onPingReady() reports that its frame was written; call endPing() from that callback to observe a write error. A received ping is answered by the engine. After a close frame is queued nothing further is written, so beginPing() throws and a received ping is no longer answered. beginShutdown() enqueues a close frame and keeps reading until the peer replies. A peer that never starts a receive never replies, so the idle timeout copied from the WebSocketService is the only bound on that wait. onShutdownReady() reports the completed handshake; call endShutdown() from that callback to observe an error, then close(). close() releases the stream and writes no frame. The destructor unbinds the stream. beginSend() and beginReceive() then throw. No close frame leaves the code at 1006. closeCode() and closeReason() report the handshake that ended the stream.
WebSocketService is the server-side factory. Map it with an HTTP servlet like any other service, so the handshake request reaches it. This service owns the endpoint policy and the allocator. A separate WebSocketServlet owns the live sessions. Destroy that servlet before this service. Its destruction releases every remaining session while the derived service is still fully constructed. This destructor does not release sessions. By then the derived destructor has already run, and the virtual release call and the allocator are gone. This service keeps a non-owning pointer to its one active servlet.
The example is the usual server setup. The mapping servlet and the session owner are different objects. The setters are endpoint policy of this service. They apply before a servlet is constructed.
The handshake responder answers a WebSocket upgrade with 101, with 503 when no servlet is attached or the accepted-session limit is already reached, or with 404 when the request is not a WebSocket upgrade. The responder is released before the server opens the stream. It is not the session. addProtocol() names one Sec-WebSocket-Protocol value this endpoint accepts. Several calls name several values, in preference order. An empty list accepts the upgrade and selects no name. A non-empty list selects the first offered name that is also accepted, and a request that offers none of them is answered with 400. setMaxSockets(), setIdleTimeout(), setPingInterval(), setPingTimeout(), setCloseTimeout() and setMaxMessageSize() are endpoint policy of this service. They apply before a servlet is constructed. The handshake reads the session limit here. The session base copies the idle, ping-interval, ping and close timeouts and the data-message limit onto the connection before the derived constructor runs. A missing servlet is still 503 when maxSockets() is zero. One service has one active WebSocketServlet. Constructing a second servlet throws std::logic_error.
After a finished 101 the HTTP server keeps the connection and delivers the upgraded stream to this service. This class implements that delivery and asks the servlet to create a session through getSession() with the stream, the opening request, and the opening reply. getSession() calls onGetSession() and counts a non-null session. The session binds the stream in its constructor, which accepts the upgrade. A null session leaves the stream unbound, and the HTTP server closes it. A null return is not counted. No attached servlet is a 503 from the handshake. The null check here covers only the window after a finished 101 and before the stream is delivered.
BasicWebSocketService is this factory for one session type. Derive this class when the session type depends on the upgrade, or when the session constructor needs more than the service, the stream, and the opening reply.
getSession() is the factory call. It calls onGetSession() and counts a non-null session. releaseSession() calls onReleaseSession() and drops that count. onGetSession() creates the session and onReleaseSession() destroys it. The request argument is the opening request. The reply argument is the opening reply. Both are valid for that call. Copy any header the session must keep. The session does not store the request or the reply. The two methods must use the same allocator. A pool, or any other detach, lives in the derived service. Return null from onGetSession() to decline the upgrade. The servlet owns the live sessions only. This service owns the endpoint policy, the allocator, and the count of sessions getSession() returned and releaseSession() has not yet released. This service does not keep a session list. socketCount() is that count.
WebSocketServlet owns the live sessions of one WebSocketService. The service owns the endpoint policy and the allocator, and stays the mapped HTTP service and the factory. Destroy this servlet before the service, so each remaining session is released while the derived service, its virtual release call, and its allocator are still available. It is not a server and does not map a URL. A URL mapping is still required so the handshake reaches the service.
The example registers the servlet before the server starts taking upgrades. The three setters are endpoint policy of the service. They apply before this servlet is constructed. Destroy the servlet before the service.
Construct it with the WebSocketService and destroy it before that service. The constructor attaches this servlet, so the handshake can accept upgrades. The destructor detaches it and releases every remaining session through releaseSession(). That order is required. The service destructor cannot release sessions, because by then the derived object is already gone.
One service has one active servlet. Constructing a second WebSocketServlet for the same service throws std::logic_error. With no servlet, the handshake answers the upgrade with 503 and opens no stream. The service owns setMaxSockets(), setIdleTimeout(), setPingInterval(), setPingTimeout(), setCloseTimeout() and setMaxMessageSize(). The handshake reads the session limit and socketCount() from the service. socketCount() is the number of sessions getSession() returned and releaseSession() has not yet released. The session base copies the idle, ping-interval, ping and close timeouts and the data-message limit from the service onto the connection before the derived constructor runs.