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. A client obtains the stream it already owns with Client::upgrade(). Binding a StreamSession 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.
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. Binding a StreamSession 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 is the listening HTTP service in the server model. It binds a local Endpoint and accepts connections, but it does not implement a resource. Incoming requests are mapped by servlets that have been added with addServlet(). The first servlet that maps the request handles it. removeServlet() unregisters a servlet.
All server I/O is asynchronous, so the server needs an EventLoop, passed to a constructor or to setActive(). The loop does not own the server. listen() binds the local endpoint, optionally with TcpServerOptions, and a second listen replaces the previous binding. cancel() stops accepting and pending work.
setSecure() assigns a Pt::Ssl::Context so the server accepts only HTTPS connections. Further use of servlets and responders is unchanged. Certificate and handshake details live in the SSL module.
setMaxThreads() bounds worker threads. setTimeout() and setKeepAliveTimeout() bound idle I/O and persistent connections, and setMaxRequestSize() rejects a request whose size exceeds that limit.
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 responder has already been released. Binding a StreamSession 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. 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(). 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.
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.
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. A finished 101 becomes the stream this socket formats. endConnect() completes the handshake and throws if it failed. After that success the socket no longer uses the client for messages. A client masks every frame it writes.
closed() is emitted while this socket is still alive. The stream has already cleared its session pointer. Peer close, an I/O error, a close frame and destruction of the stream all emit it. It ends an outstanding send or receive. The owner deletes this socket. setTimeout() bounds I/O after the upgrade. The handshake still uses the timeout of the client.
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 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. 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. 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(). 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. After finished(), clear() drops the body so the same object can carry the next message.
Ping, pong and close are socket operations, not messages.
ping() enqueues a ping. A received ping is answered by the engine. A received pong is consumed. Neither is delivered through incoming(). close() enqueues a close frame with a status code and a reason. A received close is answered by the engine. closeCode() and closeReason() report the handshake that ended the stream. closed() is emitted while this socket is still alive. The stream has already cleared its session pointer. Peer close, an I/O error, a close frame and destruction of the stream all emit it. It ends an outstanding send or receive. 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 and the endpoint policy. It does not keep the sessions it creates. A separate release scope does, and that scope is destroyed before the service so the factory is still fully constructed when the last session is released. 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 release scope 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 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. setMaxSockets(), setIdleTimeout() and setMaxMessageSize() are endpoint policy. The handshake reads the session limit. The session base copies the idle timeout and the data-message limit onto the connection before the derived constructor runs.
After a finished 101 the HTTP server keeps the connection and delivers the upgraded stream to this service. The service implements that delivery and does not pass it on. It forwards the stream to the registered WebSocketServlet. The servlet asks the factory for a WebSocketSession and passes the EventLoop of the stream together with the stream. The session binds the stream in its constructor, which accepts the upgrade. A null session, or no registered servlet, leaves the stream unbound, and the HTTP server closes it. 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. onClose() is the last look at the session. Do not call endReceive() or endSend() from onClose(). Closing the session closes the stream. The session does not own the stream or the connection.
WebSocketServlet holds the session pointers. Construct it with the service and destroy it before the service. The constructor registers this servlet, so the next upgrade of that service is delivered here. The destructor releases every session it still holds while the service is still fully constructed. The derived service destructor has not run yet, so the virtual release call and the allocator are still there. That order is required. One service has one registered servlet. A second servlet replaces the registration. The previous servlet keeps the sessions it already accepted and releases them itself.
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 servlet, the loop and the stream.
onGetSession() creates the session and onReleaseSession() destroys it. 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 onReleaseSession() after onClose() returns, and from its destructor for every session it still holds.
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 stream-session 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 servlet constructs the session with itself, with the loop that serializes this stream, and with the stream of this upgrade. The loop must be the loop of the stream. The base constructor binds the stream, copies the service limits onto the connection, and stores the loop. The derived constructor runs after that. Its members are initialized, the stream is open, and loop() is the loop of this stream. 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(). 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 or a pong does not run onInput(). onOutput() runs when data bytes were sent. Call endSend() and continue the send when progress is not finished. onClose() runs while this object is still alive, after the stream has ended. Do not call endReceive() or endSend() from onClose(). The servlet that holds this session releases it after onClose() returns.
service() reaches state shared by every connection of this endpoint. loop() is where a timer or posted work must run. 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 and close stay control operations on this type.
ping() enqueues a ping. close() enqueues a close frame. 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 type does not keep the sessions it creates. A separate release scope does, and that scope is destroyed before this service so the factory is still fully constructed when the last session is released. The service destructor does not release sessions. By then the derived destructor has already run, and the virtual release call and the allocator are gone.
The example is the usual server setup. The mapping servlet and the release scope are different objects.
The handshake responder answers a WebSocket upgrade with 101, with 503 when 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. setMaxSockets(), setIdleTimeout() and setMaxMessageSize() are endpoint policy. The handshake reads the session limit. The session base copies the idle timeout and the data-message limit onto the connection before the derived constructor runs. One service has one registered WebSocketServlet. A second registration replaces it. The previous servlet keeps the sessions it already holds and releases them itself.
After a finished 101 the HTTP server keeps the connection and delivers the upgraded stream to this service. This class implements that delivery and does not pass it on. It forwards the stream to the registered servlet. The servlet asks the factory for a session and passes the loop of the stream together with the stream. The session binds the stream in its constructor, which accepts the upgrade. A null session, or no registered servlet, leaves the stream unbound, and the HTTP server closes it.
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 servlet, the loop and the stream.
onGetSession() creates the session and onReleaseSession() destroys it. 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.
WebSocketServlet holds the sessions a WebSocket service creates and releases them while that service is still fully constructed. It is not a server. The HTTP server already owns the connection and the stream. It is not a request-mapping servlet. This type does not map a URL. A URL mapping is still required so the handshake reaches the service. The service is the factory and the endpoint policy. This type asks the factory for a session, holds the session pointer, and releases it. It does not decide the session type, the allocator, or the domain teardown.
The example registers the scope before the server starts taking upgrades. Destroy the servlet before the service.
Construct it with the WebSocketService and destroy it before that service. The constructor registers this servlet, so the next upgrade of that service is delivered here. The destructor releases every session it still holds while the derived service is still fully constructed, so the virtual release call and the allocator are still there. That order is required. The service destructor cannot release the sessions, because by then the derived object is already gone.
One service has one registered servlet. A second servlet replaces the registration. The previous servlet keeps the sessions it already accepted and releases them itself. No registration, which is the state before the first servlet and after the last destructor, declines the next upgrade. The HTTP server closes a stream that has no bound session. When a stream ends, the session close callback runs and this servlet releases that session.