一个基于 SwiftNIO 的 WebSocket 压缩库
WebSocket 压缩,由 RFC7692 定义,允许 WebSocket 客户端在 WebSocket 连接上发送和接收压缩数据。压缩减少了 WebSocket 连接的总线级负载,可能从而提高吞吐量。
本文档讨论了使用 SwiftNIO 在 Kitura-WebSocket-Compression API 中 WebSocket 压缩的实现。
本文档假定读者了解 WebSocket 协议 的基本原理。
将 Kitura-WebSocket-Compression
包添加到应用程序的 Package.swift
文件中的依赖项中。将 "x.x.x"
替换为最新的 Kitura-WebSocket-Compression
版本。
.package(url: "https://github.com/IBM-Swift/Kitura-WebSocket-Compression.git", from: "x.x.x")
将 Kitura-WebSocket-Compression
添加到您的目标的依赖项中
.target(name: "example", dependencies: ["WebSocketCompression"]),
import WebSocketCompression
WebSocket 协议为服务器配置协议扩展以及客户端从服务器请求这些扩展提供了规定。 客户端通过使用 Sec-WebSocket-Extension
标头的 协商提议
来通知它感兴趣的扩展。服务器可以支持或不支持客户端请求的扩展。 通过协商响应
,服务器将服务器同意的扩展通知客户端。 协商提议和响应还可以包括特定于扩展的参数。 一旦达成扩展协议,客户端和服务器必须从各自的 WebSocket 实现中调用该扩展。
WebSocket 压缩是一种 WebSocket 扩展。
Permessage-deflate 是由 RFC7692 定义的 WebSocket 扩展,它为压缩功能提供了规范。 它定义了协商过程和一种名为 DEFLATE
的压缩算法。 像任何 WebSocket 扩展一样,permessage-deflate 协商包括提议和响应。
在从 HTTP 升级到 WebSocket 的请求期间会发生 permessage-deflate
协商。 permessage-deflate
协商提议具有强制性的 permessage-deflate
字符串,后跟一个以分号分隔的扩展参数列表。 为 WebSocket 压缩定义了四个扩展参数
client_no_context_takeover
,server_no_context_takeover
client_max_window_bits
,server_max_window_bits
我们将在后面的章节中重新讨论这些参数,届时我们将讨论它们的用途和影响。
permessage-deflate 协商响应具有强制性的 permessage-deflate
字符串,后跟一个以分号分隔的扩展参数列表,该列表由服务器达成一致。 协商响应中的标头是关于客户端和服务器之间如何进行数据压缩/解压缩的最终决定。 客户端压缩的数据必须由服务器解压缩,反之亦然。 客户端和服务器必须采用相同的压缩/解压缩配置参数。 我们将在后面的章节中详细介绍这一点。
该规范还讨论了 DEFLATE 算法。 我们利用 zlib 压缩库 进行原始压缩和解压缩。 必须在连接的两端设置一个由压缩器和解压缩器组成的对。 服务器的解压缩器解压缩由客户端的压缩器压缩的消息,反之亦然。
SwiftNIO 框架提供了一个 API,该 API 使 HTTP/WebSocket 服务器实现能够将已从套接字读取或写入到套接字的数据的处理视为通过处理程序管道发生的一系列转换。 活动连接由 Channel
表示。 从通道读取或写入到通道的数据通过入站和出站 ChannelHandlers
的 ChannelPipeline
移动。 一个 EventLoop
与每个 Channel
相关联。 EventLoop
是一个线程安全的线程抽象,并提供了使用 EventLoopFutures
和 EventLoopPromises
执行异步代码的功能。
在 Kitura-NIO 中,我们使用 SwiftNIO 配置的管道启动 HTTP 服务器,并在最后添加 Kitura-NIO 的 HTTPRequestHandler
。 入站和出站管道的视图(为简单起见省略了一些处理程序)是这样的
入站通道处理程序管道
(操作系统)
|
NIOSSLServerHandler
|
WebSocketFrameDecoder
|
PermessageDeflateDecompressor
|
WebSocketConnection
|
(Kitura/WebSocket 应用程序)
出站通道处理程序管道
(Kitura/WebSocket 应用程序)
|
PermessageDeflaterCompressor
|
WebSocketFrameEncoder
|
NIOSSLServerHandler
|
(操作系统)
HTTPDecoder 和 HTTPResponseEncoder 分别将字节转换为 HTTP 请求,并将响应转换为字节。 NIOSSLServerHandler 是一个双工处理程序(既是入站又是出站),用于在安全连接上解密和加密数据。 HTTPRequestHandler 用于调用 Kitura 的路由器。
升级到 WebSocket 会导致 SwiftNIO 以以下方式更改上述管道
HTTPDecoder
和 HTTPResponseEncoder
(以及其他 HTTP 相关处理程序)此外,Kitura-WebSocket-NIO
对管道进行了以下更改
WebSocketConnection
,一个用于处理接收到的 WebSocket 消息的入站处理程序permessage-deflate
协商通过,则添加通道处理程序 WebSocketCompressor
和 WebSocketDecompressor
,它们分别使用 PermessageDeflateCompressor
和 PermessageDeflateDeCompressor
。管道现在看起来像
入站管道
(操作系统)
|
NIOSSLServerHandler
|
WebSocketFrameDecoder
|
PermessageDeflateDecompressor
|
WebSocketConnection
|
(Kitura/WebSocket 应用程序)
出站管道
(Kitura/WebSocket 应用程序)
|
PermessageDeflaterCompressor
|
WebSocketFrameEncoder
|
NIOSSLServerHandler
|
(操作系统)
WebSocketCompressor 是一个出站处理程序,用于压缩出站 WebSocket 消息。 WebSocketDecompressor 是一个入站处理程序,用于解压缩入站 WebSocket 消息。 每个协商了压缩的 WebSocket 连接都获得自己的 (WebSocketCompressor
, WebSocketDecompressor
) 对。 这些处理程序目前使用 permessage-deflate
进行压缩。
通过此设置,所有入站数据首先通过 SwiftNIO 的 WebSocketDecoder,在其中构建 WebSocket 帧。 然后,它移动到 WebSocketDecompressor
,在其中累积包含消息的多个帧,并使用 zlib
的 inflater 解压缩。 随后,解压缩的消息被移动到 WebSocketConnection
处理程序。
出站 WebSocket 帧首先到达 WebSocketCompressor
,后者压缩其中保存的数据,并将它们中继到 WebSocketEncoder。 在这里,帧被编组为原始字节,以便在加密后写入线路。
压缩器称为 WebSocketCompressor
。 它是 ChannelOutboundHandler。
当先前的出站处理程序 (WebSocketConnection
) 将数据写入通道时,将调用此处实现的 ChannelOutboundHandler
的 write(context:data:promise)
方法。 在这里,仅处理数据帧和延续帧。 WebSocket 消息既可以作为单个数据帧提供,也可以作为数据帧后跟一系列延续帧提供。
压缩器确保我们已累积与消息相关的所有数据。 随后,调用 deflater,并将压缩后的数据打包到新的 WebSocketFrame 中,该帧传递给 WebSocketFrameEncoder
。
该库当前实现 zlib
的 deflater PermessageDeflateCompressor
,它作为参数传递给 WebSocketCompressor
以进行压缩
解压缩器在功能上是压缩器的镜像。 它被称为 WebSocketDeCompressor
并且是一个 ChannelInboundHandler。
每当 WebSocketDecoder
生成新的 WebSocketFrame
时,都会调用此处实现的 ChannelInboundHandler
的 channelRead(context:data)
方法。 与压缩器类似,解压缩器仅处理数据帧和延续帧。 与消息相关的所有数据(可能分布在延续帧中)都会被累积,并且会调用 inflater。 然后将解压缩的数据打包到新的 WebSocketFrame
中,并移动到管道中的下一个处理程序中。
该库当前实现 zlib
的 inflater PermessageDeflateDeCompressor
,它作为参数传递给 WebSocketDeCompressor
以进行解压缩
RFC7692 定义了四个配置选项。 它们实际上是两对选项,每对分别针对客户端和服务器
这些参数允许客户端和服务器在每条消息上使用新的 zlib inflater 或 deflater。 默认情况下,我们会跨消息重复使用 inflater 和 deflater 实例。 这意味着 inflater 和 deflater 只初始化一次。 内存的分配/释放也只发生一次,并且 deflate/inflate 流的历史记录可以重复使用。 此处是一个示例。
在 Kitura-WebSocket-NIO
实现中
client_no_context_takeover
扩展参数来通知服务器它不使用上下文接管。 服务器将响应相同的 client_no_context_takeover
参数,并将其解压缩器配置为不使用上下文接管。server_no_context_takeover
扩展参数。 服务器将遵循此请求,并将其压缩器配置为不使用上下文接管。 它将在响应中添加 server_no_context_takeover
参数。这些参数允许客户端和服务器共享 LZ77 滑动窗口大小。 默认值为 15 位,表示窗口大小为 32768 (2^15)。 客户端的压缩器和服务器的解压缩器必须具有相同的 LZ77 窗口大小。 服务器的压缩器和客户端的解压缩器也适用相同的限制。
在 Kitura-WebSocket-NIO
实现中
client_max_window_bits
扩展参数来通知服务器其压缩器的 LZ77 窗口大小。 如果该参数具有有效值,服务器将相应地配置其解压缩器,并在响应中发送相同的扩展参数,以表示达成一致。server_max_window_bits
扩展参数来请求服务器使用特定的 LZ77 窗口大小。 如果该值有效,服务器将相应地配置其压缩器,并在响应中发送相同的扩展参数,以表示达成一致。PermessageDeflateCompressor
和 PermessageDeflaterDecompressor
都会将多帧消息合并为单帧。 这种帧信息丢失在典型用例中可能并不严重。 但是,在某些应用中,可能需要维护帧信息。
正如 此处 的一个示例中提到的,如果协商失败,客户端可以提供回退协商提议。 Kitura-WebSocket-NIO
尚未实现此功能。 我们确保每个提议都能通过。
LZ77 滑动窗口值必须作为负参数传递给 deflateInit2()/inflateInit2()。 这告知 zlib
我们使用原始 deflate 流(而不是具有滑动窗口正值的 zlib 流)。 请参见 此处。
存在一个关于滑动窗口大小为 8 位的 zlib bug。 请参见 此处。 针对 zlib 流有一个解决方法。 我们为原始 deflate 流实现了一个类似的解决方法,此处。
客户端可以协商压缩,但发送未压缩的帧。 为了处理这种情况,在解压缩之前,我们会检查第一帧的 RSV1 位,以确保它属于压缩消息。
SwiftNIO 提供了一个 ChannelDuplexHandler 类型,用于同时属于入站和出站管道的通道处理程序。 WebSocketCompressor
和 WebSocketDecompressor
可以合并为一个 ChannelDuplexHandler
。