Kitura

Build Status - Master macOS Linux Apache 2 Slack Status

Kitura-WebSocket-Compression

一个基于 SwiftNIO 的 WebSocket 压缩库

WebSocket 压缩

WebSocket 压缩,由 RFC7692 定义,允许 WebSocket 客户端在 WebSocket 连接上发送和接收压缩数据。压缩减少了 WebSocket 连接的总线级负载,可能从而提高吞吐量。

本文档讨论了使用 SwiftNIOKitura-WebSocket-Compression API 中 WebSocket 压缩的实现。

本文档假定读者了解 WebSocket 协议 的基本原理。

目录

  1. 用法
  2. WebSocket 扩展
  3. WebSocket 压缩 - permessage-deflate 算法
  4. 基于 SwiftNIO 的 permessage-deflate 实现
  5. 开发者注意事项

1. 用法

添加依赖项

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

2. WebSocket 扩展

WebSocket 协议为服务器配置协议扩展以及客户端从服务器请求这些扩展提供了规定。 客户端通过使用 Sec-WebSocket-Extension 标头的 协商提议 来通知它感兴趣的扩展。服务器可以支持或不支持客户端请求的扩展。 通过协商响应,服务器将服务器同意的扩展通知客户端。 协商提议和响应还可以包括特定于扩展的参数。 一旦达成扩展协议,客户端和服务器必须从各自的 WebSocket 实现中调用该扩展。

WebSocket 压缩是一种 WebSocket 扩展。

3. WebSocket 压缩:permessage-deflate 算法

Permessage-deflate 是由 RFC7692 定义的 WebSocket 扩展,它为压缩功能提供了规范。 它定义了协商过程和一种名为 DEFLATE 的压缩算法。 像任何 WebSocket 扩展一样,permessage-deflate 协商包括提议和响应。

permessage-deflate 协商提议

在从 HTTP 升级到 WebSocket 的请求期间会发生 permessage-deflate 协商。 permessage-deflate 协商提议具有强制性的 permessage-deflate 字符串,后跟一个以分号分隔的扩展参数列表。 为 WebSocket 压缩定义了四个扩展参数

我们将在后面的章节中重新讨论这些参数,届时我们将讨论它们的用途和影响。

permessage-deflate 协商响应

permessage-deflate 协商响应具有强制性的 permessage-deflate 字符串,后跟一个以分号分隔的扩展参数列表,该列表由服务器达成一致。 协商响应中的标头是关于客户端和服务器之间如何进行数据压缩/解压缩的最终决定。 客户端压缩的数据必须由服务器解压缩,反之亦然。 客户端和服务器必须采用相同的压缩/解压缩配置参数。 我们将在后面的章节中详细介绍这一点。

该规范还讨论了 DEFLATE 算法。 我们利用 zlib 压缩库 进行原始压缩和解压缩。 必须在连接的两端设置一个由压缩器和解压缩器组成的对。 服务器的解压缩器解压缩由客户端的压缩器压缩的消息,反之亦然。

4. 基于 SwiftNIO 的 permessage-deflate 实现

SwiftNIO 框架提供了一个 API,该 API 使 HTTP/WebSocket 服务器实现能够将已从套接字读取或写入到套接字的数据的处理视为通过处理程序管道发生的一系列转换。 活动连接由 Channel 表示。 从通道读取或写入到通道的数据通过入站和出站 ChannelHandlersChannelPipeline 移动。 一个 EventLoop 与每个 Channel 相关联。 EventLoop 是一个线程安全的线程抽象,并提供了使用 EventLoopFuturesEventLoopPromises 执行异步代码的功能。

Kitura-NIO 中,我们使用 SwiftNIO 配置的管道启动 HTTP 服务器,并在最后添加 Kitura-NIO 的 HTTPRequestHandler。 入站和出站管道的视图(为简单起见省略了一些处理程序)是这样的

HTTPDecoderHTTPResponseEncoder 分别将字节转换为 HTTP 请求,并将响应转换为字节。 NIOSSLServerHandler 是一个双工处理程序(既是入站又是出站),用于在安全连接上解密和加密数据。 HTTPRequestHandler 用于调用 Kitura 的路由器。

升级到 WebSocket 会导致 SwiftNIO 以以下方式更改上述管道

此外,Kitura-WebSocket-NIO 对管道进行了以下更改

管道现在看起来像

WebSocketCompressor 是一个出站处理程序,用于压缩出站 WebSocket 消息。 WebSocketDecompressor 是一个入站处理程序,用于解压缩入站 WebSocket 消息。 每个协商了压缩的 WebSocket 连接都获得自己的 (WebSocketCompressor, WebSocketDecompressor) 对。 这些处理程序目前使用 permessage-deflate 进行压缩。

通过此设置,所有入站数据首先通过 SwiftNIO 的 WebSocketDecoder,在其中构建 WebSocket 帧。 然后,它移动到 WebSocketDecompressor,在其中累积包含消息的多个帧,并使用 zlib 的 inflater 解压缩。 随后,解压缩的消息被移动到 WebSocketConnection 处理程序。

出站 WebSocket 帧首先到达 WebSocketCompressor,后者压缩其中保存的数据,并将它们中继到 WebSocketEncoder。 在这里,帧被编组为原始字节,以便在加密后写入线路。

4.1 压缩器实现

压缩器称为 WebSocketCompressor。 它是 ChannelOutboundHandler

当先前的出站处理程序 (WebSocketConnection) 将数据写入通道时,将调用此处实现的 ChannelOutboundHandlerwrite(context:data:promise) 方法。 在这里,仅处理数据帧和延续帧。 WebSocket 消息既可以作为单个数据帧提供,也可以作为数据帧后跟一系列延续帧提供。

压缩器确保我们已累积与消息相关的所有数据。 随后,调用 deflater,并将压缩后的数据打包到新的 WebSocketFrame 中,该帧传递给 WebSocketFrameEncoder

该库当前实现 zlib 的 deflater PermessageDeflateCompressor,它作为参数传递给 WebSocketCompressor 以进行压缩

4.2 解压缩器实现

解压缩器在功能上是压缩器的镜像。 它被称为 WebSocketDeCompressor 并且是一个 ChannelInboundHandler

每当 WebSocketDecoder 生成新的 WebSocketFrame 时,都会调用此处实现的 ChannelInboundHandlerchannelRead(context:data) 方法。 与压缩器类似,解压缩器仅处理数据帧和延续帧。 与消息相关的所有数据(可能分布在延续帧中)都会被累积,并且会调用 inflater。 然后将解压缩的数据打包到新的 WebSocketFrame 中,并移动到管道中的下一个处理程序中。

该库当前实现 zlib 的 inflater PermessageDeflateDeCompressor,它作为参数传递给 WebSocketDeCompressor 以进行解压缩

4.3 配置压缩器和解压缩器

RFC7692 定义了四个配置选项。 它们实际上是两对选项,每对分别针对客户端和服务器

4.3.1 client_no_context_takeoverserver_no_context_takeover

这些参数允许客户端和服务器在每条消息上使用新的 zlib inflater 或 deflater。 默认情况下,我们会跨消息重复使用 inflater 和 deflater 实例。 这意味着 inflater 和 deflater 只初始化一次。 内存的分配/释放也只发生一次,并且 deflate/inflate 流的历史记录可以重复使用。 此处是一个示例。

Kitura-WebSocket-NIO 实现中

4.3.2 client_max_window_bitsserver_max_window_bits

这些参数允许客户端和服务器共享 LZ77 滑动窗口大小。 默认值为 15 位,表示窗口大小为 32768 (2^15)。 客户端的压缩器和服务器的解压缩器必须具有相同的 LZ77 窗口大小。 服务器的压缩器和客户端的解压缩器也适用相同的限制。

Kitura-WebSocket-NIO 实现中

5. 开发者说明

  1. PermessageDeflateCompressorPermessageDeflaterDecompressor 都会将多帧消息合并为单帧。 这种帧信息丢失在典型用例中可能并不严重。 但是,在某些应用中,可能需要维护帧信息。

  2. 正如 此处 的一个示例中提到的,如果协商失败,客户端可以提供回退协商提议。 Kitura-WebSocket-NIO 尚未实现此功能。 我们确保每个提议都能通过。

  3. LZ77 滑动窗口值必须作为负参数传递给 deflateInit2()/inflateInit2()。 这告知 zlib 我们使用原始 deflate 流(而不是具有滑动窗口正值的 zlib 流)。 请参见 此处

  4. 存在一个关于滑动窗口大小为 8 位的 zlib bug。 请参见 此处。 针对 zlib 流有一个解决方法。 我们为原始 deflate 流实现了一个类似的解决方法,此处

  5. 客户端可以协商压缩,但发送未压缩的帧。 为了处理这种情况,在解压缩之前,我们会检查第一帧的 RSV1 位,以确保它属于压缩消息。

  6. SwiftNIO 提供了一个 ChannelDuplexHandler 类型,用于同时属于入站和出站管道的通道处理程序。 WebSocketCompressorWebSocketDecompressor 可以合并为一个 ChannelDuplexHandler