第7章 GPSD
原文链接: http://www.aosabook.org/en/gpsd.html
作者:Eric Raymond
译者:Liuli Chen(陈旒俐)
GPSD是一系列管理GPS设备和其他用于导航与精确计时的传感器的工具,包括航海自动识别系统(marine AIS (Automatic Identification System))的无线电广播设备和电子罗盘。其中最主要的程序是一个叫做gpsd
的服务守护进程,它管理这这些传感器,并且在TCP/IP端口生成JSON流报告。其他的程序包括用来展示代码模型和各种诊断工具的客户端。
GPSD被广泛地部署在笔记本、智能手机以及包括自动驾驶汽车和自动潜艇在内的自动交通工具上。它在嵌入式系统中被用来导航、精确耕作、位置敏感的科学遥测、网络时间服务等,起着不容忽视的作用。它甚至被用在装甲车的敌友识别系统(Identification-Friend-or-Foe)中,例如M1“艾布拉姆斯(Abrams)”主战坦克。
GPSD是一个中型项目,大约43千行代码,主要用C和Python写成,最早的历史可以追溯到1997年,现行版本从从2005年开始写的。核心成员稳定在3人,其他大约24人作出了半正规的贡献,另外还有上百人做了一次性的修补。
GPSD不管是在像splint
,valgrind
和Coverity这样的审查工具还是在突发错误记录上向来有着极低的错误率。这并不是一个巧合,这个项目做了大量的自动测试,而这一切努力都很好地得到了回报。
由于排除了所有前任的不确定性,GPSD在它做的事情上相当优秀。2010年,GPSD在Allience for Code Excellence赢得了Good Code Grant的第一名。等你看完这一章你就会明白为什么。
7.1. GPSD存在的原因
GPSD存在的原因是那些GPS和其他导航传感器上装载的应用层协议设计和文档都很差,并且不同传感器类型和模型之间存在这巨大的差异。你可以在[Ray]看到详细的讨论。尤其是,你会在哪里发现NMEA 0183(一类GPS报告包标准)的怪诞和一堆文档书写极差的供应商协议。
如果应用不得不自己处理所有的这些复杂性,必将导致大量脆弱冗余的代码。由于底层硬件的逐渐改变,这将进而造成高度的用户可视化不足以及持续的问题。
GPSD通过自己知道所有的协议(截至写这篇文章的时候我们支持大约20种不同的协议)、管理串行和USB接口、用一种简单的设备无关的JSON格式报告传感器有效载荷,来把位置感知的应用和具体的硬件接口个离开来。GPSD通过提供客户端库使得客户端应用不用知道报告格式,从而进一步简化了生活。相反,得到传感器信息成为了一个简单的进程调用。
同时,GPSD也支持精确计时:只要它附属的某个传感器有PPS (pulse-per-second)能力,它就能作为一个网络时间协议守护进程ntpd
(the Network Time Protocal Daemon)的时间源工作。GPSD的开发者与ntpd
项目的开发者紧密合作来改善网络时间服务。
目前(2011年年中),我们正在完成对航海导航接收器的AIS网络支持。将来,一旦出现新的位置干吃传感器的协议文档和测试设备,我们期望可以支持它们,比如二代飞机异频雷达接收器。
总的来说,GPSD设计中最终要的主题就是把所有设备相关隐藏在一个简单的与无需配置的服务器交互的客户端接口之后。
7.2. 外部视图
GPSD套件中最主要的程序是gpsd
服务守护进程。它能够通过RS232、USB、蓝牙、TCP/IP和UDP连接从一系列附属传感器设备收集捕获量。报告一般来说会被传送到TCP/IP端口2947,但也能通过一个共享内存或者D-BUS接口出去。
GPSD分布装载着C、C++和Python的客户端库,包含了C、C++、Python和PHP的示例客户端,Perl客户端绑定可以通过CPAN实现。这些客户端库把应用和JSON报告协议分离,不仅仅方便了应用开发者,也解决了GPSD开发者们头痛的问题。因此,即使这个协议为新的传感器类型产生了新的特性,其暴露给客户端的API依然可以保持不变。
套件中的其他程序包括一个低层设备监视器的公共程序(gpsd
),一个为错误统计和设备超时产生报告的分析器(gpsprof
),一个调整设备设置的公共程序(gpsctl
),和一个把传感器日志批量转换成可读的JSON的程序(gpsdecode
)。它们共同帮助那些技术用户深入观察附属传感器的运作。
当然了,这些工具也帮助GPSD自己的开发者检查gpsd
的正确运作。其中最重要的测试工具就是gpsfake
,一个能够连接任意数目的活动传感器日志的测试套件。通过gpsfake
,我们可以重复运行有错误报告的传感器日志来重新产生特定的问题。gpsfake
也是我们大量回归测试套件的引擎,它通过简化发现破坏性变化的过程来减少修改软件的开销。
我们从中学到的最重要的教训之一就是:对一个软件套件来说,光有正确性是不够,它还要能够证明自己的正确性。我们发现适度追求这个目标不仅不是过于敏感,而且还是一双翅膀——我们花在写测试套件和回归测试上的时间多次得到了回报,因为这给了我们充分的自由,在修改代码的时候不用担心对现有函数造成大的破坏。
7.3. 软件层次
GPSD内部运行是非常复杂的,而不仅仅是人们想像的那样“加入一个传感器使之工作”。gpsd
的内部构造分成四个部分:驱动(drivers)、包嗅探器(packet sniffer)、核心库(core library)和多路复用器(multiplexer)。我们将自底向上地介绍它们。
图7.1:软件层次
驱动是每一种我们支持的传感器芯片组的必要用户空间设备驱动。其中的关键切入点是把数据包解析成时间-位置-速度或者状态信息、更改其模式或者波特率、探测设备亚型等的方法。辅助方法能够支持驱动程序的控制操作,比如改变设备的串行速度。驱动的整个接口是一个充满数据和方法指针的C结构,刻意仿照了Unix设备驱动结构。
数据包嗅探器负责从串行输入数据流挖掘数据包。基本上,它是一个寻找任何看起来像我们那20个左右已知包类型的东西的状态机(大多数包类型有检验和,所以当我们认为我们已经确定了一个的时候我们可以非常自信)。由于设备可以热插拔或者更改模式,所以从串行口或者USB口出来的数据包类型不一定要在第一次辨认后保持不变。
核心库管理一个与传感器设备的会话。关键切入点是:
- 通过打开设备并且从中读取数据开始一个会话,在波特率和奇偶校验/停止位组合上搜寻直到包嗅探器与一个已知包类型实现同步锁;
- 为一个包轮询设备;
- 关闭设备,结束会话。
核心库的一个重要特点是,它负责根据嗅探器返回的包类型把每一个GPS连接切换到正确的设备驱动上。这个不是事先配置的,并可能随着时间变化,特别是如果设备在不同的报告协议之间切换。(大多数GPS芯片组支持NMEA和一个或多个供应商的二进制协议,AIS接收器等设备可能会在同一线路上用两种不同的协议来报告包。)
最后,多路复用器是守护进程的一部分。它用来处理客户端会话和设备分配,负责把报告传递给客户端、接收客户端命令、响应热插拔通知。它基本上全都包含在一个源文件gpsd.c
里,从不直接与设备驱动交互。
前三个组成部分(除了多路复用器)都一起连在一个叫lingpsd
的库里,可以和多路复用器分开使用。另外一些直接跟传感器交互的工具是通过直接调用核心库和驱动层实现的,比如gpsmon
和gpsctl
。
最复杂的组件是包嗅探器,它有大约2,000行代码。这些代码已经不能更简略了,因为作为一个可以是被很多不同协议的状态机,它不得不那么大那么粗糙。幸运的是,包嗅探器依然是容易分离测试的,它的问题不会被耦合到代码的其他部分。
多路复用器也差不多这么大,但少了几分粗糙。组成大部分守护程序的设备驱动的代码量大约是15,000行。所有其他的代码(所有的支持工具、库和测试客户端)加起来跟守护进程的差不多(部分代码,特别是JSON解析器,是被守护进程和客户端库共享的)。
这种分层方法的成功之处表现在几个不同的方面。一个是新的设备驱动很容易写,所以不是核心队伍里的人也写了一些设备驱动:驱动API被写成了文档,单独的驱动只通过指向一个主设备类型表的指针耦合到核心库。
另一个好处是,系统集成商可以简单地通过不编译不用的驱动来大幅降低GPSD嵌入式部署的内存占用。守护进程刚开始并不大,是一个相当简单的构造,能够欢快地在低功耗、低速、小内存的ARM设备上运行。(ARM是一个被用在移动和嵌入式电子产品中的32位RISC指令集结构。参见http://en.wikipedia.org/wiki/ARM_architecture。)
分层的第三个好处是守护进程多路复用器可以脱离核心库之上,取而代之的是简单的逻辑,比如gpsdecode
公用程序直接把传感器日志文件批量转换成JSON报告。
GPSD的这部分架构没有什么新奇的。它的教训是,有意识的严格的按照Unix设备设计模式设计应用不仅在操作系统内核上有益,在类似的要求跟很多硬件和协议打交道的用户空间程序上也是有益的。
7.4. 数据流视图
现在,让我们从数据流视图赖考虑GPSD的架构。在正常操作中,gpsd循环等待以下几个源的输入:
- 一组在TCP/IP端口发出请求的客户端。
- 一组通过串行设备或者USB设备相连的导航传感器。
- 热插拔脚本所用的特殊控制套接字和一些配置工具。
- 一组定期发布差分GPS修正更新的服务器(DGPS和NTRIP)。这些服务器的操作方法就像导航传感器一样。
当一个与某设备(可能是一个导航传感器)相连的USB端口活动时,一个热插拔脚本(附带GPSD)向控制套接字发送通知。这提示多路复用器层把该设备放到其内部的传感器列表中。相反,一个设备删除事件可以从列表中删除一个设备。
当一个客户端发布一个查看请求时,多路服用器层打开列表中的导航传感器,并把它们的文件描述子添加到主体的选择调用中,开始接收数据。否则,所有的GPS设备是关闭的(但仍然在列表中),守护程序是静态的。停止发送数据的设备会在设备列表中超时。
图7.2:数据流
当数据从导航传感器中进来的时候,它会被馈送到包嗅探器。包嗅探器是一个像编译器的词法分析器一样工作的有限状态机。它的工作就是分别从每个端口积累数据,当数据积累到一个已知类型的数据包的时候辨认出来。
一个数据包可能包含一个GPS定位、航海AIS数据报、读取指南针信息的传感器、差分全球定位系统(DGPS(Differenctial GPS))广播包等。包嗅探器不关心数据包的内容,它只在积累到一个数据包的时候通知核心库,并把有效载荷和数据包类型回传。
然后,核心库把包送到与其类型关联的驱动。驱动的工作是把数据从数据包的有效载荷中挖掘出来,构造成一个设备会话结构,并且设置一些状态位来告诉多路复用器数据类型。
其中一个位指示守护进程已经积累了足以把报告传送给客户端的数据。当这个位在一次传感器读取之后置为1,这意味着已经读到了一个数据包的末尾、一个数据包组(一个或多个数据包)的末尾,这个时候设备的会话结构中的数据应该被传送到一个输出口。
主要的输出口是套接字。它会生成一个JSON格式的报告对象,然后把它送到所有查看这个设备的客户端。也有共享内存的输出口,把数据复制到一个共享内存段。在这两种情况下,客户端库都会把数据解组成客户端程序内存空间里的一个结构。第三种输出口是通过DBUS更新位置。
GPSD的代码小心地进行了垂直水平划分。包嗅探器不知道也不需要知道任何与数据包有效载荷相关的事,也不关心输入源是USB口、RS232设备、蓝牙无线连接、伪终端、TCP套接字连接还是UDP数据包流。驱动知道怎么分析数据包有效载荷,却对包嗅探器的内部和输出口一无所知。输出口只关注驱动更新的会话数据结构。
这种功能分离对GPSD十分有利。例如,2010年初,有人要求我们修改代码使之能够接受自动潜艇上的机载导航系统以UDP数据包传入的传感器数据,这很容易在几行代码里实现,而不必干扰数据通路的后面阶段。
更一般地,细致的分层和模块化使得添加新的传感器类型变得相对简单。我们大约每6个月左右添加新的驱动,其中一部分驱动并不是核心开发者写的。
7.5. 定义架构
随着gpsd
之类的开源项目的发展,一个反复出现的主题是,每一个贡献者会在解决他或她的特定问题的同时逐渐在本是清晰划分的层次或者阶段之间泄露信息。
在写代码的时候,我们关心的是,一些关于输入源类型(USB、RS232、pty、蓝牙、TCP、UDP)的信息似乎需要被传递到多路复用器层,告诉它,例如,探测字符串是否一个该被发送到一个身份不明的设备。有时候,这种探测需要唤醒RS232传感器,但是有充分的理由不把它们送到任何其他的设备上。很多GPS装置和其他传感器设备是在低预算下仓促地设计出来的,有些存在一些出乎意料的控制字符串,令人费解。
由于类似的原因,守护进程有一个-b
选项,防止它在数据包嗅探器嗅探循环中企图改变波特率。一些质量低劣的蓝牙设备对这些问题的处理过于糟糕,以至于它们不得不重启来重新工作。在极端情况下,一用户不得不拆卸备份电池。
以上两种情况在项目设计规则中是必要的例外。虽然这些例外更通常的是一件坏事。比如说,我们已经有一些有助于PPS时间服务更好地运行的补丁,但是他们破坏了垂直分层,PPS不能在超过一个驱动的情况下正常工作。我们努力保持设备类型独立,拒绝此类问题的产生。
几年前,有一次,有人要求我们支持一种GPS。这种GPS有着奇特的属性,当这个设备没有定位的时候,它NMEA数据包里的检验和可能是无效的。为了支持这个设备,我们不得不增加要么(a)放弃所有看起来像NMEA数据包的输入数据的检验和的有效性检验,这样的话会导致包嗅探器把垃圾信息传递给NMEA驱动;或者(b)添加一个命令行选项来强制传感器类型。
项目负责人(即本章的作者)拒绝了以上两种方案。放弃NMEA数据包有效性检验显然是一个坏主意。但是一个强制传感器类型的开关将会使项目不再有着合适的自动配置,这将在GPSD的客户端应用和用户方面都产生问题。而下一步必然需要波特率开关。于是,相反的,我们拒绝支持这个坏的设备。
一个项目首席架构师的最终要任务之一就是保护架构免受权宜之计的所谓“修正”。这种“修正”会破坏架构,产生功能问题或者严重的维护问题。对此可能有相当激烈的争论,尤其是当这个架构跟一些开发者或者用户认为必需的属性冲突时。但是这样的争论是必要的,因为一个最简单的选择也许长远来看会是一个错误。
7.6. 零配置,零麻烦
gpsd
一个非常重要的特点就是,它是一个零配置服务(除了一个固件坏了的蓝牙设备的小例外)。它没有隐藏文件!守护进程通过嗅探输入数据推测与之交互的传感器类型。对于RS232和USB设备,gpsd
甚至自动检测串口速度,所以守护进程根本没有必要事先知道传输信息的传感器的速度/校验/停止位。
如果主机操作系统有热插拔功能,热插拔脚本可以把设备激活和失活的信息传送给控制套接字来通知守护进程其环境变化。GPSD发行版为Linux提供这些脚本。其结果是,终端用户可以把一个USB GPS插入到他们的笔记本,并期望它立即开始提供位置感知应用可以阅读的报告。这个过程毫不混乱,不会让人烦躁,不用修改任何隐藏文件或偏好注册表。
这种方式的好处处处体现着。不考虑其他东西,这至少意味着位置感知应用不需要配置面板来调整GPS和端口设置。这节省了写应用的人和用户的大量精力:他们可以把位置作为一种像系统时钟一样简单的服务。
零配置理念的一个后果是,我们在增加配置文件和额外命令行选项上看起来不讨人喜欢。这带来一个麻烦,就是可以编辑的配置都必须编辑。这意味着给终端用户增加了配置麻烦,而这恰恰是一个设计良好的服务守护进程必须避免的。
GPSD开发者是一些深受Unix传统影响的Unix黑客,对他们而言配置和有很多的配置选项就像一种宗教信仰。然而,我们认为开源项目可以努力抛弃隐藏文件,根据运行环境自动配置。
7.7. 有用的嵌入式限制
从2005年以来,嵌入式部署的设计成为了GPSD的一个主要目标。这最初是因为我们对单板计算机的系统集成商感兴趣。但是之后,这项工作以一种意想不到的方式得到了报偿:部署GPS功能的智能手机。(虽然我们非常喜欢的嵌入式部署报告还是那些自动潜艇。)
设计嵌入式部署对GPSD产生了重要的影响。我们想了很多保持低内存占用和CPU消耗的方法使得代码可以在低速率、小内存、功率受限的系统上较好地运行。
正如前文所述,在这个问题上的一个重要攻击点,是保证gpsd
构件不要在一个特定的传感器协议集上有任何负载让系统集成商来支持。2011年6月,x86系统上的一个最低静态gpsd
构件在64位x86上内存占用为69K(所有需要的标准C库都链接进去)。相比之下,一个有所有驱动的静态构件大约为418K.
另一点就是,我们对CPU热点的探测跟大多数项目重点略有不同。因为位置传感器在1秒的时间间隔里往往只报告少量数据,在通常意义上这不是一个GPSD问题——再低效的代码也不太可能产生足以在应用曾观察到的延迟。事实上,我们主要关注的是降低处理器占用和功耗。对此我们相当成功:即使在低功耗的没有FPU的ARM系统上,gpsd
的CPU占用率也只有探测器噪音那么低的水平。
虽然小体积好功效的核心代码设计目前已经很好的解决了,面向嵌入式部署仍然在GPSD架构上造成一个方面的压力:脚本语言的使用。一方面,我们希望通过移除尽可能多不是C的代码来减少由于低级资源管理导致的缺陷。另一方面,Python(我们首选的脚本语言)对大多数嵌入式部署来说实在是太重量级太慢了。
我们明显地分裂成不同的两个部分:gpsd
服务守护进程是C写的,而测试框架和一些支持实用程序是Python写的。随着时间的推移,我们希望把更多的辅助代码从C移植成Python,但嵌入式部署使得这些选择持续存在争议和不适。
不过,总体上我们发现来自嵌入式部署的压力相当振奋人心。写占用处理器资源少的的代码感觉很好。有人说,艺术来自限制下的创造,在某种程度上这是一种事实,GPSD就是一个压力下变得更好的艺术品。
这种感觉并没有直接转换成其他项目的建议,但是有一些东西显然做到了:不要猜测,去测量!没有东西可以像定期分析和占用测量那样在你误入自我膨胀的时候提醒你其实你没有那么好。
7.8. JSON与架构
历史上,这个项目最重要的转变之一,是我们把原始的报告协议转换成了用JSON作为元协议,把报告以JSON对象格式传递给客户端。原始的报告协议使用了一个字母的关键词作为命令和响应。因此,随着守护进程能力的逐渐增加,我们逐渐用尽了文字上的关键词空间。
转换成JSON是一个巨大的胜利。JSON结合了传统的Unix纯文本格式的美和把结构化的信息以丰富、灵活的方式传递的能力。Unix纯文本格式容易用肉眼检查,容易用标准工具编辑,容易用编程产生。
通过把报告类型映射为JSON对象,我们确保了任何报告都可以包含字符串、数字、布尔型的结构化混合数据()这是以前的报告所没有的能力)。他哦难过确定一个class
属性的报告类型,我们保证了总是可以在不破坏原有报告类型的情况下增加新的报告类型。
这个决定并非没有代价。一个JSON解析器跟它替代掉的简单有限的解析器相比,在计算上略显昂贵,当然也需要更多行代码(意味着更多可能发生问题的地方)。此外,传统的JSON解析器需要动态内存分配来处理JSON描述的可变长数组和字典,而动态内存分布因大量的缺陷而臭名昭著。
我们用几种方法应对这些问题。第一步是为(足够)大的JSON子集写一个C解析器,让它们完全使用静态内存。这要求接受一些小的限制。比如,本地集中的对象不能包含JSONnull
值,数组总是有固定的最大长度。接受这些限制使得我们能够把解析器减少到600行的C代码。
然后,我们建立了一套全面的单元测试解析器,以验证无差错操作。最后,为JSON开销可能会过高的紧致嵌入式部署,我们写了一个共享内寻的出口,只要守护程序及其客户端有访问内存的权限,就能绕过运输和完全解析JSON。
JSON已经不只是为网络应用而存在的了。我们认为任何设计应用程序协议的人都应该考虑像GPSD这样的方式。当然,把你的协议建立在标准元协议之上的想法并不新鲜,XML迷已经推崇了好多年,并且这对类似文件结构的协议很有意义。JSON比XML有更低的开销,能更好的适应数组传递和记录结构。
7.9. 无缺陷设计
由于其在导航系统中的应用,任何用户和GPS或者其他位置传感器之间的软件都有可能跟生命有关,特别是在海上或者空中使用。开源导航软件常常试图逃避这个问题,发表免责声明说:“如果这么做会让生命处于危险之中的话,就不要依赖它。”
我们认为这样的免责声明是徒劳的和危险的:说是徒劳的,是因为系统集成商很可能把他们当作形式主义而忽略他们;说是危险的,是因为他们鼓励开发者欺骗自己说代码缺陷不会造成严重的后果,而且偷工减料的后果在质量上还是有可以接受的保障的。
GPSD项目开发者认为,唯一可以接受的是无缺陷设计。由于这样的软件复杂性,我们还没有完全实现,但是对于一个项目来说,GPSD的大小、年纪、复杂性已经非常接近了。
我们做这件事的策略是把体系结构和代码政策结合起来,旨在排除附带代码存在缺陷的可能性。
其中一个重要的政策是这样的:gpsd
上的守护从来没有使用动态内存分配——没有malloc
或calloc
,也没有到需要它的函数或库的调用,避免了在C中一个最臭名昭著的缺陷引入。我们没有内存泄漏,没有重复内存分配或重复内存释放的错误,而且我们永远都不会有。
我们能够远离这一点,是因为我们处理的所有传感器发出的数据包都有相对小的固定最大长度,而守护进程的工作是消化它们,并把它们用最小的缓存运到客户端。尽管如此,消除malloc
需要编码规则和一些设计上的妥协,我们前面在讨论JSON解析器时已经提到了一些。我们愿意付出这些代价来减少我们的缺陷率。
这项政策的一个有用的副作用是,它增加了静态代码检查器的效率,比如splint
、cppcheck
和Coverity
。这导致另一个主要的政策选择,我们非常大量使用这些代码审计工具和自定义的回归测试框架。(我们不知道任何完全splint
-标注的大于GPSD的程序套件,并强烈怀疑目前没有这样的存在。)
GPSD高度模块化的架构也在这一点上帮助了我们。该模块的边界作为切点,在这里我们可以装配测试工具,我们已经在很系统地这样做。我们正常的回归测试检查一切,从主机硬件的浮点行为,通过JSON的解析,到纠正超过70个不同传感器日志的报告行为。
当然,在比许多应用程序严格的同时我们也有稍微轻松一点的时候,因为守护进程没有面向用户的接口,它周围的环境只是一堆的串行数据流,是比较容易模拟。不过,随着消除malloc
,实际上利用这一优势需要正确的态度,这意味着在做产品代码的时候,愿意花尽可能多设计编码时间在测试工具上。我们认为其他开源项目可以而且应该效仿这种政策。
在我写这篇文章的时候(2011年7月),GPSD的项目bug跟踪系统是空的。它已经空了几个星期,根据过去提交的错误率,我们可以期望它持久保持这种状态。我们在六年中从来没有因为崩溃修改过代码。当我们真的有bug的时候,它们往往只是少量特征的丢失或者跟说明有点不相符,很容易在几分钟之内修复。
这并不是说,该项目一直很成功。下一步,我们将回顾一些我们的失误……
7.10. 经验教训
软件设计是困难的,其中往往充斥着错误和盲目,GPSD也不例外。这个项目历史上最大的失误就是用来请求和报告GPS信息的前JSON协议的最初设计。我们花了多年的努力从这个错误中恢复过来。不管从最初的错误设计中,还是恢复过程中,我们都学到了很多经验教训。
原协议有两个严重的问题:
扩展性不佳。它使用请求和响应标签,每个标签都已一个单一的字母,不区分大小写。因此,例如,请求报告的经度和纬度用
“P”
,一个响应类似“P-75.3240.05”
。此外,解析器把请求“PA”
解释为一个“P”
要求后跟着一个“A”
(高度)请求。随着守护进程的功能逐步增加,我们用尽了字母的命令空间。协议隐式模型的传感器行为和他们的实际行为之间的不匹配。旧协议是请求/响应模式的:发送位置请求(高度或者其他),一段时间后得到一个报告。事实上,它通常是不可能请求一个GPS或其他与导航传感器的报告,他们自己流出报告,而一个请求可以做的最多是查询缓存。这种不匹配催生了应用程序草率的数据处理。很多时候,他们会请求一个不要求时间戳或任何检查信息的位置数据,这种做法很容易导致过时或无效数据被呈现给用户。
早在2006年,我们就清楚地认识到旧协议设计的不足,但是它花了近三年来设计草图,开始设计新的协议。之后又过了两年的过渡期,并给客户端应用程序的开发人员造成了一定的痛苦。要不是这个项目把隐藏了大多数协议细节的客户端库传给客户,这将造成更大的开销——但我们最初并没有完全正确地得到这些库的API
如果当时我们知道这些的话,基于JSON的协议将早介绍5年,客户端库的API设计会少很多修改。但只有经验和实践可以教会我们这些教训。
未来的服务伺服器可以从我们的失误中学到至少两条设计理念:
可扩展的设计。如果你的守护进程的应用程序有可能像我们的旧协议那样用尽命名空间,你就做错了。高估短期成本,低估像XML和JSON这样的元协议的长远利益是很常见的错误。
客户端库比暴露应用程序协议细节好。一个库能够适应多个版本的应用协议,大大降低了接口的复杂性和缺陷率,相比之下,另一种方式下,每个应用程序编写者需要建立一个特定捆绑。这种差异将直接转化为您的项目跟踪器上更少的错误报告。
对于我们强调可扩展性的一种可能应答,是觉得这是不必要的,应该被消除。这种应答不仅是对于GPSD的应用协议的,还对项目架构的其他方面,如数据包驱动程序接口。 Unix程序员一直接受“做好一件事”的传统教育。他们可能会问gpsd
的命令集是否真的有必要像2011年的时候那么大,而不是维持2006年时候那样;为什么gpsd
现在要处理非GPS传感器,比如磁罗盘和航海AIS接收器;为什么我们要考虑ADS-B飞机跟踪这种可能性。
这些都是公平问题。我们可以通过观察添加新设备类型的实际复杂度成本来接近一个答案。由于一些很好的理由,包括历史上与传感器串行线相关的相对低的数据量和高的电噪声水平,几乎所有的GPS装置和其他与导航相关的传感器报告协议看起来大同小异:小的数据包,并带有某种有效性校验和。这样的协议看起来处理很复杂,但事实上并不是真的很难相互区分和解析,添加一个新的协议的增量成本往往是小于一千行代码的。即使是我们支持的最复杂的协议,并且带有自己的报告生成器,如航海AIS,也只需要三千行代码。整体而言,驱动器加上数据包嗅探器及其相关的JSON报告生成器总共有大约18千行代码。
相比整个项目的43千行代码,我们可以看到,GPSD大部分的复杂性成本实际上是在围绕着驱动的周围框架代码以及(很重要地)在于测试工具和验证守护程序正确性的框架上。多写这些将是一个比写任何单独的数据包解析器更大的项目。因此,为一个GPSD不支持的包协议写一个GPSD等价物的工作量远远大于向GPSD添加另一个驱动和测试集。相反,最经济的结果(和一个最低预期累计的缺陷率)是让GPSD增加不同传感器类型的数据包驱动。
GPSD已经做好的“一件事”是处理任何传送可识别检验和包的传感器集合。那些看起来像画蛇添足,实际上是防止不得不写入许多不同的和重复的处理程序守护进程。相反,应用程序开发人员获得一个相对简单的API和我们来之不易的对越来越多的传感器类型的设计和测试经验。
把GPSD和简单的画蛇添足去分开来的不是运气或黑魔法,而是对已知的最佳实践在软件工程中的仔细应用。这些特性的回报在目前看来是低缺陷率,而将来还会持续的有着以少量工作或者缺陷率影响为代价支持新特性的能力。
也许这个项目对其他开源项目最重要的经验是:让缺陷率接近零是困难的,但不是不可能——甚至对于一个不像GPSD这样广泛部署的项目也不是不可能。合理的体系结构,良好的编码习惯,和一个非常确定的重点测试就可以实现它——而最重要的前提就是做到这三点。