A WebSocket compression library based on SwiftNIO
WebSocket Compression, defined by RFC7692 allows WebSocket clients to send and receive compressed data on a WebSocket connection. Compression reduces the total wire-level payload of a WebSocket connection, possibly resulting in an improved throughput.
This document assumes the reader is aware of the fundamentals of the WebSocket protocol.
Table of contents
- WebSocket extensions
- WebSocket compression - the
- An implementation of permessage-deflate based on SwiftNIO
- Developer notes
Kitura-WebSocket-Compression package to the dependencies within your application’s
Package.swift file. Substitute
"x.x.x" with the latest
.package(url: "https://github.com/Kitura-Next/Kitura-WebSocket-Compression.git", from: "x.x.x")
Kitura-WebSocket-Compression to your target's dependencies:
.target(name: "example", dependencies: ["WebSocketCompression"]),
2. WebSocket extensions
The WebSocket protocol has a provision for servers to configure protocol extensions, and for clients to request these extensions from the servers. A client notifies about extensions it is interested in through a
negotiation offer using the
Sec-WebSocket-Extension header. A server may or may not support the extensions requested by the client. Through a
negotiation response, the server notifies the client of the extensions that the server agrees upon. Negotiation offers and responses may also include extension-specific parameters. Once an extension is agreed upon, the client and server must invoke the extension from their respective WebSocket implementations.
WebSocket compression is a WebSocket extension.
3. WebSocket Compression: the permessage-deflate algorithm
Permessage-deflate is a WebSocket extension defined by RFC7692 which provides a specification for the compression functionality. It defines the negotiation process and a compression algorithm called
DEFLATE. Like any WebSocket extension, the permessage-deflate negotiation comprises of an offer and a response.
The permessage-deflate negotiation offer
permessage-deflate negotiation happens during the upgrade request from HTTP to WebSocket. A
permessage-deflate negotiation offer has a mandatory
permessage-deflate string followed by a semi-colon separated list of extension parameters. There are four extension parameters defined for WebSocket compression:
We will revisit these parameters in a later section, where we will discuss their use and effects.
The permessage-deflate negotiation response
A permessage-deflate negotiation response has a mandatory
permessage-deflate string followed by a semi-colon separated list of extension parameters, agreed upon by the server. The headers in the negotiation response are the final word on how the data compression/decompression will be done between the client and server. Data compressed by the client must be decompressed by the server and vice versa. The client and server must adopt the same compression/decompression configuration parameters. We will take a detailed look at this in the later sections.
The specification also discusses the DEFLATE algorithm. We utilize the zlib compression library for doing raw compression and decompression. A pair, comprising of a compressor and a decompressor, must be set up at both the ends of the connection. The server's decompressor decompresses messages compressed by the client's compressor and vice versa.
4. An implementation of permessage-deflate based on SwiftNIO
The SwiftNIO framework provides an API which enables HTTP/WebSocket server implementations to view the processing of data, that has been read from or written to sockets, as a sequence of transformations that happen through a pipeline of handlers. An active connection is represented by a
Channel. Data which is read from, or written to, a channel moves through a
ChannelPipeline of inbound and outbound
EventLoop is associated with every
EventLoop is a thread-safe abstraction of a thread and provides features for asynchronous code execution using
In Kitura-NIO, we start the HTTP server with the pipeline configured by SwiftNIO, adding Kitura-NIO's
HTTPRequestHandler at the end. A view of the inbound and outbound pipelines (with some handlers omitted for simplicity) is this:
Inbound channel handler pipeline:
Outbound channel handler pipeline:
HTTPDecoder and HTTPResponseEncoder convert bytes to HTTP requests, and responses to bytes, respectively. NIOSSLServerHandler is a duplex handler (both inbound and outbound) used to decrypt and encrypt data on a secure connection. The HTTPRequestHandler is used to invoke Kitura's router.
An upgrade to WebSocket causes SwiftNIO to alter the above pipeline in these ways:
HTTPResponseEncoder(and other HTTP related handlers) are removed from the pipeline
- an inbound handler WebSocketFrameDecoder is added. It convert raw bytes, received on the wire, to WebSocket frames.
- an outbound handler WebSocketFrameEncoder is added to convert WebSocket frames to raw bytes to be sent on the wire.
Kitura-WebSocket-NIO makes the following changes to the pipeline:
WebSocketConnection, an inbound handler to process received WebSocket messages
- if the
permessage-deflatenegotiation goes through, channel handlers
PermessageDeflateDeCompressorrespectively are added.
The pipelines now look like:
WebSocketCompressor is an outbound handler used to compress outbound WebSocket messages. WebSocketDecompressor is an inbound handler used to decompress inbound WebSocket messages. Every WebSocket connection where a compression was negotiated, gets its own (
These handlers currently use
permessage-deflate for compression.
With this setup, all the inbound data first passes through SwiftNIO's WebSocketDecoder where the WebSocket frames are built. It then moves into the
WebSocketDecompressor where multiple frames comprising a message are accumulated and decompressed using
zlib's inflater. Subsequently, the decompressed messages are moved to the
Outbound WebSocket frames first reach the
WebSocketCompressor which compresses the data held within them and relays them to the WebSocketEncoder. Here the frames are marshalled into raw bytes to be written to the wire after encryption.
4.1 Compressor implementation
The compressor is called
WebSocketCompressor. It is a ChannelOutboundHandler.
write(context:data:promise) method implemented here gets invoked when the previous outbound handler (
WebSocketConnection) writes data to the channel. Here, only data frames and continuation frames are processed. A WebSocket message is either available in a single data frame or a data frame followed by sequence of continuation frames.
The compressor makes sure that we have all the data pertaining to a message accumulated. Subsequently, the deflater is invoked and deflated data is packed into a new WebSocketFrame, which is passed to the
This library currently implements
PermessageDeflateCompressor which is passed as an argument to
WebSocketCompressor for compression
4.2 Decompressor implementation
The decompressor is, functionally, a mirror image of the compressor. It is called
WebSocketDeCompressor and is a ChannelInboundHandler.
channelRead(context:data) method implemented here is invoked whenever a new
WebSocketFrame is produced by the
WebSocketDecoder. Similar to the compressor, the decompressor processes data and continuation frames only. All the data pertaining to a message, possibly spread across continuation frames, is accumulated and the inflater is invoked. The inflated data is then packed into a new
WebSocketFrame and moved into the next handler in the pipeline.
This library currently implements
PermessageDeflateDeCompressor which is passed as an argument to
WebSocketDeCompressor for decompression
4.3 Configuring the Compressor and Decompressor
RFC7692 defines four configuration options. They are actually two pairs of option, one each for the client and the server:
These allow the client and server to use a new zlib inflater or deflater on every message. By default we reuse the inflater and deflater instances across messages. This means the inflater and deflater are initialized only once. The memory allocation/deallocation happens only once and the history of the deflate/inflate stream can be reused. Here is an example.
- a client can inform the server that it isn't using context takeover by sending the
client_no_context_takeoverextension parameter. The server will respond with the same
client_no_context_takeoverparameter and configure its decompressor to not use context takeover.
- a client can request the server to not use context takeover by sending the
server_no_context_takeoverextension parameter. The server will regard this request and configure its compressor to not use context takeover. It will add the
server_no_context_takeoverparameter in the response.
- by default the server will configure its compressor and decompressor to use context takeover i.e. to reuse compression context
These allow the client and server to share the LZ77 sliding window size. The default value is 15 bits which represents a window size of 32768 (2^15). The client's compressor and the server's decompressor must have the same LZ77 window size. The same restriction applies to the server's compressor and the client's decompressor.
- a client can inform the server of its compressor's LZ77 window size using the
client_max_window_bitsextension parameter. If the parameter has a valid value, the server will configure its decompressor accordingly and send the same extension parameter in the response, indicating an agreement.
- a client can also request the server to use a particular LZ77 window size using the
server_max_window_bitsextension parameter. If the value is valid, the server will configure its compressor accordingly and send the same extension parameter in the response, indicating an agreement.
5. Developer notes
PermessageDeflaterDecompressorboth consolidate multi-frame messages into a single frame. This loss of framing information may not be serious in typical use-cases. But there may be applications where framing information has to be maintained.
As mentioned in one of the examples here, a client may supply fallback negotiation offers, in case negotiation fails.
Kitura-WebSocket-NIOhasn't implemented this. We make sure every offer goes through.
The LZ77 sliding window value must be passed as a negative parameter to deflateInit2()/inflateInit2(). This informs
zlibthat we use raw deflate streams (as against zlib streams that result with sliding window positive values). See this.
Clients may negotiate for compression but send uncompressed frames. To handle this, just before decompressing, we check the RSV1 bit of the first frame to make sure it belongs to a compressed message.
SwiftNIO offers a ChannelDuplexHandler type for channel handlers that are a part of both, the inbound and outbound pipelines. The
WebSocketDecompressorcould be merged into a single