在你使用 Java 编写软件时实现持久化配置。当你编写一个应用时，你通常都会希望用户能够定制化他们和应用交互的方式，以及应用与系统进行交互的方式。这种方式通常被称为 “ 偏好 preference ” 或者 “ 设置 setting ”，它们被保存在一个 “偏好文件” 或者 “配置文件” 中，有时也直接简称为 “ 配置 config ”。配置文件可以有很多种格式，包括 INI、JSON、YAML 和 XML。每一种编程语言解析这些格式的方式都不同。本文主要讨论，当你在使用 Java 编程语言 来编写软件时，实现持久化配置的方式。选择一个格式编写配置文件是一件相当复杂的事情。我曾经试过把配置项使用逗号分隔保存在一个文本文件里，也试过把配置项保存在非常详细的 YAML 和 XML 中。对于配置文件来说，最重要是要有一致性和规律性，它们使你可以简单快速地编写代码，从配置文件中解析出数据；同时，当用户决定要做出修改时，很方便地保存和更新配置。目前有 几种流行的配置文件格式。对于大多数常见的配置文件格式，Java 都有对应的 库 library 。在本文中，我将使用 XML 格式。对于一些项目，你可能会选择使用 XML，因为它的一个突出特点是能够为包含的数据提供大量相关的元数据，而在另外一些项目中，你可能会因为 XML 的冗长而不选择它。在 Java 中使用 XML 是非常容易的，因为它默认包含了许多健壮的 XML 库。XML 基础讨论 XML 可是一个大话题。我有一本关于 XML 的书，它有超过 700 页的内容。幸运的是，使用 XML 并不需要非常了解它的诸多特性。就像 HTML 一样，XML 是一个带有开始和结束标记的分层标记语言，每一个标记（标签）内可以包含零个或更多数据。下面是一个 XML 的简单示例片段：<xml> <node> <element>Penguin</element> </node> </xml> 在这个 自我描述的 self-descriptive 例子中，XML 解析器使用了以下几个概念： 文档 Document ：<xml> 标签标志着一个 文档 的开始，</xml> 标签标志着这个文档的结束。 节点 Node ：<node> 标签代表了一个 节点。 元素 Element ：<element>Penguin</element> 中，从开头的 < 到最后的 > 表示了一个 元素。 内容 Content ： 在 <element> 元素里，字符串 Penguin 就是 内容。不管你信不信，只要了解了以上几个概念，你就可以开始编写、解析 XML 文件了。创建一个示例配置文件要学习如何解析 XML 文件，只需要一个极简的示例文件就够了。假设现在有一个配置文件，里面保存的是关于一个图形界面窗口的属性：<xml> <window> <theme>Dark</theme> <fullscreen>0</fullscreen> <icons>Tango</icons> </window> </xml> 创建一个名为 ~/.config/DemoXMLParser 的目录：$ mkdir ~/.config/DemoXMLParser 在 Linux 中，~/.config 目录是存放配置文件的默认位置，这是在 自由桌面工作组 的规范中定义的。如果你正在使用一个不遵守 自由桌面工作组 Freedesktop 标准的操作系统，你也仍然可以使用这个目录，只不过你需要自己创建这些目录了。 复制 XML 的示例配置文件，粘贴并保存为 ~/.config/DemoXMLParser/myconfig.xml 文件。使用 Java 解析 XML如果你是 Java 的初学者，你可以先阅读我写的 面向 Java 入门开发者的 7 个小技巧。一旦你对 Java 比较熟悉了，打开你最喜爱的集成开发工具（IDE），创建一个新工程。我会把我的新工程命名为 myConfigParser。刚开始先不要太关注依赖导入和异常捕获这些，你可以先尝试用 javax 和 java.io 包里的标准 Java 扩展来实例化一个解析器。如果你使用了 IDE，它会提示你导入合适的依赖。如果没有，你也可以在文章稍后的部分找到完整的代码，里面就有完整的依赖列表。Path configPath = Paths.get(System.getProperty("user.home"), ".config", "DemoXMLParser"); File configFile = new File(configPath.toString(), "myconfig.xml"); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = null; builder = factory.newDocumentBuilder(); Document doc = null; doc = builder.parse(configFile); doc.getDocumentElement().normalize(); 这段示例代码使用了 java.nio.Paths 类来找到用户的主目录，然后在拼接上默认配置文件的路径。接着，它用 java.io.File 类来把配置文件定义为一个 File 对象。紧接着，它使用了 javax.xml.parsers.DocumentBuilder 和 javax.xml.parsers.DocumentBuilderFactory 这两个类来创建一个内部的文档构造器，这样 Java 程序就可以导入并解析 XML 数据了。最后，Java 创建一个叫 doc 的文档对象，并且把 configFile 文件加载到这个对象里。通过使用 org.w3c.dom 包，它读取并规范化了 XML 数据。基本上就是这样啦。理论上来讲，你已经完成了数据解析的工作。可是，如果你不能够访问数据的话，数据解析也没有多少用处嘛。所以，就让我们再来写一些查询，从你的配置中读取重要的属性值吧。使用 Java 访问 XML 的值从你已经读取的 XML 文档中获取数据，其实就是要先找到一个特定的节点，然后遍历它包含的所有元素。通常我们会使用多个循环语句来遍历节点中的元素，但是为了保持代码可读性，我会尽可能少地使用循环语句：NodeList nodes = doc.getElementsByTagName("window"); for (int i = 0; i < nodes.getLength(); i++) { Node mynode = nodes.item(i); System.out.println("Property = " + mynode.getNodeName()); if (mynode.getNodeType() == Node.ELEMENT_NODE) { Element myelement = (Element) mynode; System.out.println("Theme = " + myelement.getElementsByTagName("theme").item(0).getTextContent()); System.out.println("Fullscreen = " + myelement.getElementsByTagName("fullscreen").item(0).getTextContent()); System.out.println("Icon set = " + myelement.getElementsByTagName("icons").item(0).getTextContent()); } } 这段示例代码使用了 org.w3c.dom.NodeList 类，创建了一个名为 nodes 的 NodeList 对象。这个对象包含了所有名字匹配字符串 window 的子节点，实际上这样的节点只有一个，因为本文的示例配置文件中只配置了一个。紧接着，它使用了一个 for 循环来遍历 nodes 列表。具体过程是：根据节点出现的顺序逐个取出，然后交给一个 if-then 子句处理。这个 if-then 子句创建了一个名为 myelement 的 Element 对象，其中包含了当前节点下的所有元素。你可以使用例如 getChildNodes 和 getElementById 方法来查询这些元素，项目中还 记录了 其他查询方法。在这个示例中，每个元素就是配置的键。而配置的值储存在元素的内容中，你可以使用 .getTextContent 方法来提取出配置的值。在你的 IDE 中运行代码（或者运行编译后的二进制文件）：$ java ./DemoXMLParser.java Property = window Theme = Dark Fullscreen = 0 Icon set = Tango 下面是完整的代码示例：package myConfigParser; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; public class ConfigParser { public static void main(String[] args) { Path configPath = Paths.get(System.getProperty("user.home"), ".config", "DemoXMLParser"); File configFile = new File(configPath.toString(), "myconfig.xml"); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = null; try { builder = factory.newDocumentBuilder(); } catch (ParserConfigurationException e) { e.printStackTrace(); } Document doc = null; try { doc = builder.parse(configFile); } catch (SAXException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } doc.getDocumentElement().normalize(); NodeList nodes = doc.getElementsByTagName("window"); for (int i = 0; i < nodes.getLength(); i++) { Node mynode = nodes.item(i); System.out.println("Property = " + mynode.getNodeName()); if (mynode.getNodeType() == Node.ELEMENT_NODE) { Element myelement = (Element) mynode; System.out.println("Theme = " + myelement.getElementsByTagName("theme").item(0).getTextContent()); System.out.println("Fullscreen = " + myelement.getElementsByTagName("fullscreen").item(0).getTextContent()); System.out.println("Icon set = " + myelement.getElementsByTagName("icons").item(0).getTextContent()); } // close if } // close for } // close method } //close class 使用 Java 更新 XML用户时不时地会改变某个偏好项，这时候 org.w3c.dom 库就可以帮助你更新某个 XML 元素的内容。你只需要选择这个 XML 元素，就像你读取它时那样。不过，此时你不再使用 .getTextContent 方法，而是使用 .setTextContent 方法。updatePref = myelement.getElementsByTagName("fullscreen").item(0); updatePref.setTextContent("1"); System.out.println("Updated fullscreen to " + myelement.getElementsByTagName("fullscreen").item(0).getTextContent()); 这么做会改变应用程序内存中的 XML 文档，但是还没有把数据写回到磁盘上。配合使用 javax 和 w3c 库，你就可以把读取到的 XML 内容写回到配置文件中。TransformerFactory transformerFactory = TransformerFactory.newInstance(); Transformer xtransform; xtransform = transformerFactory.newTransformer(); DOMSource mydom = new DOMSource(doc); StreamResult streamResult = new StreamResult(configFile); xtransform.transform(mydom, streamResult); 这么做会没有警告地写入转换后的数据，并覆盖掉之前的配置。下面是完整的代码，包括更新 XML 的操作：package myConfigParser; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; public class ConfigParser { public static void main(String[] args) { Path configPath = Paths.get(System.getProperty("user.home"), ".config", "DemoXMLParser"); File configFile = new File(configPath.toString(), "myconfig.xml"); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = null; try { builder = factory.newDocumentBuilder(); } catch (ParserConfigurationException e) { // TODO Auto-generated catch block e.printStackTrace(); } Document doc = null; try { doc = builder.parse(configFile); } catch (SAXException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } doc.getDocumentElement().normalize(); Node updatePref = null; // NodeList nodes = doc.getChildNodes(); NodeList nodes = doc.getElementsByTagName("window"); for (int i = 0; i < nodes.getLength(); i++) { Node mynode = nodes.item(i); System.out.println("Property = " + mynode.getNodeName()); if (mynode.getNodeType() == Node.ELEMENT_NODE) { Element myelement = (Element) mynode; System.out.println("Theme = " + myelement.getElementsByTagName("theme").item(0).getTextContent()); System.out.println("Fullscreen = " + myelement.getElementsByTagName("fullscreen").item(0).getTextContent()); System.out.println("Icon set = " + myelement.getElementsByTagName("icons").item(0).getTextContent()); updatePref = myelement.getElementsByTagName("fullscreen").item(0); updatePref.setTextContent("2"); System.out.println("Updated fullscreen to " + myelement.getElementsByTagName("fullscreen").item(0).getTextContent()); } // close if }// close for // write DOM back to the file TransformerFactory transformerFactory = TransformerFactory.newInstance(); Transformer xtransform; DOMSource mydom = new DOMSource(doc); StreamResult streamResult = new StreamResult(configFile); try { xtransform = transformerFactory.newTransformer(); xtransform.transform(mydom, streamResult); } catch (TransformerException e) { e.printStackTrace(); } } // close method } //close class 如何保证配置不出问题编写配置文件看上去是一个还挺简单的任务。一开始，你可能会用一个简单的文本格式，因为你的应用程序只要寥寥几个配置项而已。但是，随着你引入了更多的配置项，读取或者写入错误的数据可能会给你的应用程序带来意料之外的错误。一种帮助你保持配置过程安全、不出错的方法，就是使用类似 XML 的规范格式，然后依靠你用的编程语言的内置功能来处理这些复杂的事情。这也正是我喜欢使用 Java 和 XML 的原因。每当我试图读取错误的配置值时，Java 就会提醒我。通常，这是由于我在代码中试图获取的节点，并不存在于我期望的 XML 路径中。XML 这种高度结构化的格式帮助了代码保持可靠性，这对用户和开发者来说都是有好处的。via: https://opensource.com/article/21/7/parsing-config-files-java作者：Seth Kenlon 选题：lujun9972 译者：lkxed 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
借助终端上的 XML 工具包 XMLStarlet，你就是 XML 之星。学习解析 XML 通常被认为是一件复杂的事情，但它不一定是这样。XML 是高度严格结构化的，所以也是相对来说可预测的。也有许多其他工具可以帮助你使这项工作易于管理。我最喜欢的 XML 实用程序之一是 XMLStarlet，这是一个用于终端的 XML 工具包，借助这个 XML 工具包，你可以验证、解析、编辑、格式化和转换 XML 数据。XMLStarLet 是个相对较小的命令，但浏览 XML 却充满潜力，因此本文演示了如何使用它来查询 XML 数据。安装XMLStarLet 默认安装在 CentOS、Fedora，和许多其他现代 Linux 发行版上，所以你可以打开终端，输入 xmlstarlet 来访问它。如果 XMLStarLet 还没有被安装，你的操作系统则会为你安装它。或者，你可以用包管理器安装 xmlstarlet：$ sudo dnf install xmlstarlet 在 macOS 上，可以使用 MacPorts 或 Homebrew。在 Windows 上，可以使用 Chocolatey。如果都失败了，你可以从 Sourceforge 上的源代码 手动安装它。用 XMLStarlet 解析 XML有许多工具可以帮助解析和转换 XML 数据，包括允许你 编写自己的解析器 的软件库，和复杂的命令，如 fop 和 xsltproc。不过有时你不需要处理 XML 数据；你只需要一个方便的方法从 XML 数据中来提取、更新或验证重要数据。对于随手的 XML 交互，我使用 xmlstarlet，这是常见的处理 XML任务的一个典型的“瑞士军刀”式应用。通过运行 --help 命令，你可以看到它提供哪些选项：$ xmlstarlet --help Usage: xmlstarlet [<options>] <command> [<cmd-options>] where <command> is one of: ed (or edit) - Edit/Update XML document(s) sel (or select) - Select data or query XML document(s) (XPATH, etc) tr (or transform) - Transform XML document(s) using XSLT val (or validate) - Validate XML document(s) (well-formed/DTD/XSD/RelaxNG) fo (or format) - Format XML document(s) el (or elements) - Display element structure of XML document c14n (or canonic) - XML canonicalization ls (or list) - List directory as XML [...] 你可以通过在这些子命令的末尾附加 -help 来获得进一步的帮助：$ xmlstarlet sel --help -Q or --quiet - do not write anything to standard output. -C or --comp - display generated XSLT -R or --root - print root element <xsl-select> -T or --text - output is text (default is XML) -I or --indent - indent output [...] 用 sel 命令选择数据可以使用 xmlstarlet select（简称 sel）命令查看 XML 格式的数据。下面是一个简单的 XML 文档：<?xml version="1.0" encoding="UTF-8" standalone="no"?> <xml> <os> <linux> <distribution> <name>Fedora</name> <release>7</release> <codename>Moonshine</codename> <spins> <name>Live</name> <name>Fedora</name> <name>Everything</name> </spins> </distribution> <distribution> <name>Fedora Core</name> <release>6</release> <codename>Zod</codename> <spins></spins> </distribution> </linux> </os> </xml> 在 XML 文件中查找数据时，你的第一个任务是关注要探索的节点。如果知道节点的路径，请使用 -value of 选项指定完整路径。你越早浏览 文档对象模型（DOM）树，就可以看到更多信息：$ xmlstarlet select --template \ --value-of /xml/os/linux/distribution \ --nl myfile.xml Fedora 7 Moonshine Live Fedora Everything Fedora Core 6 Zod --nl 代表“新的一行”，它插入大量的空白，以确保在输入结果后，终端在新的一行显示。我已经删除了样本输出中的一些多余空间。通过进一步深入 DOM 树来凝聚关注点：$ xmlstarlet select --template \ --value-of /xml/os/linux/distribution/name \ --nl myfile.xml Fedora Fedora Core 条件选择用于导航和解析 XML 的最强大工具之一被称为 XPath。它规范了 XML 搜索中使用的语法，并从 XML 库调用函数。XMLStarlet 能够解析 XPath 表达式，因此可以使用 XPath 函数来有条件的进行选择。XPath 具有丰富的函数，由 W3C 提供了详细文档，但我觉得 Mozilla 的 XPath 文档 更简洁。可以使用方括号作为测试函数，将元素的内容与某个值进行比较。下面是对 <name> 元素的值的测试，它仅返回与特定匹配相关联的版本号。想象一下，示例 XML 文件包含以 1 开头的所有 Fedora 版本。要查看与旧名称 “Fedora Core” 关联的所有版本号（该项目从版本 7 开始删除了名称中的 “Core”），请执行以下操作：$ xmlstarlet sel --template \ --value-of '/xml/os/linux/distribution[name = "Fedora Core"]/release' \ --nl myfile.xml 6 5 4 3 2 1 通过将路径的 --value-of 更改为 /xml/os/linux/distribution[name=“Fedora Core”]/codename，你便可以查看这些版本的所有代号。匹配路径和获取目标值将 XML 标记视为节点的一个好处是，一旦找到节点，就可以将其视为当前的数据的“目录”。它不是一个真正的目录，至少不是文件系统意义上的目录，但它是一个可以查询的数据集合。为了帮助你将目标和“里面”的数据分开，XMLStarlet 把你试图用 --match 选项匹配的内容和用 --value-of 选项匹配的数据值进行了区分。假设你知道 <spin> 节点包含几个元素。这就是你的目标节点。一旦到了这里，就可以使用 --value-of 指定想要哪个元素的值。要查看所有元素，可以使用点（.）来代表当前位置：$ xmlstarlet sel --template \ --match '/xml/os/linux/distribution/spin' \ --value-of '.' --nl myfile.xml \ Live Fedora Everything 与浏览 DOM 一样，可以使用 XPath 表达式来限制返回数据的范围。在本例中，我使用 last() 函数来检索 spin 节点中的最后一个元素：$ xmlstarlet select --template \ --match '/xml/os/linux/distribution/spin' \ --value-of '*[last()]' --nl myfile.xml Everything 在本例中，我使用 position() 函数选择 spin 节点中的特定元素：$ xmlstarlet select --template \ --match '/xml/os/linux/distribution/spin' \ --value-of '*[position() = 2]' --nl myfile.xml Fedora --match 和 --value 选项可以重叠，因此如何将它们一起使用取决于你自己。对于示例 XML，这两个表达式执行的是相同的操作：$ xmlstarlet select --template \ --match '/xml/os/linux/distribution/spin' \ --value-of '.' \ --nl myfile.xml Live Fedora Everything $ xmlstarlet select --template \ --match '/xml/os/linux/distribution' \ --value-of 'spin' \ --nl myfile.xml Live Fedora Everything 熟悉 XMLXML 有时看起来过于冗长和笨拙，但为与之交互和构建的工具却总是让我吃惊。如果你想要好好使用 XML，那么 XMLStarlet 可能是一个很好的切入点。下次要打开 XML 文件查看其结构化数据时，请尝试使用 XMLStarlet，看看是否可以查询这些数据。当你对 XML 越熟悉时，它就越能作为一种健壮灵活的数据格式而为你服务。via: https://opensource.com/article/21/7/parse-xml-linux作者：Seth Kenlon 选题：lujun9972 译者：zepoch 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
了解一下 XML 吧，它是一种严格但灵活的标记语言，无论是在文档还是图像方面应用都十分广泛。XML 是一种分层的标记语言。它使用打开和关闭标签来定义数据，它常用来存储和交换数据，而且由于它极大的灵活性，不论是在 文档 还是 图像 中都用的非常多。这里是一个 XML 文档的例子：<xml> <os> <linux> <distribution> <name>Fedora</name> <release>8</release> <codename>Werewolf</codename> </distribution> <distribution> <name>Slackware</name> <release>12.1</release> <mascot> <official>Tux</official> <unofficial>Bob Dobbs</unofficial> </mascot> </distribution> </linux> </os> </xml> 阅读这个示例 XML，你可能会发现这个 XML 的格式具有直观的特性。 无论你是否熟悉这个文档的主题，你都可能理解本文档中的数据。 这部分原因是因为 XML 被认为是冗长的。 它使用了很多标签，标签可以有很长的描述性名称，并且数据以分层的方式排序，这有助于解释数据之间的关系。 你可能从这个示例中了解到 Fedora 发行版和 Slackware 发行版是两个不同且不相关的 Linux 发行版，因为每个实例都“包含”在自己独立的 <distribution> 标签中。XML 也非常灵活。 与 HTML 不同，它没有预定义的标签列表。 你可以自由创建你需要表示任何数据结构的标签。XML 的组成数据的存在为了读取，当计算机“读取”数据时，该过程称为 解析。 再次使用示例 XML 数据，以下是大多数 XML 解析器认为重要的术语。文档：<xml> 标签标记文档的开始， </xml> 标签标记文档的结束。节点：<os>、<distribution>、<mascot> 这些都是节点，在解析术语中，节点是包含其他标签的标签。元素：像 <name>Fedora</name> 和 <official>Tux</official> 这些都是元素。从第一个<开始，到最后一个 > 结束是一个元素。内容：在两个元素标签之间的数据被称之为内容，在第一个 <name> 标签中，Fedora 字符串就是一个内容。XML 模式XML 文档中的标签和标签继承性称为 模式。一些模式是随意组成的（例如，本文中的示例 XML 代码纯粹是即兴创作的），而其他模式则由标准组织严格定义。 例如，可缩放矢量图形（SVG）模式 由 W3C 定义，而 DocBook 模式 由 Norman Walsh 定义。模式强制执行一致性。 最基本的模式通常也是最严格的。 在我的示例 XML 代码中，将发行版名称放在 <mascot> 节点中是没有意义的，因为文档的隐含模式清楚地表明 mascot 必须是发行版的“子”元素。数据对象模型（DOM）如果你必须不断地描述标签和位置（例如，“系统部分中的 Linux 部分中第二个发行版标签的名称标签”），那么谈论 XML 会让人感到困惑，因此解析器使用文档对象模型（DOM）的概念来表示 XML 数据。 DOM 将 XML 数据放入一种“家谱”结构中，从根元素（在我的示例 XML 中，即 os 标记）开始并包括路径上的每个标记。这种相同的 XML 数据结构可以表示为路径，就像 Linux 系统中的文件或互联网上网页的位置一样。 例如，<mascot> 标签的路径可以表示为 //os/linux/distribution/slackware/mascot。两个 <distribution> 标签可以被表示为 //os/linux/distribution ，因为这里有两个发行版的节点，因此一个解析器可以直接将两个节点的内容载入到一个数组中，可以进行查询。严格的 XMLXML 也以严格而著称。 这意味着大多数应用程序被设计为在遇到 XML 错误时就会故意失败。 这听起来可能有问题，但这是开发人员最欣赏 XML 的事情之一，因为当应用程序试图猜测如何解决错误时，可能会发生不可预测的事情。 例如，在 HTML 定义明确之前，大多数 Web 浏览器都包含“怪癖模式”，因此当人们试图查看糟糕的 HTML 代码时，Web 浏览器却可以加载作者可能想要的内容。 结果非常难以预测，尤其是当一个浏览器的猜测与另一个浏览器不同时。XML 通过在出现故意错误时失败而不允许这样做。 这让作者可以修复错误，直到它们生成有效的 XML。 因为 XML 是良好定义的，所以有许多应用程序的验证器插件以及像 xmllint 和 xmlstarlet 这样的独立命令来帮助你及早定位错误。转换 XML因为 XML 通常用作数据交换，所以将 XML 转换为其他数据格式或其他 XML 模式是很常见的。 经典示例包括 XSLTProc、xmlto 和 pandoc，但从技术上讲，还有许多其他应用程序或者至少程序的一部分就是在转换 XML。事实上，LibreOffice 使用 XML 来布局其文字处理器和电子表格文档，因此无论何时你导出或 从 LibreOffice 转换文件，你都在转换 XML。开源 EPUB 格式的电子书 使用 XML，因此无论何时你 将文档转换为 EPUB 或从 EPUB 转换，你都在转换 XML。Inkscape 是基于矢量的插图应用程序，它将其文件保存在 SVG 中，这是一种专为图形设计的 XML 模式。 任何时候你将 Inkscape 中的图像导出为 PNG 文件时，你都在转换 XML。名单还可以一直继续下去。 XML 是一种数据存储格式，旨在确保你的数据，无论是画布上的点和线、图表上的节点，还是文档中的文字，都可以轻松准确地提取、更新和转换。学习 XML编写 XML 很像编写 HTML。 感谢 Jay Nick 的辛勤工作，在线提供免费且有趣的 XML 课程 可以教你如何使用 XML 创建图形。通常，探索 XML 所需的特殊工具很少。 由于 HTML 和 XML 之间的密切关系，你可以 使用 Web 浏览器查看 XML。 此外，QXmlEdit、NetBeans 和 Kate 等开源文本编辑器通过有用的提示、自动完成、语法验证等，使键入和阅读 XML 变得容易。选择 XMLXML 起初可能看起来有很多数据，但它与 HTML 并没有太大的不同（实际上，HTML 已经 以 XHTML 的形式重新实现为 XML）。 XML 有一个独特的好处，即构成其结构的标签也恰好是元数据，提供有关其存储内容的信息。 精心设计的 XML 模式包含并描述你的数据，使用户能够一目了然并快速解析它，并使开发人员能够使用一些库 快速解析。via: https://opensource.com/article/21/7/what-xml作者：Seth Kenlon 选题：lujun9972 译者：amwps290 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
在以不同语言编写并在不同平台上运行的应用程序之间交换数据时，Protobuf 编码可提高效率。 协议缓冲区 Protocol Buffers （Protobufs）像 XML 和 JSON 一样，可以让用不同语言编写并在不同平台上运行的应用程序交换数据。例如，用 Go 编写的发送程序可以在 Protobuf 中对以 Go 表示的销售订单数据进行编码，然后用 Java 编写的接收方可以对它进行解码，以获取所接收订单数据的 Java 表示方式。这是在网络连接上的结构示意图：Go 销售订单 —> Pbuf 编码 —> 网络 —> Pbuf 界面 —> Java 销售订单与 XML 和 JSON 相比，Protobuf 编码是二进制而不是文本，这会使调试复杂化。但是，正如本文中的代码示例所确认的那样，Protobuf 编码在大小上比 XML 或 JSON 编码要有效得多。Protobuf 以另一种方式提供了这种有效性。在实现级别，Protobuf 和其他编码系统对结构化数据进行 序列化 serialize 和 反序列化 deserialize 。序列化将特定语言的数据结构转换为字节流，反序列化是将字节流转换回特定语言的数据结构的逆运算。序列化和反序列化可能成为数据交换的瓶颈，因为这些操作会占用大量 CPU。高效的序列化和反序列化是 Protobuf 的另一个设计目标。最近的编码技术，例如 Protobuf 和 FlatBuffers，源自 1990 年代初期的 DCE/RPC（ 分布式计算环境/远程过程调用 Distributed Computing Environment/Remote Procedure Call ）计划。与 DCE/RPC 一样，Protobuf 在数据交换中为 IDL（接口定义语言）和编码层做出了贡献。本文将着眼于这两层，然后提供 Go 和 Java 中的代码示例以充实 Protobuf 的细节，并表明 Protobuf 是易于使用的。Protobuf 作为一个 IDL 和编码层像 Protobuf 一样，DCE/RPC 被设计为与语言和平台无关。适当的库和实用程序允许任何语言和平台用于 DCE/RPC 领域。此外，DCE/RPC 体系结构非常优雅。IDL 文档是一侧的远程过程与另一侧的调用者之间的协定。Protobuf 也是以 IDL 文档为中心的。IDL 文档是文本，在 DCE/RPC 中，使用基本 C 语法以及元数据的语法扩展（方括号）和一些新的关键字，例如 interface。这是一个例子：[uuid (2d6ead46-05e3-11ca-7dd1-426909beabcd), version(1.0)] interface echo { const long int ECHO_SIZE = 512; void echo( [in] handle_t h, [in, string] idl_char from_client[ ], [out, string] idl_char from_service[ECHO_SIZE] ); }该 IDL 文档声明了一个名为 echo 的过程，该过程带有三个参数：类型为 handle_t（实现指针）和 idl_char（ASCII 字符数组）的 [in] 参数被传递给远程过程，而 [out] 参数（也是一个字符串）从该过程中传回。在此示例中，echo 过程不会显式返回值（echo 左侧的 void），但也可以返回值。返回值，以及一个或多个 [out] 参数，允许远程过程任意返回许多值。下一节将介绍 Protobuf IDL，它的语法不同，但同样用作数据交换中的协定。DCE/RPC 和 Protobuf 中的 IDL 文档是创建用于交换数据的基础结构代码的实用程序的输入：IDL 文档 —> DCE/PRC 或 Protobuf 实用程序 —> 数据交换的支持代码作为相对简单的文本，IDL 是同样便于人类阅读的关于数据交换细节的文档（特别是交换的数据项的数量和每个项的数据类型）。Protobuf 可用于现代 RPC 系统，例如 gRPC；但是 Protobuf 本身仅提供 IDL 层和编码层，用于从发送者传递到接收者的消息。与原本的 DCE/RPC 一样，Protobuf 编码是二进制的，但效率更高。目前，XML 和 JSON 编码仍在通过 Web 服务等技术进行的数据交换中占主导地位，这些技术利用 Web 服务器、传输协议（例如 TCP、HTTP）以及标准库和实用程序等原有的基础设施来处理 XML 和 JSON 文档。 此外，各种类型的数据库系统可以存储 XML 和 JSON 文档，甚至旧式关系型系统也可以轻松生成查询结果的 XML 编码。现在，每种通用编程语言都具有支持 XML 和 JSON 的库。那么，是什么让我们回到 Protobuf 之类的二进制编码系统呢？让我们看一下负十进制值 -128。以 2 的补码二进制表示形式（在系统和语言中占主导地位）中，此值可以存储在单个 8 位字节中：10000000。此整数值在 XML 或 JSON 中的文本编码需要多个字节。例如，UTF-8 编码需要四个字节的字符串，即 -128，即每个字符一个字节（十六进制，值为 0x2d、0x31、0x32 和 0x38）。XML 和 JSON 还添加了标记字符，例如尖括号和大括号。有关 Protobuf 编码的详细信息下面就会介绍，但现在的关注点是一个通用点：文本编码的压缩性明显低于二进制编码。在 Go 中使用 Protobuf 的示例我的代码示例着重于 Protobuf 而不是 RPC。以下是第一个示例的概述：名为 dataitem.proto 的 IDL 文件定义了一个 Protobuf 消息，它具有六个不同类型的字段：具有不同范围的整数值、固定大小的浮点值以及两个不同长度的字符串。Protobuf 编译器使用 IDL 文件生成 Go 版本（以及后面的 Java 版本）的 Protobuf 消息及支持函数。Go 应用程序使用随机生成的值填充原生的 Go 数据结构，然后将结果序列化为本地文件。为了进行比较， XML 和 JSON 编码也被序列化为本地文件。作为测试，Go 应用程序通过反序列化 Protobuf 文件的内容来重建其原生数据结构的实例。作为语言中立性测试，Java 应用程序还会对 Protobuf 文件的内容进行反序列化以获取原生数据结构的实例。我的网站上提供了该 IDL 文件以及两个 Go 和一个 Java 源文件，打包为 ZIP 文件。最重要的 Protobuf IDL 文档如下所示。该文档存储在文件 dataitem.proto 中，并具有常规的.proto 扩展名。示例 1、Protobuf IDL 文档syntax = "proto3"; package main; message DataItem { int64 oddA = 1; int64 evenA = 2; int32 oddB = 3; int32 evenB = 4; float small = 5; float big = 6; string short = 7; string long = 8; }该 IDL 使用当前的 proto3 而不是较早的 proto2 语法。软件包名称（在本例中为 main）是可选的，但是惯例使用它以避免名称冲突。这个结构化的消息包含八个字段，每个字段都有一个 Protobuf 数据类型（例如，int64、string）、名称（例如，oddA、short）和一个等号 = 之后的数字标签（即键）。标签（在此示例中为 1 到 8）是唯一的整数标识符，用于确定字段序列化的顺序。Protobuf 消息可以嵌套到任意级别，而一个消息可以是另外一个消息的字段类型。这是一个使用 DataItem 消息作为字段类型的示例：message DataItems { repeated DataItem item = 1; }单个 DataItems 消息由重复的（零个或多个）DataItem 消息组成。为了清晰起见，Protobuf 还支持枚举类型：enum PartnershipStatus { reserved "FREE", "CONSTRAINED", "OTHER"; }reserved 限定符确保用于实现这三个符号名的数值不能重复使用。为了生成一个或多个声明 Protobuf 消息结构的特定于语言的版本，包含这些结构的 IDL 文件被传递到protoc 编译器（可在 Protobuf GitHub 存储库中找到）。对于 Go 代码，可以以通常的方式安装支持的 Protobuf 库（这里以 ％ 作为命令行提示符）：% go get github.com/golang/protobuf/proto将 Protobuf IDL 文件 dataitem.proto 编译为 Go 源代码的命令是：% protoc --go_out=. dataitem.proto标志 --go_out 指示编译器生成 Go 源代码。其他语言也有类似的标志。在这种情况下，结果是一个名为 dataitem.pb.go 的文件，该文件足够小，可以将其基本内容复制到 Go 应用程序中。以下是生成的代码的主要部分：var _ = proto.Marshal type DataItem struct { OddA int64 `protobuf:"varint,1,opt,name=oddA" json:"oddA,omitempty"` EvenA int64 `protobuf:"varint,2,opt,name=evenA" json:"evenA,omitempty"` OddB int32 `protobuf:"varint,3,opt,name=oddB" json:"oddB,omitempty"` EvenB int32 `protobuf:"varint,4,opt,name=evenB" json:"evenB,omitempty"` Small float32 `protobuf:"fixed32,5,opt,name=small" json:"small,omitempty"` Big float32 `protobuf:"fixed32,6,opt,name=big" json:"big,omitempty"` Short string `protobuf:"bytes,7,opt,name=short" json:"short,omitempty"` Long string `protobuf:"bytes,8,opt,name=long" json:"long,omitempty"` } func (m *DataItem) Reset() { *m = DataItem{} } func (m *DataItem) String() string { return proto.CompactTextString(m) } func (*DataItem) ProtoMessage() {} func init() {}编译器生成的代码具有 Go 结构 DataItem，该结构导出 Go 字段（名称现已大写开头），该字段与 Protobuf IDL 中声明的名称匹配。该结构字段具有标准的 Go 数据类型：int32、int64、float32 和 string。在每个字段行的末尾，是描述 Protobuf 类型的字符串，提供 Protobuf IDL 文档中的数字标签及有关 JSON 信息的元数据，这将在后面讨论。此外也有函数；最重要的是 Proto.Marshal，用于将 DataItem 结构的实例序列化为 Protobuf 格式。辅助函数包括：清除 DataItem 结构的 Reset，生成 DataItem 的单行字符串表示的 String。描述 Protobuf 编码的元数据应在更详细地分析 Go 程序之前进行仔细研究。Protobuf 编码Protobuf 消息的结构为键/值对的集合，其中数字标签为键，相应的字段为值。字段名称（例如，oddA 和 small）是供人类阅读的，但是 protoc 编译器的确使用了字段名称来生成特定于语言的对应名称。例如，Protobuf IDL 中的 oddA 和 small 名称在 Go 结构中分别成为字段 OddA 和 Small。键和它们的值都被编码，但是有一个重要的区别：一些数字值具有固定大小的 32 或 64 位的编码，而其他数字（包括消息标签）则是 varint 编码的，位数取决于整数的绝对值。例如，整数值 1 到 15 需要 8 位 varint 编码，而值 16 到 2047 需要 16 位。varint 编码在本质上与 UTF-8 编码类似（但细节不同），它偏爱较小的整数值而不是较大的整数值。（有关详细分析，请参见 Protobuf 编码指南）结果是，Protobuf 消息应该在字段中具有较小的整数值（如果可能），并且键数应尽可能少，但每个字段至少得有一个键。下表 1 列出了 Protobuf 编码的要点：编码示例类型长度varintint32、uint32、int64可变长度fixedfixed32、float、double固定的 32 位或 64 位长度字节序列string、bytes序列长度表 1. Protobuf 数据类型未明确固定长度的整数类型是 varint 编码的；因此，在 varint 类型中，例如 uint32（u 代表无符号），数字 32 描述了整数的范围（在这种情况下为 0 到 2 32 - 1），而不是其位的大小，该位大小取决于值。相比之下，对于固定长度类型（例如 fixed32 或 double），Protobuf 编码分别需要 32 位和 64 位。Protobuf 中的字符串是字节序列；因此，字段编码的大小就是字节序列的长度。另一个高效的方法值得一提。回想一下前面的示例，其中的 DataItems 消息由重复的 DataItem 实例组成：message DataItems { repeated DataItem item = 1; }repeated 表示 DataItem 实例是打包的：集合具有单个标签，在这里是 1。因此，具有重复的 DataItem 实例的 DataItems 消息比具有多个但单独的 DataItem 字段、每个字段都需要自己的标签的消息的效率更高。了解了这一背景，让我们回到 Go 程序。dataItem 程序的细节dataItem 程序创建一个 DataItem 实例，并使用适当类型的随机生成的值填充字段。Go 有一个 rand 包，带有用于生成伪随机整数和浮点值的函数，而我的 randString 函数可以从字符集中生成指定长度的伪随机字符串。设计目标是要有一个具有不同类型和位大小的字段值的 DataItem 实例。例如，OddA 和 EvenA 值分别是 64 位非负整数值的奇数和偶数；但是 OddB 和 EvenB 变体的大小为 32 位，并存放 0 到 2047 之间的小整数值。随机浮点值的大小为 32 位，字符串为 16（Short）和 32（Long）字符的长度。这是用随机值填充 DataItem 结构的代码段：// 可变长度整数 n1 := rand.Int63() // 大整数 if (n1 & 1) == 0 { n1++ } // 确保其是奇数 ... n3 := rand.Int31() % UpperBound // 小整数 if (n3 & 1) == 0 { n3++ } // 确保其是奇数 // 固定长度浮点数 ... t1 := rand.Float32() t2 := rand.Float32() ... // 字符串 str1 := randString(StrShort) str2 := randString(StrLong) // 消息 dataItem := &DataItem { OddA: n1, EvenA: n2, OddB: n3, EvenB: n4, Big: f1, Small: f2, Short: str1, Long: str2, }创建并填充值后，DataItem 实例将以 XML、JSON 和 Protobuf 进行编码，每种编码均写入本地文件：func encodeAndserialize(dataItem *DataItem) { bytes, _ := xml.MarshalIndent(dataItem, "", " ") // Xml to dataitem.xml ioutil.WriteFile(XmlFile, bytes, 0644) // 0644 is file access permissions bytes, _ = json.MarshalIndent(dataItem, "", " ") // Json to dataitem.json ioutil.WriteFile(JsonFile, bytes, 0644) bytes, _ = proto.Marshal(dataItem) // Protobuf to dataitem.pbuf ioutil.WriteFile(PbufFile, bytes, 0644) }这三个序列化函数使用术语 marshal，它与 serialize 意思大致相同。如代码所示，三个 Marshal 函数均返回一个字节数组，然后将其写入文件。（为简单起见，忽略可能的错误处理。）在示例运行中，文件大小为：dataitem.xml: 262 bytes dataitem.json: 212 bytes dataitem.pbuf: 88 bytesProtobuf 编码明显小于其他两个编码方案。通过消除缩进字符（在这种情况下为空白和换行符），可以稍微减小 XML 和 JSON 序列化的大小。以下是 dataitem.json 文件，该文件最终是由 json.MarshalIndent 调用产生的，并添加了以 ## 开头的注释：{ "oddA": 4744002665212642479, ## 64-bit >= 0 "evenA": 2395006495604861128, ## ditto "oddB": 57, ## 32-bit >= 0 but < 2048 "evenB": 468, ## ditto "small": 0.7562016, ## 32-bit floating-point "big": 0.85202795, ## ditto "short": "ClH1oDaTtoX$HBN5", ## 16 random chars "long": "xId0rD3Cri%3Wt%^QjcFLJgyXBu9^DZI" ## 32 random chars }尽管这些序列化的数据写入到本地文件中，但是也可以使用相同的方法将数据写入网络连接的输出流。测试序列化和反序列化Go 程序接下来通过将先前写入 dataitem.pbuf 文件的字节反序列化为 DataItem 实例来运行基本测试。这是代码段，其中去除了错误检查部分：filebytes, err := ioutil.ReadFile(PbufFile) // get the bytes from the file ... testItem.Reset() // clear the DataItem structure err = proto.Unmarshal(filebytes, testItem) // deserialize into a DataItem instance用于 Protbuf 反序列化的 proto.Unmarshal 函数与 proto.Marshal 函数相反。原始的 DataItem 和反序列化的副本将被打印出来以确认完全匹配：Original: 2041519981506242154 3041486079683013705 1192 1879 0.572123 0.326855 boPb#T0O8Xd&Ps5EnSZqDg4Qztvo7IIs 9vH66AiGSQgCDxk& Deserialized: 2041519981506242154 3041486079683013705 1192 1879 0.572123 0.326855 boPb#T0O8Xd&Ps5EnSZqDg4Qztvo7IIs 9vH66AiGSQgCDxk&一个 Java Protobuf 客户端用 Java 写的示例是为了确认 Protobuf 的语言中立性。原始 IDL 文件可用于生成 Java 支持代码，其中涉及嵌套类。但是，为了抑制警告信息，可以进行一些补充。这是修订版，它指定了一个 DataMsg 作为外部类的名称，内部类在该 Protobuf 消息后面自动命名为 DataItem：syntax = "proto3"; package main; option java_outer_classname = "DataMsg"; message DataItem { ...进行此更改后，protoc 编译与以前相同，只是所期望的输出现在是 Java 而不是 Go：% protoc --java_out=. dataitem.proto生成的源文件（在名为 main 的子目录中）为 DataMsg.java，长度约为 1,120 行：Java 并不简洁。编译然后运行 Java 代码需要具有 Protobuf 库支持的 JAR 文件。该文件位于 Maven 存储库中。放置好这些片段后，我的测试代码相对较短（并且在 ZIP 文件中以 Main.java 形式提供）：package main; import java.io.FileInputStream; public class Main { public static void main(String[] args) { String path = "dataitem.pbuf"; // from the Go program's serialization try { DataMsg.DataItem deserial = DataMsg.DataItem.newBuilder().mergeFrom(new FileInputStream(path)).build(); System.out.println(deserial.getOddA()); // 64-bit odd System.out.println(deserial.getLong()); // 32-character string } catch(Exception e) { System.err.println(e); } } }当然，生产级的测试将更加彻底，但是即使是该初步测试也可以证明 Protobuf 的语言中立性：dataitem.pbuf 文件是 Go 程序对 Go 语言版的 DataItem 进行序列化的结果，并且该文件中的字节被反序列化以产生一个 Java 语言的 DataItem 实例。Java 测试的输出与 Go 测试的输出相同。用 numPairs 程序来结束让我们以一个示例作为结尾，来突出 Protobuf 效率，但又强调在任何编码技术中都会涉及到的成本。考虑以下 Protobuf IDL 文件：syntax = "proto3"; package main; message NumPairs { repeated NumPair pair = 1; } message NumPair { int32 odd = 1; int32 even = 2; }NumPair 消息由两个 int32 值以及每个字段的整数标签组成。NumPairs 消息是嵌入的 NumPair 消息的序列。Go 语言的 numPairs 程序（如下）创建了 200 万个 NumPair 实例，每个实例都附加到 NumPairs 消息中。该消息可以按常规方式进行序列化和反序列化。示例 2、numPairs 程序package main import ( "math/rand" "time" "encoding/xml" "encoding/json" "io/ioutil" "github.com/golang/protobuf/proto" ) // protoc-generated code: start var _ = proto.Marshal type NumPairs struct { Pair []*NumPair `protobuf:"bytes,1,rep,name=pair" json:"pair,omitempty"` } func (m *NumPairs) Reset() { *m = NumPairs{} } func (m *NumPairs) String() string { return proto.CompactTextString(m) } func (*NumPairs) ProtoMessage() {} func (m *NumPairs) GetPair() []*NumPair { if m != nil { return m.Pair } return nil } type NumPair struct { Odd int32 `protobuf:"varint,1,opt,name=odd" json:"odd,omitempty"` Even int32 `protobuf:"varint,2,opt,name=even" json:"even,omitempty"` } func (m *NumPair) Reset() { *m = NumPair{} } func (m *NumPair) String() string { return proto.CompactTextString(m) } func (*NumPair) ProtoMessage() {} func init() {} // protoc-generated code: finish var numPairsStruct NumPairs var numPairs = &numPairsStruct func encodeAndserialize() { // XML encoding filename := "./pairs.xml" bytes, _ := xml.MarshalIndent(numPairs, "", " ") ioutil.WriteFile(filename, bytes, 0644) // JSON encoding filename = "./pairs.json" bytes, _ = json.MarshalIndent(numPairs, "", " ") ioutil.WriteFile(filename, bytes, 0644) // ProtoBuf encoding filename = "./pairs.pbuf" bytes, _ = proto.Marshal(numPairs) ioutil.WriteFile(filename, bytes, 0644) } const HowMany = 200 * 100 * 100 // two million func main() { rand.Seed(time.Now().UnixNano()) // uncomment the modulus operations to get the more efficient version for i := 0; i < HowMany; i++ { n1 := rand.Int31() // % 2047 if (n1 & 1) == 0 { n1++ } // ensure it's odd n2 := rand.Int31() // % 2047 if (n2 & 1) == 1 { n2++ } // ensure it's even next := &NumPair { Odd: n1, Even: n2, } numPairs.Pair = append(numPairs.Pair, next) } encodeAndserialize() }每个 NumPair 中随机生成的奇数和偶数值的范围在 0 到 20 亿之间变化。就原始数据（而非编码数据）而言，Go 程序中生成的整数总共为 16MB：每个 NumPair 为两个整数，总计为 400 万个整数，每个值的大小为四个字节。为了进行比较，下表列出了 XML、JSON 和 Protobuf 编码的示例 NumsPairs 消息的 200 万个 NumPair 实例。原始数据也包括在内。由于 numPairs 程序生成随机值，因此样本运行的输出有所不同，但接近表中显示的大小。编码文件字节大小Pbuf/其它 比例无pairs.raw16MB169%Protobufpairs.pbuf27MB—JSONpairs.json100MB27%XMLpairs.xml126MB21%表 2. 16MB 整数的编码开销不出所料，Protobuf 和之后的 XML 和 JSON 差别明显。Protobuf 编码大约是 JSON 的四分之一，是 XML 的五分之一。但是原始数据清楚地表明 Protobuf 也会产生编码开销：序列化的 Protobuf 消息比原始数据大 11MB。包括 Protobuf 在内的任何编码都涉及结构化数据，这不可避免地会增加字节。序列化的 200 万个 NumPair 实例中的每个实例都包含四个整数值：Go 结构中的 Even 和 Odd 字段分别一个，而 Protobuf 编码中的每个字段、每个标签一个。对于原始数据（而不是编码数据），每个实例将达到 16 个字节，样本 NumPairs 消息中有 200 万个实例。但是 Protobuf 标记（如 NumPair 字段中的 int32 值）使用 varint 编码，因此字节长度有所不同。特别是，小的整数值（在这种情况下，包括标签在内）需要不到四个字节进行编码。如果对 numPairs 程序进行了修改，以使两个 NumPair 字段的值小于 2048，且其编码为一或两个字节，则 Protobuf 编码将从 27MB 下降到 16MB，这正是原始数据的大小。下表总结了样本运行中的新编码大小。编码文件字节大小Pbuf/其它 比例Nonepairs.raw16MB100%Protobufpairs.pbuf16MB—JSONpairs.json77MB21%XMLpairs.xml103MB15%表 3. 编码 16MB 的小于 2048 的整数总之，修改后的 numPairs 程序的字段值小于 2048，可减少原始数据中每个四字节整数值的大小。但是 Protobuf 编码仍然需要标签，这些标签会在 Protobuf 消息中添加字节。Protobuf 编码确实会增加消息大小，但是如果要编码相对较小的整数值（无论是字段还是键），则可以通过 varint 因子来减少此开销。对于包含混合类型的结构化数据（且整数值相对较小）的中等大小的消息，Protobuf 明显优于 XML 和 JSON 等选项。在其他情况下，数据可能不适合 Protobuf 编码。例如，如果两个应用程序需要共享大量文本记录或大整数值，则可以采用压缩而不是编码技术。via: https://opensource.com/article/19/10/protobuf-data-interchange作者：Marty Kalin 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
一段超级严肃的关于样本序列化的集合、子集和超集的文字我是一名开发者，我读代码，我写代码，我写会写代码的代码，我写会写出供其它代码读的代码的代码。这些都非常火星语，但是有其美妙之处。然而，最后一点，写会写出供其它代码读的代码的代码，可以很快变得比这段文字更费解。有很多方法可以做到这一点。一种不那么复杂而且开发者社区最爱的方式是数据序列化。对于那些不了解我刚刚抛给你的时髦词的人，数据序列化是从一个系统获取一些信息，将其转换为其它系统可以读取的格式，然后将其传递给其它系统的过程。虽然数据序列化格式多到可以埋葬哈利法塔，但它们大多分为两类：易于人类读写，易于机器读写。很难两全其美，因为人类喜欢让我们更具表现力的松散类型和灵活格式标准，而机器倾向于被确切告知一切事情而没有二义性和细节缺失，并且认为“严格规范”才是它们最爱的口味。由于我是一名 web 开发者，而且我们是一个创建网站的机构，我们将坚持使用 web 系统可以理解或不需要太多努力就能理解的特殊格式，而且对人类可读性特别有用的格式：XML、JSON、TOML、CSON 以及 YAML。每个都有各自的优缺点和适当的用例场景。事实最先回到互联网的早期，一些非常聪明的家伙决定整合一种让每个系统都能理解的标准语言，并创造性地将其命名为 标准通用标记语言 Standard Generalized Markup Language （简称 SGML）。SGML 非常灵活，发布者也很好地定义了它。它成为了 XML、SVG 和 HTML 等语言之父。所有这三个都符合 SGML 规范，可是它们都是规则更严格、灵活性更少的子集。最终，人们开始看到非常小、简洁、易读且易于生成的数据的好处，这些数据可以在系统之间以编程的方式共享，而开销很小。大约在那个时候，JSON 诞生了并且能够满足所有的需求。而另一方面，其它语言也开始出现以处理更多的专业用例，如 CSON，TOML 和 YAML。XML：不行了原本，XML 语言非常灵活且易于编写，但它的缺点是冗长，人类难以阅读、计算机非常难以读取，并且有很多语法对于传达信息并不是完全必要的。今天，它在 web 上的数据序列化的用途已经消失了。除非你在编写 HTML 或者 SVG，否则你不太能在许多其它地方看到 XML。一些过时的系统今天仍在使用它，但是用它传递数据往往太重了。我已经可以听到 XML 老爷爷开始在它们的石碑上乱写为什么 XML 是了不起的，所以我将提供一个小小的补充：XML 可以很容易地由系统和人读写。然而，真的，我的意思是荒谬的，很难创建一个可以规范的读取它的系统。这是一个简单美观的 XML 示例：<book id="bk101"> <author>Gambardella, Matthew</author> XML Developer's Guide <genre>Computer</genre> <price>44.95</price> <publish_date>2000-10-01</publish_date> <description>An in-depth look at creating applications with XML.</description> </book>太棒了。易于阅读、理解、写入，也容易编码一个可以读写它的系统。但请考虑这个例子：<!DOCTYPE r [ <!ENTITY y "a]>b"> ]> <r> " /> [a>b b <a <?x <!-- ?> c --> d </r>这上面是 100% 有效的 XML。几乎不可能阅读、理解或推理。编写可以使用和理解这个的代码将花费至少 36 根头发和 248 磅咖啡渣。我们没有那么多时间或咖啡，而且我们大多数老程序员们现在都是秃头。所以，让它活在我们的记忆里，就像 css hacks、IE 6 浏览器 和真空管一样好了。JSON：并列聚会好吧，我们都同意，XML = 差劲。那么，好的替代品是什么？ JavaScript 对象表示法 JavaScript Object Notation ，简称 JSON。JSON（读起来像 Jason 这个名字） 是 Brendan Eich 发明的，并且得到了伟大而强力的 JavaScript 意见领袖 Douglas Crockford 的推广。它现在几乎用在任何地方。这种格式很容易由人和机器编写，按规范中的严格规则解析也相当容易，并且灵活 —— 允许深层嵌套数据，支持所有的原始数据类型，及将集合解释为数组或对象。JSON 成为了将数据从一个系统传输到另一个系统的事实标准。几乎所有语言都有内置读写它的功能。JSON语法很简单。方括号表示数组，花括号表示记录，由冒号分隔的两个值分别表示属性或“键”（在左边）、值（在右边）。所有键必须用双引号括起来： { "books": [ { "id": "bk102", "author": "Crockford, Douglas", "title": "JavaScript: The Good Parts", "genre": "Computer", "price": 29.99, "publish_date": "2008-05-01", "description": "Unearthing the Excellence in JavaScript" } ] }这对你来说应该是完全有意义的。它简洁明了，并且从 XML 中删除了大量额外废话，并传达相同数量的信息。JSON 现在是王道，本文剩下的部分会介绍其它语言格式，这些格式只不过是 JSON 的简化版，尝试让其更简洁或对人类更易读，可结构还是非常相似的。TOML: 缩短到彻底的利他主义TOML（ Tom 的显而易见的最小化语言 Tom’s Obvious, Minimal Language ）允许以相当快捷、简洁的方式定义深层嵌套的数据结构。名字中的 Tom 是指发明者 Tom Preston Werner，他是一位活跃于我们行业的创造者和软件开发人员。与 JSON 相比，语法有点尴尬，更类似 ini 文件。这不是一个糟糕的语法，但是需要一些时间适应。books id = 'bk101' author = 'Crockford, Douglas' title = 'JavaScript: The Good Parts' genre = 'Computer' price = 29.99 publish_date = 2008-05-01T00:00:00+00:00 description = 'Unearthing the Excellence in JavaScript'TOML 中集成了一些很棒的功能，例如多行字符串、保留字符的自动转义、日期、时间、整数、浮点数、科学记数法和“表扩展”等数据类型。最后一点是特别的，是 TOML 如此简洁的原因：[a.b.c] d = 'Hello' e = 'World'以上扩展到以下内容：{ "a": { "b": { "c": { "d": "Hello" "e": "World" } } } }使用 TOML，你可以肯定在时间和文件长度上会节省不少。很少有系统使用它或非常类似的东西作为配置，这是它最大的缺点。根本没有很多语言或库可以用来解释 TOML。CSON: 特定系统所包含的简单样本首先，有两个 CSON 规范。 一个代表 CoffeeScript Object Notation，另一个代表 Cursive Script Object Notation。后者不经常使用，所以我们不会关注它。我们只关注 CoffeeScript。CSON 需要一点介绍。首先，我们来谈谈 CoffeeScript。CoffeeScript 是一种通过运行编译器生成 JavaScript 的语言。它允许你以更加简洁的语法编写 JavaScript 并转译成实际的 JavaScript，然后你可以在你的 web 应用程序中使用它。CoffeeScript 通过删除 JavaScript 中必需的许多额外语法，使编写 JavaScript 变得更容易。CoffeeScript 摆脱的一个大问题是花括号 —— 不需要它们。同样，CSON 是没有大括号的 JSON。它依赖于缩进来确定数据的层次结构。CSON 非常易于读写，并且通常比 JSON 需要更少的代码行，因为没有括号。CSON 还提供一些 JSON 不提供的额外细节。多行字符串非常容易编写，你可以通过使用 # 符号开始一行来输入注释)，并且不需要用逗号分隔键值对。books: [ id: 'bk102' author: 'Crockford, Douglas' title: 'JavaScript: The Good Parts' genre: 'Computer' price: 29.99 publish_date: '2008-05-01' description: 'Unearthing the Excellence in JavaScript' ]这是 CSON 的大问题。它是 CoffeScript 对象表示法 CoffeeScript Object Notation 。也就是说你要用 CoffeeScript 解析/标记化/lex/转译或其它方式来使用 CSON。CoffeeScript 是读取数据的系统。如果数据序列化的目的是允许数据从一个系统传递到另一个系统，这里我们有一个只能由单个系统读取的数据序列化格式，这使得它与防火火柴、防水海绵或者叉匙恼人的脆弱叉子部分一样有用。如果这种格式被其它系统也采用，那它在开发者世界中可能非常有用。但到目前为止这基本上没有发生，所以在 PHP 或 JAVA 等替代语言中使用它是不行的。YAML：年轻人的呼喊开发人员感到高兴，因为 YAML 来自一个 Python 的贡献者。YAML 具有与 CSON 相同的功能集和类似的语法，有一系列新功能，以及几乎所有 web 编程语言都可用的解析器。它还有一些额外的功能，如循环引用、软包装、多行键、类型转换标签、二进制数据、对象合并和集合映射。它具有非常好的可读性和可写性，并且是 JSON 的超集，因此你可以在 YAML 中使用完全合格的 JSON 语法并且一切正常工作。你几乎不需要引号，它可以解释大多数基本数据类型（字符串、整数、浮点数、布尔值等）。books: - id: bk102 author: Crockford, Douglas title: 'JavaScript: The Good Parts' genre: Computer price: 29.99 publish_date: !!str 2008-05-01 description: Unearthing the Excellence in JavaScript业界的年轻人正在迅速采用 YAML 作为他们首选的数据序列化和系统配置格式。他们这样做很机智。YAML 具有像 CSON 一样简洁的所有好处，以及与 JSON 一样的数据类型解释的所有功能。YAML 像加拿大人容易相处一样容易阅读。YAML 有两个问题，对我而言，第一个是大问题。在撰写本文时，YAML 解析器尚未内置于多种语言，因此你需要使用第三方库或扩展来为你选择的语言解析 .yaml 文件。这不是什么大问题，可似乎大多数为 YAML 创建解析器的开发人员都选择随机将“附加功能”放入解析器中。有些允许标记化，有些允许链引用，有些甚至允许内联计算。这一切都很好（某种意义上），只是这些功能都不是规范的一部分，因此很难在其他语言的其他解析器中找到。这导致系统限定，你最终遇到了与 CSON 相同的问题。如果你使用仅在一个解析器中找到的功能，则其他解析器将无法解释输入。大多数这些功能都是无意义的，不属于数据集，而是属于你的应用程序逻辑，因此最好简单地忽略它们和编写符合规范的 YAML。第二个问题是很少有解析器完全实现规范。所有的基本要素都有，但是很难找到一些更复杂和更新的东西，比如软包装、文档标记和首选语言的循环引用。我还没有看到对这些东西的刚需，所以希望它们不让你很失望。考虑到上述情况，我倾向于保持 1.1 规范 中呈现的更成熟的功能集，而避免在 1.2 规范 中找到的新东西。然而，编程是一个不断发展的怪兽，所以当你读完这篇文章时，你或许就可以使用 1.2 规范了。最终哲学这是最后一段话。每个序列化语言都应该以个案标准的方式评价。当涉及机器的可读性时，有些 无出其右 the bee’s knees 。对于人类可读性，有些 名至实归 the cat’s meow ，有些只是 金玉其外 gilded turds 。以下是最终细分：如果你要编写供其他代码阅读的代码，请使用 YAML。如果你正在编写能写出供其他代码读取的代码的代码，请使用 JSON。最后，如果你正在编写将代码转译为供其他代码读取的代码的代码，请重新考虑你的人生选择。via: https://www.zionandzion.com/json-vs-xml-vs-toml-vs-cson-vs-yaml/作者：Tim Anderson 选题：lujun9972 译者：GraveAccent 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
简介XML 和 JSON 是现今互联网中最常用的两种数据交换格式。XML 格式由 W3C 于 1996 年提出。JSON 格式由 Douglas Crockford 于 2002 年提出。虽然这两种格式的设计目标并不相同，但它们常常用于同一个任务，也就是数据交换中。XML 和 JSON 的文档都很完善（RFC 7159、RFC 4825），且都同时具有 人类可读性 human-readable 和 机器可读性 machine-readable 。这两种格式并没有哪一个比另一个更强，只是各自适用的领域不用。（LCTT 译注：W3C 是互联网联盟，制定了各种 Web 相关的标准，如 HTML、CSS 等。Douglas Crockford 除了制定了 JSON 格式，还致力于改进 JavaScript，开发了 JavaScript 相关工具 JSLint 和 JSMin）XML 的优点XML 与 JSON 相比有很多优点。二者间最大的不同在于 XML 可以通过在标签中添加属性这一简单的方法来存储 元数据 metadata 。而使用 JSON 时需要创建一个对象，把元数据当作对象的成员来存储。虽然二者都能达到存储元数据的目的，但在这一情况下 XML 往往是更好的选择，因为 JSON 的表达形式会让客户端程序开发人员误以为要将数据转换成一个对象。举个例子，如果你的 C++ 程序需要使用 JSON 格式发送一个附带元数据的整型数据，需要创建一个对象，用对象中的一个 名称/值对 name/value pair 来记录整型数据的值，再为每一个附带的属性添加一个名称/值对。接收到这个 JSON 的程序在读取后很可能把它当成一个对象，可事实并不是这样。虽然这是使用 JSON 传递元数据的一种变通方法，但他违背了 JSON 的核心理念：“ JSON 的结构与常规的程序语言中的结构相对应，而无需修改。 JSON’s structures look like conventional programming language structures. No restructuring is necessary. ” 1 虽然稍后我会说这也是 XML 的一个缺点，但 XML 中对命名冲突、 前缀 prefix 的处理机制赋予了它 JSON 所不具备的能力。程序员们可以通过前缀来把统一名称给予两个不同的实体。 2 当不同的实体在客户端中使用的名称相同时，这一特性会非常有用。XML 的另一个优势在于大多数的浏览器可以把它以 具有高可读性和强组织性的方式 highly readable and organized way 展现给用户。XML 的树形结构让它易于结构化，浏览器也让用户可以自行展开或折叠树中的元素，这简直就是调试的福音。XML 对比 JSON 有一个很重要的优势就是它可以记录 混合内容 mixed content 。例如在 XML 中处理包含结构化标记的字符串时，程序员们只要把带有标记的文本放在一个标签内就可以了。可因为 JSON 只包含数据，没有用于指明标签的简单方式，虽然可以使用处理元数据的解决方法，但这总有点滥用之嫌。JSON 的优点JSON 自身也有很多优点。其中最显而易见的一点就是 JSON 比 XML 简洁得多。因为 XML 中需要打开和关闭标签，而 JSON 使用名称/值对表示数据，使用简单的 { 和 } 来标记对象，[ 和 ] 来标记数组，, 来表示数据的分隔，: 表示名称和值的分隔。就算是使用 gzip 压缩，JSON 还是比 XML 要小，而且耗时更少。 3 正如 Sumaray 和 Makki 在实验中指出的那样，JSON 在很多方面都比 XML 更具优势，得出同样结果的还有 Nurseitov、Paulson、Reynolds 和 Izurieta。首先，由于 JSON 文件天生的简洁性，与包含相同信息的 XML 相比，JSON 总是更小，这意味着更快的传输和处理速度。第二，在不考虑大小的情况下，两组研究 4 5 表明使用 JSON 执行序列化和反序列化的速度显著优于使用 XML。第三，后续的研究指出 JSON 的处理在 CPU 资源的使用上也优于 XML。研究人员发现 JSON 在总体上使用的资源更少，其中更多的 CPU 资源消耗在用户空间，系统空间消耗的 CPU 资源较少。这一实验是在 RedHat 的设备上进行的，RedHat 表示更倾向于在用户空间使用 CPU 资源。 6 不出意外，Sumaray 和 Makki 在研究里还说明了在移动设备上 JSON 的性能也优于 XML。 7 这是有道理的，因为 JSON 消耗的资源更少，而移动设备的性能也更弱。JSON 的另一个优点在于其对对象和数组的表述和 宿主语言 host language 中的数据结构相对应，例如 对象 object 、 记录 record 、 结构体 struct 、 字典 dictionary 、 哈希表 hash table 、 键值列表 keyed list 还有 数组 array 、 向量 vector 、 列表 list ，以及对象组成的数组等等。 8 虽然 XML 里也能表达这些数据结构，也只需调用一个函数就能完成解析，而往往需要更多的代码才能正确的完成 XML 的序列化和反序列化处理。而且 XML 对于人类来说不如 JSON 那么直观，XML 标准缺乏对象、数组的标签的明确定义。当结构化的标记可以替代嵌套的标签时，JSON 的优势极为突出。JSON 中的花括号和中括号则明确表示了数据的结构，当然这一优势也包含前文中的问题，在表示元数据时 JSON 不如 XML 准确。虽然 XML 支持 命名空间 namespace 与 前缀 prefix ，但这不代表 JSON 没有处理命名冲突的能力。比起 XML 的前缀，它处理命名冲突的方式更简洁，在程序中的处理也更自然。在 JSON 里，每一个对象都在它自己的命名空间中，因此不同对象内的元素名称可以随意重复。在大多数编程语言中，不同的对象中的成员可以包含相同的名字，所以 JSON 根据对象进行名称区分的规则在处理时更加自然。也许 JSON 比 XML 更优的部分是因为 JSON 是 JavaScript 的子集，所以在 JavaScript 代码中对它的解析或封装都非常的自然。虽然这看起来对 JavaScript 程序非常有用，而其他程序则不能直接从中获益，可实际上这一问题已经被很好的解决了。现在 JSON 的网站的列表上展示了 64 种不同语言的 175 个工具，它们都实现了处理 JSON 所需的功能。虽然我不能评价大多数工具的质量，但它们的存在明确了开发者社区拥抱 JSON 这一现象，而且它们切实简化了在不同平台使用 JSON 的难度。二者的动机简单地说，XML 的目标是标记文档。这和 JSON 的目标想去甚远，所以只要用得到 XML 的地方就尽管用。它使用树形的结构和包含语义的文本来表达混合内容以实现这一目标。在 XML 中可以表示数据的结构，但这并不是它的长处。JSON 的目标是用于数据交换的一种结构化表示。它直接使用对象、数组、数字、字符串、布尔值这些元素来达成这一目标。这完全不同于文档标记语言。正如上面说的那样，JSON 没有原生支持 混合内容 mixed content 的记录。软件这些主流的开放 API 仅提供 XML： 亚马逊产品广告 API Amazon Product Advertising API 。这些主流 API 仅提供 JSON： 脸书图 API Facebook Graph API 、 谷歌地图 API Google Maps API 、 推特 API Twitter API 、AccuWeather API、Pinterest API、Reddit API、Foursquare API。这些主流 API 同时提供 XML 和 JSON： 谷歌云存储 Google Cloud Storage 、 领英 API Linkedin API 、Flickr API。根据 可编程网络 Programmable Web 9 的数据，最流行的 10 个 API 中只有一个是仅提供 XML 且不支持 JSON 的。其他的要么同时支持 XML 和 JSON，要么只支持 JSON。这表明了大多数应用开发者都更倾向于使用支持 JSON 的 API，原因大概是 JSON 更快的处理速度与良好口碑，加之与 XML 相比更加轻量。此外，大多数 API 只是传递数据而非文档，所以 JSON 更加合适。例如 Facebook 的重点在于用户的交流与帖子，谷歌地图则主要处理坐标和地图信息，AccuWeather 就只传递天气数据。总之，虽然不能说天气 API 在使用时究竟是 JSON 用的多还是 XML 用的多，但是趋势明确偏向了 JSON。 10 11 这些主流的桌面软件仍然只是用 XML：Microsoft Word、Apache OpenOffice、LibraOffice。因为这些软件需要考虑引用、格式、存储等等，所以比起 JSON，XML 优势更大。另外，这三款程序都支持混合内容，而 JSON 在这一点上做得并不如 XML 好。举例说明，当用户使用 Microsoft Word 编辑一篇论文时，用户需要使用不同的文字字形、文字大小、文字颜色、页边距、段落格式等，而 XML 结构化的组织形式与标签属性生来就是为了表达这些信息的。这些主流的数据库支持 XML：IBM DB2、Microsoft SQL Server、Oracle Database、PostgresSQL、BaseX、eXistDB、MarkLogic、MySQL。这些是支持 JSON 的主流数据库：MongoDB、CouchDB、eXistDB、Elastisearch、BaseX、MarkLogic、OrientDB、Oracle Database、PostgreSQL、Riak。在很长一段时间里，SQL 和关系型数据库统治着整个数据库市场。像 甲骨文 Oracle 和 微软 Microsoft 这样的软件巨头都提供这类数据库，然而近几年 NoSQL 数据库正逐步受到开发者的青睐。也许是正巧碰上了 JSON 的普及，大多数 NoSQL 数据库都支持 JSON，像 MongoDB、CouchDB 和 Riak 这样的数据库甚至使用 JSON 来存储数据。这些数据库有两个重要的特性是它们适用于现代网站：一是它们与关系型数据库相比 更容易扩展 more scalable ；二是它们设计的目标就是 web 运行所需的核心组件。 12 由于 JSON 更加轻量，又是 JavaScript 的子集，所以很适合 NoSQL 数据库，并且让这两个品质更容易实现。此外，许多旧的关系型数据库增加了 JSON 支持，例如 Oracle Database 和 PostgreSQL。由于 XML 与 JSON 间的转换比较麻烦，所以大多数开发者会直接在他们的应用里使用 JSON，因此开发数据库的公司才有支持 JSON 的理由。（LCTT 译注：NoSQL 是对不同于传统的关系数据库的数据库管理系统的统称。参考来源） 13 未来对互联网的种种变革中，最让人期待的便是 物联网 Internet of Things （IoT）。这会给互联网带来大量计算机之外的设备，例如手表、温度计、电视、冰箱等等。这一势头的发展良好，预期在不久的将来迎来爆发式的增长。据估计，到 2020 年时会有 260 亿 到 2000 亿的物联网设备被接入互联网。 14 15 几乎所有的物联网设备都是小型设备，因此性能比笔记本或台式电脑要弱很多，而且大多数都是嵌入式系统。因此，当它们需要与互联网上的系统交换数据时，更轻量、更快速的 JSON 自然比 XML 更受青睐。 16 受益于 JSON 在 web 上的快速普及，与 XML 相比，这些新的物联网设备更有可能从使用 JSON 中受益。这是一个典型的梅特卡夫定律的例子，无论是 XML 还是 JSON，抑或是什么其他全新的格式，现存的设备和新的设备都会从支持最广泛使用的格式中受益。Node.js 是一款服务器端的 JavaScript 框架，随着她的诞生与快速成长，与 MongoDB 等 NoSQL 数据库一起，让全栈使用 JavaScript 开发成为可能。这些都预示着 JSON 光明的未来，这些软件的出现让 JSON 运用在全栈开发的每一个环节成为可能，这将使应用更加轻量，响应更快。这也是任何应用的追求之一，所以，全栈使用 JavaScript 的趋势在不久的未来都不会消退。 17 此外，另一个应用开发的趋势是从 SOAP 转向 REST。 18 19 20 XML 和 JSON 都可以用于 REST，可 SOAP 只能使用 XML。从这些趋势中可以推断，JSON 的发展将统一 Web 的信息交换格式，XML 的使用率将继续降低。虽然不应该把 JSON 吹过头了，因为 XML 在 Web 中的使用依旧很广，而且它还是 SOAP 的唯一选择，可考虑到 SOAP 到 REST 的迁移，NoSQL 数据库和全栈 JavaScript 的兴起，JSON 卓越的性能，我相信 JSON 很快就会在 Web 开发中超过 XML。至于其他领域，XML 比 JSON 更好的情况并不多。角注Introducing JSON ↩XML Tutorial ↩JSON vs. XML: Some hard numbers about verbosity ↩Comparison of JSON and XML Data Interchange Formats: A Case Study ↩A comparison of data serialization formats for optimal efficiency on a mobile platform ↩Comparison of JSON and XML Data Interchange Formats: A Case Study ↩A comparison of data serialization formats for optimal efficiency on a mobile platform ↩Introducing JSON ↩Most Popular APIs: At Least One Will Surprise You ↩Why JSON will continue to push XML out of the picture ↩Thousands of APIs Paint a Bright Future for the Web ↩Why JSON will continue to push XML out of the picture ↩How JSON sparked NoSQL – and will return to the RDBMS fold ↩A Simple Explanation Of ‘The Internet Of Things’ ↩Proofpoint Uncovers Internet of Things (IoT) Cyberattack ↩Why JSON will continue to push XML out of the picture ↩Why JSON will continue to push XML out of the picture ↩Thousands of APIs Paint a Bright Future for the Web ↩3,000 Web APIs: Trends From A Quickly Growing Directory ↩How REST replaced SOAP on the Web: What it means to you ↩via: https://www.cs.tufts.edu/comp/150IDS/final_papers/tstras01.1/FinalReport/FinalReport.html作者：TOM STRASSNER 选题：lujun9972 译者：wwhio 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
JSON 已经占领了全世界。当今，任何两个应用程序彼此通过互联网通信时，可以打赌它们在使用 JSON。它已被所有大型企业所采用：十大最受欢迎的 web API 接口列表中（主要由 Google、Facebook 和 Twitter 提供），仅仅只有一个 API 接口是以 XML 的格式开放数据的。 1 这个列表中的 Twitter API 为此做了一个鲜活的注脚：其对 XML 格式的支持到 2013 年结束，其时发布的新版本的 API 取消 XML 格式，转而仅使用 JSON。JSON 也在程序编码级别和文件存储上被广泛采用：在 Stack Overflow（LCTT 译注：一个面向程序员的问答网站）上，现在更多的是关于 JSON 的问题，而不是其他的数据交换格式。 2 XML 仍然在很多地方存在。网络上它被用于 SVG 和 RSS / Atom 信息流。Android 开发者想要获得用户权限许可时，需要在其 APP 的 manifest 文件中声明 —— 此文件是 XML 格式的。XML 的替代品也不仅仅只有 JSON，现在有很多人在使用 YAML 或 Google 的 Protocol Buffers 等技术，但这些技术的受欢迎程度远不如 JSON。目前来看，JSON 是应用程序在网络之间通信的首选协议格式。考虑到自 2005 年来 Web 编程世界就垂涎于 “异步 JavaScript 和 XML” 而非 “异步 JavaScript 和 JSON” 的技术潜力，你可以发现 JSON 的主导地位是如此的让人惊讶。当然了，这可能与这两种通信格式的受欢迎程度无关，而仅反映出缩写 “AJAX” 似乎比 “AJAJ” 更具吸引力。但是，即使在 2005 年有些人（实际上没有太多人）已经用 JSON 来取代 XML 了，我们不禁要问 XML 的噩运来的如此之快，以至于短短十来年，“异步 JavaScript 和 XML” 这个名称就成为一个很讽刺的误称。那这十来年发生了什么？JSON 怎么会在那么多应用程序中取代了 XML？现在被全世界工程师和系统所使用、依赖的这种数据格式是谁提出的？JSON 之诞生2001 年 4 月，首个 JSON 格式的消息被发送出来。此消息是从旧金山湾区某车库的一台计算机发出的，这是计算机历史上重要的的时刻。Douglas Crockford 和 Chip Morningstar 是一家名为 State Software 的技术咨询公司的联合创始人，他们当时聚集在 Morningstar 的车库里测试某个想法，发出了此消息。在 “AJAX” 这个术语被创造之前，Crockford 和 Morningstar 就已经在尝试构建好用的 AJAX 应用程序了，可是浏览器对其兼容性不好。他们想要在初始页面加载后就将数据传递给应用程序，但其目标要针对所有的浏览器，这就实现不了。这在今天看来不太可信，但是要记得 2001 年的时候 Internet Explorer（IE）代表了网页浏览器的最前沿技术产品。早在 1999 年的时候，Internet Explorer 5 就支持了原始形式的 XMLHttpRequest，开发者可以使用名为 ActiveX 的框架来访问此对象。Crockford 和 Morningstar 能够使用此技术（在 IE 中）来获取数据，但是在 Netscape 4 中（这是他们想要支持的另一种浏览器）就无法使用这种解决方案。为此 Crockford 和 Morningstar 只得使用一套不同的系统以兼容不同的浏览器。第一条 JSON 消息如下所示： 消息中只有一小部分类似于今天我们所知的 JSON，本身其实是一个包含有一些 JavaScript 的 HTML 文档。类似于 JSON 的部分只是传递给名为 receive() 的函数的 JavaScript 对象字面量。Crockford 和 Morningstar 决定滥用 HTML 的帧（ ）以发送数据。他们可以让一个帧指向一个返回的上述 HTML 文档的 URL。当接收到 HTML 时，JavaScript 代码段就会运行，就可以把数据对象字面量如实地传递回应用程序。只要小心的回避浏览器保护策略（即子窗口不允许访问父窗口），这种技术就可以正常运行无误。可以看到 Crockford 和 Mornginstar 通过明确地设置文档域这种方法来达到其目的。（这种基于帧的技术，有时称为隐藏帧技术，通常在 90 年代后期，即在广泛使用 XMLHttpRequest 技术之前使用。 3 ）第一个 JSON 消息的惊人之处在于，它显然不是一种新的数据格式的首次使用。它就是 JavaScript！实际上，以此方式使用 JavaScript 的想法如此简单，Crockford 自己也说过他不是第一个这样做的人。他说 Netscape 公司的某人早在 1996 年就使用 JavaScript 数组字面量来交换信息。 4 因为消息就是 JavaScript，其不需要任何特殊解析工作，JavaScript 解释器就可搞定一切。最初的 JSON 信息实际上与 JavaScript 解释器发生了冲突。JavaScript 保留了大量的关键字（ECMAScript 6 版本就有 64 个保留字），Crockford 和 Morningstar 无意中在其 JSON 中使用了一个保留字。他们使用了 do 作为了键名，但 do 是解释器中的保留字。因为 JavaScript 使用的保留字太多了，Crockford 做了决定：既然不可避免的要使用到这些保留字，那就要求所有的 JSON 键名都加上引号。被引起来的键名会被 JavaScript 解释器识别成字符串，其意味着那些保留字也可以放心安全的使用。这就为什么今天 JSON 键名都要用引号引起来的原因。Crockford 和 Morningstar 意识到这技术可以应用于各类应用系统。想给其命名为 “JSML”，即 JavaScript 标记语言 JavaScript Markup Language 的意思，但发现这个缩写已经被一个名为 Java Speech 标记语言的东西所使用了。因此他们决定采用 “JavaScript Object Notation”，缩写为 JSON。他们开始向客户推销，但很快发现客户不愿意冒险使用缺乏官方规范的未知技术。所以 Crockford 决定写一个规范。2002 年，Crockford 买下了 JSON.org 域名，放上了 JSON 语法及一个解释器的实例例子。该网站仍然在运行，现在已经包含有 2013 年正式批准的 JSON ECMA 标准的显著链接。在该网站建立后，Crockford 并没有过多的推广，但很快发现很多人都在提交各种不同编程语言的 JSON 解析器实现。JSON 的血统显然与 JavaScript 相关联，但很明显 JSON 非常适合于不同语言之间的数据交换。AJAX 导致的误会2005 年，JSON 有了一次大爆发。那一年，一位名叫 Jesse James Garrett 的网页设计师和开发者在博客文章中创造了 “AJAX” 一词。他很谨慎地强调：AJAX 并不是新技术，而是 “好几种蓬勃发展的技术以某种强大的新方式汇集在一起。 5 ” AJAX 是 Garrett 给这种正受到青睐的 Web 应用程序的新开发方法的命名。他的博客文章接着描述了开发人员如何利用 JavaScript 和 XMLHttpRequest 构建新型应用程序，这些应用程序比传统的网页更具响应性和状态性。他还以 Gmail 和 Flickr 网站已经使用 AJAX 技术作为了例子。当然了，“AJAX” 中的 “X” 代表 XML。但在随后的问答帖子中，Garrett 指出，JSON 可以完全替代 XML。他写道：“虽然 XML 是 AJAX 客户端进行数据输入、输出的最完善的技术，但要实现同样的效果，也可以使用像 JavaScript Object Notation（JSON）或任何类似的结构数据方法等技术。 ” 6 开发者确实发现在构建 AJAX 应用程序时可以很容易的使用 JSON，许多人更喜欢它而不是 XML。具有讽刺意味的是，对 AJAX 的兴趣逐渐的导致了 JSON 的普及。大约在这个时候，JSON 引起了博客圈的注意。2006 年，Dave Winer，一位高产的博主，他也是许多基于 XML 的技术（如 RSS 和 XML-RPC）背后的开发工程师，他抱怨到 JSON 毫无疑问的正在重新发明 XML。尽管人们认为数据交换格式之间的竞争不会导致某一技术的消亡。其写到：毫无疑问，我可以编写一个例程来解析 JSON，但来看看他们要重新发明一个东西有多大的意义，出于某种原因 XML 本身对他们来说还不够好（我很想听听原因）。谁想干这荒谬之事？查找一棵树然后把节点串起来。可以立马试试。 7 我很理解 Winer 的挫败感。事实上并没有太多人喜欢 XML。甚至 Winer 也说过他不喜欢 XML。 8 但 XML 已被设计成一个可供任何人使用，并且可以用于几乎能想象到的所有事情。归根到底，XML 实际上是一门元语言，允许你为特定应用程序自定义该领域特定的语言。如 Web 信息流技术 RSS 和 SOAP（简单对象访问协议）就是例子。Winer 认为由于通用交换格式所带来的好处，努力达成共识非常重要。XML 的灵活性应该能满足任何人的需求，然而 JSON 格式呢，其并没有比 XML 提供更多东西，除了它抛弃了使 XML 更灵活的那些繁琐的东西。Crockford 阅读了 Winer 的这篇文章并留下了评论。为了回应 JSON 重新发明 XML 的指责，Crockford 写到：“重造轮子的好处是可以得到一个更好的轮子。” 9 JSON 与 XML 对比到 2014 年，JSON 已经由 ECMA 标准和 RFC 官方正式认可。它有了自己的 MIME 类型。JSON 已经进入了大联盟时代。为什么 JSON 比 XML 更受欢迎？在 JSON.org 网站上，Crockford 总结了一些 JSON 的优势。他写到，JSON 的语法极少，其结构可预测，因此 JSON 更容易被人类和机器理解。 10 其他博主一直关注 XML 的冗长啰嗦及“尖括号负担”。 11 XML 中每个开始标记都必须与结束标记匹配，这意味着 XML 文档包含大量的冗余信息。在未压缩时，XML 文档的体积比同等信息量 JSON 文档的体积大很多，但是，更重要的，这也使 XML 文档更难以阅读。Crockford 还声称 JSON 的另一个巨大优势是其被设计为数据交换格式。 12 从一开始，它的目的就是在应用程序间传递结构化信息的。而 XML 呢，虽然也可以使用来传递数据，但其最初被设计为文档标记语言。它演变自 SGML（通用标准标记语言），而它又是从被称为 Scribe 的标记语言演变而来，其旨在发展成类似于 LaTeX 一样的文字处理系统。XML 中，一个标签可以包含有所谓的“混合内容”，即带有围绕单词、短语的内嵌标签的文本。这会让人浮现出一副用红蓝笔记录的手稿画面，这是标记语言核心思想的形象比喻。另一方面，JSON 不支持对混合内容模型的清晰构建，但这也意味着它的结构足够简单。一份文档最好的建模就是一棵树，但 JSON 抛弃了这种文档的思想，Crockford 将 JSON 抽象限制为字典和数组，这是所有程序员构建程序时都会使用的最基本的、也最熟悉的元素。最后，我认为人们不喜欢 XML 是因为它让人困惑。它让人迷惑的地方就是有很多不同的风格。乍一看，XML 本身及其子语言（如 RSS、ATOM、SOAP 或 SVG）之间的界限并不明显。典型的 XML 文档的第一行标识了该 XML 的版本，然后该 XML 文档应该符合特定的子语言。这就有变化需要考虑了，特别是跟 JSON 做比较，JSON 是如此简单，以至于永远不需要编写新版本的 JSON 规范。XML 的设计者试图将 XML 做为唯一的数据交换格式以支配所有格式，就会掉入那个经典的程序员陷阱：过度工程化。XML 非常笼统及概念化，所以很难于简单的使用。在 2000 年的时候，发起了一场使 HTML 符合 XML 标准的活动，发布了一份符合 XML 标准的 HTML 开发规范，这就此后很出名的 XHTML。虽然一些浏览器厂商立即开始支持这个新标准，但也很明显，大部分基于 HTML 技术的开发者不愿意改变他们的习惯。新标准要求对 XHTML 文档进行严格的验证，而不是基于 HTML 的基准。但大多的网站都是依赖于 HTML 的宽容规则的。到 2009 年的时候，试图编写第二版本的 XHTML 标准的努力已经流产，因为未来已清晰可见，HTML 将会发展为 HTML5（一种不强制要求接受 XML 规则的标准）。如果 XHTML 的努力取得了成功，那么 XML 可能会成为其设计者所希望的通用数据格式。想象一下，一个 HTML 文档和 API 响应具有完全相同结构的世界。在这样的世界中，JSON 可能不会像现在一样普遍存在。但我把 HTML 的失败看做是 XML 阵营的一种道义上的失败。如果 XML 不是 HTML 的最佳工具，那么对于其他应用程序，也许会有更好的工具出现。在这个世界，我们的世界，很容易看到像 JSON 格式这样的足够简单、量体裁衣的才能获得更大的成功。如果你喜欢这博文，每两周会更新一次！ 请在 Twitter 上关注 @TwoBitHistory 或订阅 RSS feed，以确保得到更新的通知。http://www.cs.tufts.edu/comp/150IDS/final_papers/tstras01.1/FinalReport/FinalReport.html#software ↩https://insights.stackoverflow.com/trends?tags=json%2Cxml%2Cprotocol-buffers%2Cyaml%2Ccsv ↩Zakas, Nicholas C., et al. “What Is Ajax?” Professional Ajax, 2nd ed., Wiley, 2007. ↩https://youtu.be/-C-JoyNuQJs?t=32s ↩http://adaptivepath.org/ideas/ajax-new-approach-web-applications/ ↩同上 ↩http://scripting.com/2006/12/20.html ↩http://blogoscoped.com/archive/2009-03-05-n15.html ↩https://scripting.wordpress.com/2006/12/20/scripting-news-for-12202006/#comment-26383 ↩http://www.json.org/xml.html ↩https://blog.codinghorror.com/xml-the-angle-bracket-tax ↩https://youtu.be/-C-JoyNuQJs?t=33m50sgg ↩via: https://twobithistory.org/2017/09/21/the-rise-and-rise-of-json.html作者：Two-Bit History 选题：lujun9972 译者：runningwater 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
这篇教程提供了一个面向 C++ 程序员关于 protocol buffers 的基础介绍。通过创建一个简单的示例应用程序，它将向我们展示：在 .proto 文件中定义消息格式使用 protocol buffer 编译器使用 C++ protocol buffer API 读写消息这不是一个关于在 C++ 中使用 protocol buffers 的全面指南。要获取更详细的信息，请参考 Protocol Buffer Language Guide 和 Encoding Reference。为什么使用 Protocol Buffers我们接下来要使用的例子是一个非常简单的"地址簿"应用程序，它能从文件中读取联系人详细信息。地址簿中的每一个人都有一个名字、ID、邮件地址和联系电话。如何序列化和获取结构化的数据？这里有几种解决方案：以二进制形式发送/接收原生的内存数据结构。通常，这是一种脆弱的方法，因为接收/读取代码必须基于完全相同的内存布局、大小端等环境进行编译。同时，当文件增加时，原始格式数据会随着与该格式相关的软件而迅速扩散，这将导致很难扩展文件格式。你可以创造一种 ad-hoc 方法，将数据项编码为一个字符串——比如将 4 个整数编码为 12:3:-23:67。虽然它需要编写一次性的编码和解码代码且解码需要耗费一点运行时成本，但这是一种简单灵活的方法。这最适合编码非常简单的数据。序列化数据为 XML。这种方法是非常吸引人的，因为 XML 是一种适合人阅读的格式，并且有为许多语言开发的库。如果你想与其他程序和项目共享数据，这可能是一种不错的选择。然而，众所周知，XML 是空间密集型的，且在编码和解码时，它对程序会造成巨大的性能损失。同时，使用 XML DOM 树被认为比操作一个类的简单字段更加复杂。Protocol buffers 是针对这个问题的一种灵活、高效、自动化的解决方案。使用 Protocol buffers，你需要写一个 .proto 说明，用于描述你所希望存储的数据结构。利用 .proto 文件，protocol buffer 编译器可以创建一个类，用于实现对高效的二进制格式的 protocol buffer 数据的自动化编码和解码。产生的类提供了构造 protocol buffer 的字段的 getters 和 setters，并且作为一个单元来处理读写 protocol buffer 的细节。重要的是，protocol buffer 格式支持格式的扩展，代码仍然可以读取以旧格式编码的数据。在哪可以找到示例代码示例代码被包含于源代码包，位于“examples”文件夹。可在这里下载代码。定义你的协议格式为了创建自己的地址簿应用程序，你需要从 .proto 开始。.proto 文件中的定义很简单：为你所需要序列化的每个数据结构添加一个 消息 （ message ） ，然后为消息中的每一个字段指定一个名字和类型。这里是定义你消息的 .proto 文件 addressbook.proto。package tutorial; message Person { required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phone = 4; } message AddressBook { repeated Person person = 1; } 如你所见，其语法类似于 C++ 或 Java。我们开始看看文件的每一部分内容做了什么。.proto 文件以一个 package 声明开始，这可以避免不同项目的命名冲突。在 C++，你生成的类会被置于与 package 名字一样的命名空间。下一步，你需要定义 消息 （ message ） 。消息只是一个包含一系列类型字段的集合。大多标准的简单数据类型是可以作为字段类型的，包括 bool、int32、float、double 和 string。你也可以通过使用其他消息类型作为字段类型，将更多的数据结构添加到你的消息中——在以上的示例，Person 消息包含了 PhoneNumber 消息，同时 AddressBook 消息包含 Person 消息。你甚至可以定义嵌套在其他消息内的消息类型——如你所见，PhoneNumber 类型定义于 Person 内部。如果你想要其中某一个字段的值是预定义值列表中的某个值，你也可以定义 enum 类型——这儿你可以指定一个电话号码是 MOBILE、HOME 或 WORK 中的某一个。每一个元素上的 = 1、= 2 标记确定了用于二进制编码的唯一 “标签” （ tag ） 。标签数字 1-15 的编码比更大的数字少需要一个字节，因此作为一种优化，你可以将这些标签用于经常使用的元素或 repeated 元素，剩下 16 以及更高的标签用于非经常使用的元素或 optional 元素。每一个 repeated 字段的元素需要重新编码标签数字，因此 repeated 字段适合于使用这种优化手段。每一个字段必须使用下面的修饰符加以标注：required：必须提供该字段的值，否则消息会被认为是 “未初始化的” （ uninitialized ） 。如果 libprotobuf 以调试模式编译，序列化未初始化的消息将引起一个断言失败。以优化形式构建，将会跳过检查，并且无论如何都会写入该消息。然而，解析未初始化的消息总是会失败（通过 parse 方法返回 false）。除此之外，一个 required 字段的表现与 optional 字段完全一样。optional：字段可能会被设置，也可能不会。如果一个 optional 字段没被设置，它将使用默认值。对于简单类型，你可以指定你自己的默认值，正如例子中我们对电话号码的 type 一样，否则使用系统默认值：数字类型为 0、字符串为空字符串、布尔值为 false。对于嵌套消息，默认值总为消息的“默认实例”或“原型”，它的所有字段都没被设置。调用 accessor 来获取一个没有显式设置的 optional（或 required） 字段的值总是返回字段的默认值。repeated：字段可以重复任意次数（包括 0 次）。repeated 值的顺序会被保存于 protocol buffer。可以将 repeated 字段想象为动态大小的数组。你可以查找关于编写 .proto 文件的完整指导——包括所有可能的字段类型——在 Protocol Buffer Language Guide 里面。不要在这里面查找与类继承相似的特性，因为 protocol buffers 不会做这些。required 是永久性的在把一个字段标识为 required 的时候，你应该特别小心。如果在某些情况下你不想写入或者发送一个 required 的字段，那么将该字段更改为 optional 可能会遇到问题——旧版本的读者（LCTT 译注：即读取、解析旧版本 Protocol Buffer 消息的一方）会认为不含该字段的消息是不完整的，从而有可能会拒绝解析。在这种情况下，你应该考虑编写特别针对于应用程序的、自定义的消息校验函数。Google 的一些工程师得出了一个结论：使用 required 弊多于利；他们更愿意使用 optional 和 repeated 而不是 required。当然，这个观点并不具有普遍性。编译你的 Protocol Buffers既然你有了一个 .proto，那你需要做的下一件事就是生成一个将用于读写 AddressBook 消息的类（从而包括 Person 和 PhoneNumber）。为了做到这样，你需要在你的 .proto 上运行 protocol buffer 编译器 protoc：如果你没有安装编译器，请下载这个包，并按照 README 中的指令进行安装。现在运行编译器，指定源目录（你的应用程序源代码位于哪里——如果你没有提供任何值，将使用当前目录）、目标目录（你想要生成的代码放在哪里；常与 $SRC_DIR 相同），以及你的 .proto 路径。在此示例中：protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto 因为你想要 C++ 的类，所以你使用了 --cpp_out 选项——也为其他支持的语言提供了类似选项。在你指定的目标文件夹，将生成以下的文件：addressbook.pb.h，声明你生成类的头文件。addressbook.pb.cc，包含你的类的实现。Protocol Buffer API让我们看看生成的一些代码，了解一下编译器为你创建了什么类和函数。如果你查看 addressbook.pb.h，你可以看到有一个在 addressbook.proto 中指定所有消息的类。关注 Person 类，可以看到编译器为每个字段生成了 读写函数 （ accessors ） 。例如，对于 name、id、email 和 phone 字段，有下面这些方法：（LCTT 译注：此处原文所指文件名有误，径该之。）// name inline bool has_name() const; inline void clear_name(); inline const ::std::string& name() const; inline void set_name(const ::std::string& value); inline void set_name(const char* value); inline ::std::string* mutable_name(); // id inline bool has_id() const; inline void clear_id(); inline int32_t id() const; inline void set_id(int32_t value); // email inline bool has_email() const; inline void clear_email(); inline const ::std::string& email() const; inline void set_email(const ::std::string& value); inline void set_email(const char* value); inline ::std::string* mutable_email(); // phone inline int phone_size() const; inline void clear_phone(); inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phone() const; inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phone(); inline const ::tutorial::Person_PhoneNumber& phone(int index) const; inline ::tutorial::Person_PhoneNumber* mutable_phone(int index); inline ::tutorial::Person_PhoneNumber* add_phone(); 正如你所见到，getters 的名字与字段的小写名字完全一样，并且 setter 方法以 set_ 开头。同时每个 单一 （ singular ） （required 或 optional）字段都有 has_ 方法，该方法在字段被设置了值的情况下返回 true。最后，所有字段都有一个 clear_ 方法，用以清除字段到 空 （ empty ） 状态。数字型的 id 字段仅有上述的基本 读写函数 （ accessors ） 集合，而 name 和 email 字段有两个额外的方法，因为它们是字符串——一个是可以获得字符串直接指针的mutable_ 的 getter ，另一个为额外的 setter。注意，尽管 email 还没被 设置 （ set ） ，你也可以调用 mutable_email；因为 email 会被自动地初始化为空字符串。在本例中，如果你有一个单一的（required 或 optional）消息字段，它会有一个 mutable_ 方法，而没有 set_ 方法。repeated 字段也有一些特殊的方法——如果你看看 repeated 的 phone 字段的方法，你可以看到：检查 repeated 字段的 _size（也就是说，与 Person 相关的电话号码的个数）使用下标取得特定的电话号码更新特定下标的电话号码添加新的电话号码到消息中，之后你便可以编辑。（repeated 标量类型有一个 add_ 方法，用于传入新的值）为了获取 protocol 编译器为所有字段定义生成的方法的信息，可以查看 C++ generated code reference。枚举和嵌套类与 .proto 的枚举相对应，生成的代码包含了一个 PhoneType 枚举。你可以通过 Person::PhoneType 引用这个类型，通过 Person::MOBILE、Person::HOME 和 Person::WORK 引用它的值。（实现细节有点复杂，但是你无须了解它们而可以直接使用）编译器也生成了一个 Person::PhoneNumber 的嵌套类。如果你查看代码，你可以发现真正的类型为 Person_PhoneNumber，但它通过在 Person 内部使用 typedef 定义，使你可以把 Person_PhoneNumber 当成嵌套类。唯一产生影响的一个例子是，如果你想要在其他文件前置声明该类——在 C++ 中你不能前置声明嵌套类，但是你可以前置声明 Person_PhoneNumber。标准的消息方法所有的消息方法都包含了许多别的方法，用于检查和操作整个消息，包括：bool IsInitialized() const; ：检查是否所有 required 字段已经被设置。string DebugString() const; ：返回人类可读的消息表示，对调试特别有用。void CopyFrom(const Person& from);：使用给定的值重写消息。void Clear();：清除所有元素为空的状态。上面这些方法以及下一节要讲的 I/O 方法实现了被所有 C++ protocol buffer 类共享的 消息 （ Message ） 接口。为了获取更多信息，请查看 complete API documentation for Message。解析和序列化最后，所有 protocol buffer 类都有读写你选定类型消息的方法，这些方法使用了特定的 protocol buffer 二进制格式。这些方法包括：bool SerializeToString(string* output) const;：序列化消息并将消息字节数据存储在给定的字符串中。注意，字节数据是二进制格式的，而不是文本格式；我们只使用 string 类作为合适的容器。bool ParseFromString(const string& data);：从给定的字符创解析消息。bool SerializeToOstream(ostream* output) const;：将消息写到给定的 C++ ostream。bool ParseFromIstream(istream* input);：从给定的 C++ istream 解析消息。这些只是两个用于解析和序列化的选择。再次说明，可以查看 Message API reference 完整的列表。Protocol Buffers 和面向对象设计Protocol buffer 类通常只是纯粹的数据存储器（像 C++ 中的结构体）；它们在对象模型中并不是一等公民。如果你想向生成的 protocol buffer 类中添加更丰富的行为，最好的方法就是在应用程序中对它进行封装。如果你无权控制 .proto 文件的设计的话，封装 protocol buffers 也是一个好主意（例如，你从另一个项目中重用一个 .proto 文件）。在那种情况下，你可以用封装类来设计接口，以更好地适应你的应用程序的特定环境：隐藏一些数据和方法，暴露一些便于使用的函数，等等。但是你绝对不要通过继承生成的类来添加行为。这样做的话，会破坏其内部机制，并且不是一个好的面向对象的实践。写消息现在我们尝试使用 protocol buffer 类。你的地址簿程序想要做的第一件事是将个人详细信息写入到地址簿文件。为了做到这一点，你需要创建、填充 protocol buffer 类实例，并且将它们写入到一个 输出流 （ output stream ） 。这里的程序可以从文件读取 AddressBook，根据用户输入，将新 Person 添加到 AddressBook，并且再次将新的 AddressBook 写回文件。这部分直接调用或引用 protocol buffer 类的代码会以“// pb”标出。#include <iostream> #include <fstream> #include <string> #include "addressbook.pb.h" // pb using namespace std; // This function fills in a Person message based on user input. void PromptForAddress(tutorial::Person* person) { cout << "Enter person ID number: "; int id; cin >> id; person->set_id(id); // pb cin.ignore(256, '\n'); cout << "Enter name: "; getline(cin, *person->mutable_name()); // pb cout << "Enter email address (blank for none): "; string email; getline(cin, email); if (!email.empty()) { // pb person->set_email(email); // pb } while (true) { cout << "Enter a phone number (or leave blank to finish): "; string number; getline(cin, number); if (number.empty()) { break; } tutorial::Person::PhoneNumber* phone_number = person->add_phone(); //pb phone_number->set_number(number); // pb cout << "Is this a mobile, home, or work phone? "; string type; getline(cin, type); if (type == "mobile") { phone_number->set_type(tutorial::Person::MOBILE); // pb } else if (type == "home") { phone_number->set_type(tutorial::Person::HOME); // pb } else if (type == "work") { phone_number->set_type(tutorial::Person::WORK); // pb } else { cout << "Unknown phone type. Using default." << endl; } } } // Main function: Reads the entire address book from a file, // adds one person based on user input, then writes it back out to the same // file. int main(int argc, char* argv[]) { // Verify that the version of the library that we linked against is // compatible with the version of the headers we compiled against. GOOGLE_PROTOBUF_VERIFY_VERSION; // pb if (argc != 2) { cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl; return -1; } tutorial::AddressBook address_book; // pb { // Read the existing address book. fstream input(argv[1], ios::in | ios::binary); if (!input) { cout << argv[1] << ": File not found. Creating a new file." << endl; } else if (!address_book.ParseFromIstream(&input)) { // pb cerr << "Failed to parse address book." << endl; return -1; } } // Add an address. PromptForAddress(address_book.add_person()); // pb { // Write the new address book back to disk. fstream output(argv[1], ios::out | ios::trunc | ios::binary); if (!address_book.SerializeToOstream(&output)) { // pb cerr << "Failed to write address book." << endl; return -1; } } // Optional: Delete all global objects allocated by libprotobuf. google::protobuf::ShutdownProtobufLibrary(); // pb return 0; } 注意 GOOGLE_PROTOBUF_VERIFY_VERSION 宏。它是一种好的实践——虽然不是严格必须的——在使用 C++ Protocol Buffer 库之前执行该宏。它可以保证避免不小心链接到一个与编译的头文件版本不兼容的库版本。如果被检查出来版本不匹配，程序将会终止。注意，每个 .pb.cc 文件在初始化时会自动调用这个宏。同时注意在程序最后调用 ShutdownProtobufLibrary()。它用于释放 Protocol Buffer 库申请的所有全局对象。对大部分程序，这不是必须的，因为虽然程序只是简单退出，但是 OS 会处理释放程序的所有内存。然而，如果你使用了内存泄漏检测工具，工具要求全部对象都要释放，或者你正在写一个 Protocol Buffer 库，该库可能会被一个进程多次加载和卸载，那么你可能需要强制 Protocol Buffer 清除所有东西。读取消息当然，如果你无法从它获取任何信息，那么这个地址簿没多大用处！这个示例读取上面例子创建的文件，并打印文件里的所有内容。#include <iostream> #include <fstream> #include <string> #include "addressbook.pb.h" // pb using namespace std; // Iterates though all people in the AddressBook and prints info about them. void ListPeople(const tutorial::AddressBook& address_book) { // pb for (int i = 0; i < address_book.person_size(); i++) { // pb const tutorial::Person& person = address_book.person(i); // pb cout << "Person ID: " << person.id() << endl; // pb cout << " Name: " << person.name() << endl; // pb if (person.has_email()) { // pb cout << " E-mail address: " << person.email() << endl; // pb } for (int j = 0; j < person.phone_size(); j++) { // pb const tutorial::Person::PhoneNumber& phone_number = person.phone(j); // pb switch (phone_number.type()) { // pb case tutorial::Person::MOBILE: // pb cout << " Mobile phone #: "; break; case tutorial::Person::HOME: // pb cout << " Home phone #: "; break; case tutorial::Person::WORK: // pb cout << " Work phone #: "; break; } cout << phone_number.number() << endl; // ob } } } // Main function: Reads the entire address book from a file and prints all // the information inside. int main(int argc, char* argv[]) { // Verify that the version of the library that we linked against is // compatible with the version of the headers we compiled against. GOOGLE_PROTOBUF_VERIFY_VERSION; // pb if (argc != 2) { cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl; return -1; } tutorial::AddressBook address_book; // pb { // Read the existing address book. fstream input(argv[1], ios::in | ios::binary); if (!address_book.ParseFromIstream(&input)) { // pb cerr << "Failed to parse address book." << endl; return -1; } } ListPeople(address_book); // Optional: Delete all global objects allocated by libprotobuf. google::protobuf::ShutdownProtobufLibrary(); // pb return 0; } 扩展 Protocol Buffer或早或晚在你发布了使用 protocol buffer 的代码之后，毫无疑问，你会想要 "改善" protocol buffer 的定义。如果你想要新的 buffers 向后兼容，并且老的 buffers 向前兼容——几乎可以肯定你很渴望这个——这里有一些规则，你需要遵守。在新的 protocol buffer 版本：你绝不可以修改任何已存在字段的标签数字你绝不可以添加或删除任何 required 字段你可以删除 optional 或 repeated 字段你可以添加新的 optional 或 repeated 字段，但是你必须使用新的标签数字（也就是说，标签数字在 protocol buffer 中从未使用过，甚至不能是已删除字段的标签数字）。（对于上面规则有一些例外情况，但它们很少用到。）如果你能遵守这些规则，旧代码则可以欢快地读取新的消息，并且简单地忽略所有新的字段。对于旧代码来说，被删除的 optional 字段将会简单地赋予默认值，被删除的 repeated 字段会为空。新代码显然可以读取旧消息。然而，请记住新的 optional 字段不会呈现在旧消息中，因此你需要显式地使用 has_ 检查它们是否被设置或者在 .proto 文件在标签数字后使用 [default = value] 提供一个合理的默认值。如果一个 optional 元素没有指定默认值，它将会使用类型特定的默认值：对于字符串，默认值为空字符串；对于布尔值，默认值为 false；对于数字类型，默认类型为 0。注意，如果你添加一个新的 repeated 字段，新代码将无法辨别它被留空（被新代码）或者从没被设置（被旧代码），因为 repeated 字段没有 has_ 标志。优化技巧C++ Protocol Buffer 库已极度优化过了。但是，恰当的用法能够更多地提高性能。这里是一些技巧，可以帮你从库中挤压出最后一点速度：尽可能复用消息对象。即使它们被清除掉，消息也会尽量保存所有被分配来重用的内存。因此，如果我们正在处理许多相同类型或一系列相似结构的消息，一个好的办法是重用相同的消息对象，从而减少内存分配的负担。但是，随着时间的流逝，对象可能会膨胀变大，尤其是当你的消息尺寸（LCTT 译注：各消息内容不同，有些消息内容多一些，有些消息内容少一些）不同的时候，或者你偶尔创建了一个比平常大很多的消息的时候。你应该自己通过调用 SpaceUsed 方法监测消息对象的大小，并在它太大的时候删除它。对于在多线程中分配大量小对象的情况，你的操作系统内存分配器可能优化得不够好。你可以尝试使用 google 的 tcmalloc。高级用法Protocol Buffers 绝不仅用于简单的数据存取以及序列化。请阅读 C++ API reference 来看看你还能用它来做什么。protocol 消息类所提供的一个关键特性就是 反射 （ reflection ） 。你不需要针对一个特殊的消息类型编写代码，就可以遍历一个消息的字段并操作它们的值。一个使用反射的有用方法是 protocol 消息与其他编码互相转换，比如 XML 或 JSON。反射的一个更高级的用法可能就是可以找出两个相同类型的消息之间的区别，或者开发某种“协议消息的正则表达式”，利用正则表达式，你可以对某种消息内容进行匹配。只要你发挥你的想像力，就有可能将 Protocol Buffers 应用到一个更广泛的、你可能一开始就期望解决的问题范围上。反射是由 Message::Reflection interface 提供的。via: https://developers.google.com/protocol-buffers/docs/cpptutorial作者：Google 译者：cposture 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出