简体中文

开发者工具 · Base64 编码器和解码器

JSON API 中的 Base64:为什么二进制字段要进行编码以及它的成本

· 为什么它很重要

base64 编码

JSON 字段,包含表示为传输而编码的二进制数据的长 Base64 字符串
原始 ToolAcre 矢量图

JSON 没有字节类型,因此二进制数据通常采用 Base64 编码为字符串。这篇文章解释了为什么存在这种约定、它的大小和 CPU 成本以及何时使用单独的二进制端点是更好的调用。

主导响应的 PDF 字段是一个具体的 API 负载,其中一个 Base64 blob 超过了其他所有内容

API 响应包含一个大对象,该对象具有一个主导有效负载大小的字段。响应是 JSON,因此每个值都是字符串或数字。大多数字段都很小:用户 ID、时间戳、状态代码。其中一个字段包含 imageData 或 fileContents,并且是一个 40 千字节的 Base64 字符串。整个响应为 50 KB。该单个字段占传输的 80%,这似乎很浪费,因为服务器最初将其作为字节发送,而客户端最终再次需要字节。

Base64 解决了这个问题: JSON 没有本机字节类型,因此二进制数据必须包装在字符串中。 Base64 将任意字节转换为 JSON 中安全的 ASCII 字符。客户端和服务器都必须在发送时编码并在接收时解码,从而增加了 CPU 开销。生成的有效负载大约比原始字节大三分之一。这篇文章解释了为什么存在该约定、它在实践中的成本以及何时使用单独的二进制端点打破 JSON 约束是值得的。

为什么 JSON 不能携带原始字节 - 字符串必须是有效的 Unicode 文本,因此任意字节都需要文本包装器

JSON 是一种文本格式,其中所有值都必须是有效的 Unicode 文本。该规范定义了字符串、数字、布尔值和 null。它没有字节数组或缓冲区类型。如果 API 需要返回图像、加密签名或文件上传等二进制数据,则它不能直接将原始字节放入 JSON 对象中。这些字节可能包含 JSON 解析器解释为结构标记的字符。二进制 blob 中间的空字节可能会提前终止字符串或破坏解析器。

常见的解决方案是将二进制数据编码为 Base64,生成 JSON 解析器将其视为纯文本的 ASCII 字符字符串。然后,接收客户端将 Base64 解码回字节并使用它们。此编码步骤发生在 API 级别,对大多数开发人员来说是隐藏的,但当 API 返回许多二进制字段时,它是累积的实际成本。 JSON 中 Base64 的成本在整个请求-响应周期中复合。第一个是大小损失:由于编码开销,Base64 输出大约比输入大 33%。

成本:三分之一的字节数、解码时间和内存副本 - 每个成本都出现在典型的客户端中

30 兆字节视频文件在进行 Base64 编码时变为 40 兆字节。下载 40 而不是 30 兆字节会消耗移动设备上的带宽和电池,以及慢速连接的用户时间。第二个成本是 CPU 时间。服务器必须将二进制数据编码为 Base64,然后才能将其字符串化为 JSON。客户端必须解析 JSON ,然后将每个 Base64 字段解码回字节。对于具有多个二进制字段的响应或处理数千个响应的客户端,CPU 时间会累积。

在手机等受限设备上,JavaScript 字符串操作和用于 Base64 解码的 TextDecoder 会消耗电池并降低应用程序速度。第三个成本是内存:JSON 解析器为 Base64 字段创建一个字符串对象,然后解码创建另一个副本作为 Uint8Array。在应用程序可以使用大字段之前,它会在内存中实例化两次。一个有效的例子阐明了成本。假设 API 端点返回包含 100-kilobyte 头像图像的用户配置文件数据。服务器以字节形式从磁盘读取图像,将其编码为 Base64,并将其包含在 JSON 响应中。

工作示例:检查 API 响应中的 Base64 字段 — 在浏览器中对其进行解码以确认服务器实际发送的内容

JSON 响应现在约为 135 千字节(33% 开销加上其他字段)。客户端下载 135 千字节而不是 100。在浏览器中, JSON 解析器为 Base64 数据创建 JavaScript 字符串对象。

当应用程序需要图像时,它会调用 Base64 解码器,该解码器创建原始 100 千字节的 Uint8Array。在解码的几毫秒内,两个对象都存在于内存中。如果页面显示十个带有头像的个人资料,则成本会成倍增加。另一种方法是 API 返回 JSON 响应,并为每个头像资源提供单独的 URL,让浏览器通过其本机缓存、渐进式渲染和内存管理来处理图像下载。

替代方案:多部分、单独的下载 URL 和原始二进制端点 - 每个的权衡

在 JSON 中包含二进制文件和单独获取它之间的权衡取决于 API 目的和使用模式。对于返回数百个微小缩略图的搜索结果页面,将每个缩略图作为单独的请求获取会破坏 HTTP 连接池和缓存。在 JSON 响应中将它们内联为 Base64 可能会更快。对于需要一张或两张高分辨率图像的详细个人资料页面,单独下载显然更好。 API 文档应说明 Base64 字段的最大大小以及客户端何时应期望单独的端点。

如果字段经常超过一两千字节,则内联 Base64 策略表明 API 设计需要重新考虑。 JSON 中存在 Base64 的替代方案,但每种方案都有其优缺点。多部分 MIME 响应将二进制和文本分开,因此二进制部分作为原始字节发送,只有文本部分是 JSON。这需要客户端解析多部分消息,而不是仅仅调用 JSON.parse,从而增加了复杂性。 JSON 响应中的单独下载 URL 指示客户端单独获取二进制资源。

值得在 API 文档中说明的约定 — 标准字母表与 base64url 字母表、填充和最大大小

当二进制资源较大或访问频率低于元数据时,此方法效果很好。仅返回字节并完全放弃 JSON 的原始二进制端点是最简单的方法,但删除了 JSON 提供的结构。一些 API 返回压缩的二进制数据并对其进行 Base64 编码,从而减少了大小损失,但增加了解压缩开销。选择取决于预期的用途:小字段可以很好地内联,大字段属于单独的资源,即使有 Base64 成本,结构化数据也值得保留在 JSON 中。

约定对于互操作性很重要。对二进制数据进行 Base64 编码的 API 应清楚地记录它并说明字母表是标准的还是 URL 安全的。标准 Base64 使用 + 和 /, ,它们在 JSON 字符串中是安全的,但在 URL 中不安全。 URL 安全的 Base64 将它们替换为 - 和 _,这适用于 data: URI,但在 JSON 中进行不必要的转义。文档应指定是否包含或省略填充,因为两者都是有效的 Base64,但期望填充并接收未填充数据的客户端将默默失败或产生垃圾。

这不包括什么 - protobuf、CBOR 和其他二进制序列化格式

对于非常大或经常更新的字段,记录单独的二进制端点至关重要,这样客户端就不会尝试获取数千字节的不必要数据。使用正确的工具,使用 Base64 字段调试 API 非常简单。 Base64 编码器和解码器可让您在浏览器中本地解码任何字段,而无需将其存储或发送到任何地方。从 JSON 响应中复制 Base64 字段,将其粘贴到解码器中并点击解码。对于类似文本的数据(例如 Base64 内的 JSON),解码后的输出会立即出现。

对于图像等二进制数据,十六进制视图显示字节。这有助于确认服务器发送了您所期望的内容,并且您的客户端解码器工作正常。如果字段解码为意外数据,则问题出在服务器编码或复制字段的方式上。如果它解码为部分 blob,则该字段可能已被截断或 Base64 长度可能错误。与将字段写入文件并打开外部工具相比,本地解码可加快调试速度。

要点:JSON 中的 Base64 是一种折衷方案,因此请记录下来 — Base64 编码器和解码器如何帮助您在本地检查和验证编码字段

API 中 Base64 的实用方法是认识而不是回避。 Base64 是在 JSON 中传输二进制数据的标准方式,并且它可以工作。了解每个 Base64 字段的大小要多花费三分之一,并且每个请求-响应周期要花费几毫秒的 CPU 时间。对于像身份验证令牌这样的小型关键元数据(其中 JWT 本身是 Base64 编码的),成本可以忽略不计。对于大型附件,请询问二进制文件是否应在同一响应中传输或作为单独的资源传输。

在 API 规范中记录编码方案和最大大小。检查响应时,使用 Base64 编码器和解码器来验证字段解码正确并了解服务器实际发送的内容。这种纪律使权衡显而易见,并且决策是经过深思熟虑的而不是偶然的。