> 原文鏈接:?[http://www.aosabook.org/en/gpsd.html](http://www.aosabook.org/en/gpsd.html)
> 作者:[Eric Raymond](http://www.aosabook.org/en/intro2.html#raymond-eric)
> 譯者:[Liuli Chen](https://sites.google.com/site/liulichencs)(陳旒俐)
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](http://www.aosabook.org/en/bib2.html#gps-suck)]看到詳細的討論。尤其是,你會在哪里發現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](http://en.wikipedia.org/wiki/ARM_architecture)。)
分層的第三個好處是守護進程多路復用器可以脫離核心庫之上,取而代之的是簡單的邏輯,比如`gpsdecode`公用程序直接把傳感器日志文件批量轉換成JSON報告。
GPSD的這部分架構沒有什么新奇的。它的教訓是,有意識的嚴格的按照Unix設備設計模式設計應用不僅在操作系統內核上有益,在類似的要求跟很多硬件和協議打交道的用戶空間程序上也是有益的。
**7.4\. 數據流視圖**
現在,讓我們從數據流視圖賴考慮GPSD的架構。在正常操作中,gpsd循環等待以下幾個源的輸入:
1. 一組在TCP/IP端口發出請求的客戶端。
2. 一組通過串行設備或者USB設備相連的導航傳感器。
3. 熱插拔腳本所用的特殊控制套接字和一些配置工具。
4. 一組定期發布差分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解析器,讓它們完全使用靜態內存。這要求接受一些小的限制。比如,本地集中的對象不能包含JSON`null`值,數組總是有固定的最大長度。接受這些限制使得我們能夠把解析器減少到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協議的最初設計。我們花了多年的努力從這個錯誤中恢復過來。不管從最初的錯誤設計中,還是恢復過程中,我們都學到了很多經驗教訓。
原協議有兩個嚴重的問題:
1. 擴展性不佳。它使用請求和響應標簽,每個標簽都已一個單一的字母,不區分大小寫。因此,例如,請求報告的經度和緯度用`“P”`,一個響應類似`“P-75.3240.05”`。此外,解析器把請求`“PA”`解釋為一個`“P”`要求后跟著一個`“A”`(高度)請求。隨著守護進程的功能逐步增加,我們用盡了字母的命令空間。
2. 協議隱式模型的傳感器行為和他們的實際行為之間的不匹配。舊協議是請求/響應模式的:發送位置請求(高度或者其他),一段時間后得到一個報告。事實上,它通常是不可能請求一個GPS或其他與導航傳感器的報告,他們自己流出報告,而一個請求可以做的最多是查詢緩存。這種不匹配催生了應用程序草率的數據處理。很多時候,他們會請求一個不要求時間戳或任何檢查信息的位置數據,這種做法很容易導致過時或無效數據被呈現給用戶。
早在2006年,我們就清楚地認識到舊協議設計的不足,但是它花了近三年來設計草圖,開始設計新的協議。之后又過了兩年的過渡期,并給客戶端應用程序的開發人員造成了一定的痛苦。要不是這個項目把隱藏了大多數協議細節的客戶端庫傳給客戶,這將造成更大的開銷——但我們最初并沒有完全正確地得到這些庫的API
如果當時我們知道這些的話,基于JSON的協議將早介紹5年,客戶端庫的API設計會少很多修改。但只有經驗和實踐可以教會我們這些教訓。
未來的服務伺服器可以從我們的失誤中學到至少兩條設計理念:
1. 可擴展的設計。如果你的守護進程的應用程序有可能像我們的舊協議那樣用盡命名空間,你就做錯了。高估短期成本,低估像XML和JSON這樣的元協議的長遠利益是很常見的錯誤。
2. 客戶端庫比暴露應用程序協議細節好。一個庫能夠適應多個版本的應用協議,大大降低了接口的復雜性和缺陷率,相比之下,另一種方式下,每個應用程序編寫者需要建立一個特定捆綁。這種差異將直接轉化為您的項目跟蹤器上更少的錯誤報告。
對于我們強調可擴展性的一種可能應答,是覺得這是不必要的,應該被消除。這種應答不僅是對于GPSD的應用協議的,還對項目架構的其他方面,如數據包驅動程序接口。 Unix程序員一直接受“做好一件事”的傳統教育。他們可能會問`gpsd`的命令集是否真的有必要像2011年的時候那么大,而不是維持2006年時候那樣;為什么`gpsd`現在要處理非GPS傳感器,比如磁羅盤和航海AIS接收器;為什么我們要考慮ADS-B飛機跟蹤這種可能性。
這些都是公平問題。我們可以通過觀察添加新設備類型的實際復雜度成本來接近一個答案。由于一些很好的理由,包括歷史上與傳感器串行線相關的相對低的數據量和高的電噪聲水平,幾乎所有的GPS裝置和其他與導航相關的傳感器報告協議看起來大同小異:小的數據包,并帶有某種有效性校驗和。這樣的協議看起來處理很復雜,但事實上并不是真的很難相互區分和解析,添加一個新的協議的增量成本往往是小于一千行代碼的。即使是我們支持的最復雜的協議,并且帶有自己的報告生成器,如航海AIS,也只需要三千行代碼。整體而言,驅動器加上數據包嗅探器及其相關的JSON報告生成器總共有大約18千行代碼。
相比整個項目的43千行代碼,我們可以看到,GPSD大部分的復雜性成本實際上是在圍繞著驅動的周圍框架代碼以及(很重要地)在于測試工具和驗證守護程序正確性的框架上。多寫這些將是一個比寫任何單獨的數據包解析器更大的項目。因此,為一個GPSD不支持的包協議寫一個GPSD等價物的工作量遠遠大于向GPSD添加另一個驅動和測試集。相反,最經濟的結果(和一個最低預期累計的缺陷率)是讓GPSD增加不同傳感器類型的數據包驅動。
GPSD已經做好的“一件事”是處理任何傳送可識別檢驗和包的傳感器集合。那些看起來像畫蛇添足,實際上是防止不得不寫入許多不同的和重復的處理程序守護進程。相反,應用程序開發人員獲得一個相對簡單的API和我們來之不易的對越來越多的傳感器類型的設計和測試經驗。
把GPSD和簡單的畫蛇添足去分開來的不是運氣或黑魔法,而是對已知的最佳實踐在軟件工程中的仔細應用。這些特性的回報在目前看來是低缺陷率,而將來還會持續的有著以少量工作或者缺陷率影響為代價支持新特性的能力。
也許這個項目對其他開源項目最重要的經驗是:讓缺陷率接近零是困難的,但不是不可能——甚至對于一個不像GPSD這樣廣泛部署的項目也不是不可能。合理的體系結構,良好的編碼習慣,和一個非常確定的重點測試就可以實現它——而最重要的前提就是做到這三點。
- 前言(卷一)
- 卷1:第1章 Asterisk
- 卷1:第3章 The Bourne-Again Shell
- 卷1:第5章 CMake
- 卷1:第6章 Eclipse之一
- 卷1:第6章 Eclipse之二
- 卷1:第6章 Eclipse之三
- 卷1:第8章 HDFS——Hadoop分布式文件系統之一
- 卷1:第8章 HDFS——Hadoop分布式文件系統之二
- 卷1:第8章 HDFS——Hadoop分布式文件系統
- 卷1:第12章 Mercurial
- 卷1:第13章 NoSQL生態系統
- 卷1:第14章 Python打包工具
- 卷1:第15章 Riak與Erlang/OTP
- 卷1:第16章 Selenium WebDriver
- 卷1:第18章 SnowFlock
- 卷1:第22章 Violet
- 卷1:第24章 VTK
- 卷1:第25章 韋諾之戰
- 卷2:第1章 可擴展Web架構與分布式系統之一
- 卷2:第1章 可擴展Web架構與分布式系統之二
- 卷2:第2章 Firefox發布工程
- 卷2:第3章 FreeRTOS
- 卷2:第4章 GDB
- 卷2:第5章 Glasgow Haskell編譯器
- 卷2:第6章 Git
- 卷2:第7章 GPSD
- 卷2:第9章 ITK
- 卷2:第11章 matplotlib
- 卷2:第12章 MediaWiki之一
- 卷2:第12章 MediaWiki之二
- 卷2:第13章 Moodle
- 卷2:第14章 NginX
- 卷2:第15章 Open MPI
- 卷2:第18章 Puppet part 1
- 卷2:第18章 Puppet part 2
- 卷2:第19章 PyPy
- 卷2:第20章 SQLAlchemy
- 卷2:第21章 Twisted
- 卷2:第22章 Yesod
- 卷2:第24章 ZeroMQ