當協定的主要目的是傳輸文字時,文字協定是很好的選擇——這也是郵件傳輸協定、即時通訊與新聞聚合協定通常採用文字格式的原因。
文字協定必須具備與二進位協定類似的結構。原因很簡單:兩者的主要內容雖然不同,但目標一致——把資料從一處送到另一處。
數值資料#
千百年來,科學與書寫語言發明了各種以文字表示數值的方法。電腦協定當然不需要人類可讀,但除非你的目的就是刻意混淆,否則沒有必要特地讓協定變得不可讀。
整數#
用目前字元集中 0 到 9 的字元(若是十六進位則加上 A 到 F)來表示整數值非常容易。在這種簡單表示法下,大小限制不成問題——數字若需要大於二進位字組長度,多加幾位數就好。
要表示有號數,在數字前加上減號 - 即可;正數的加號 + 則是隱含的。
「多加幾位數就好」的另一面是:你最好祈禱協定解析器能處理這些多出來的位數,否則安全問題必然出現。
小數#
小數通常以人類可讀的形式定義,例如寫成 1.234,用點字元分隔整數與小數部分。不過之後解析這個值的需求仍然必須納入考量。
浮點這類二進位表示法無法以有限精度精確表示所有十進位值(就像十進位無法精確表示 1/3 一樣)。這使得某些值在文字格式下難以表示,也可能造成安全問題,尤其是在值與值互相比較的時候。
文字布林值#
布林值在文字協定中很好表示,通常就用 true 或 false 這兩個字。但有些協定偏偏要求大小寫必須完全正確才算有效。有時也會改用整數值,例如 0 表示假、1 表示真,不過這種做法並不常見。
日期與時間#
簡單來說,日期與時間很容易編碼:照人類可讀語言中書寫的樣子表示就好。只要所有應用程式對表示法有共識,這樣就夠了。
麻煩在於大家無法就標準格式達成共識,於是通常同時存在許多互相競爭的日期表示法。這在郵件客戶端這類需要處理各國日期格式的應用程式上,是特別棘手的問題。
變長資料#
除了最簡單的協定之外,所有協定都必須有辦法分隔重要的文字欄位,才能順利解讀。當一個文字欄位從原始協定中被分離出來時,通常稱之為符記(token)。有些協定為 token 指定固定長度,但要求某種變長資料的做法遠為常見。
分隔式文字#
用分隔字元來區分 token 與欄位是非常常見的做法,容易理解、也容易建構與解析。任何字元都可以當分隔符(取決於傳輸的資料型態),但在人類可讀格式中最常遇到的是空白字元。
分隔符不一定得是空白。例如金融資訊交換協定(Financial Information Exchange, FIX)就用值為 1 的 ASCII Start of Header(SOH)字元來分隔 token。
終止式文字#
有辦法分隔個別 token 的協定,也必須有辦法定義「指令結束」的條件。如果協定被拆成獨立的行,那些行就必須以某種方式終止。
大多數知名的文字型網際網路協定都是行導向的,例如 HTTP 與 IRC;行通常用來界定整個結構的邊界,例如一個指令的結束。
那麼什麼算是行尾字元?這要看你問誰:
| 行尾表示 | ASCII 值 |
|---|---|
| Line Feed(LF) | 10 |
| Carriage Return(CR) | 13 |
| CR LF 組合 | 13, 10 |
HTTP 與簡單郵件傳輸協定(Simple Mail Transfer Protocol, SMTP)等協定明定 CR LF 為官方的行尾組合。
但因為不正確的實作實在太多,大多數解析器也會接受單獨的 LF 作為行尾指示。這種「規格說一套、實作接受另一套」的落差,正是分析協定時值得留意的地方。
結構化文字格式#
就像 ASN.1 這類結構化二進位格式一樣,當你想在文字協定中表示結構化資料時,通常也沒有理由重造輪子。你可以把結構化文字格式想成「加強版的分隔式文字」——正因如此,它必須制定值如何表示、階層如何構成的規則。以下是三種在真實世界文字協定中常見的格式。
MIME#
多用途網際網路郵件擴充(Multipurpose Internet Mail Extensions, MIME)最初是為了傳送多部分(multipart)電子郵件訊息而開發,後來進入 HTTP 等多種協定。它的規格定義在 RFC 2045、2046、2047 以及其他許多相關 RFC 中,規範了如何在單一 MIME 編碼訊息中編入多個離散附件。
MIME 訊息以一條前綴兩個破折號 -- 的共同分隔行來區隔各個 body part,而訊息的結束則是在該分隔行後再加上同樣的兩個破折號。
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary=MSG_2934894829
This is a message with multiple parts in MIME format.
--MSG_2934894829
Content-Type: text/plain
Hello World!
--MSG_2934894829
Content-Type: application/octet-stream
Content-Transfer-Encoding: base64
PGh0bWw+Cjxib2R5PgpIZWxsbyBXb3JsZCEKPC9ib2R5Pgo8L2h0bWw+Cg==
--MSG_2934894829--MIME 型別是 MIME 最常見的用途之一,指的其實就是 Content-Type 值。它廣泛用於提供 HTTP 內容,也用在作業系統中把應用程式對映到特定內容型別。每個型別由資料的形式(例如 text 或 application)與資料的格式組成——上例中 plain 是未編碼文字,octet-stream 則是一串位元組。
JSON#
JavaScript 物件表示法(JavaScript Object Notation, JSON)設計為一種簡單的結構表示法,基於 JavaScript 程式語言提供的物件格式。它最初用於在瀏覽器網頁與後端服務之間傳輸資料,例如 AJAX(Asynchronous JavaScript and XML);目前則廣泛用於 Web 服務資料傳輸與各式各樣的協定。
JSON 格式很簡單:JSON 物件以大括號 {} 這組 ASCII 字元包住,括號內是零個或多個成員項目,每個項目由一個鍵與一個值組成。
{
"index": 0,
"str": "Hello World!",
"arr": ["A", "B"]
}JSON 格式是為 JavaScript 處理而設計的,可以用
eval函式解析——但這麼做帶有重大安全風險:在物件建立過程中可以插入任意腳本程式碼。雖然多數現代應用程式使用不需連結 JavaScript 的解析函式庫,仍應確保應用程式脈絡中不會執行任意 JavaScript 程式碼,否則可能導致跨站腳本(cross-site scripting, XSS)這類安全問題——攻擊者控制的 JavaScript 在另一個網頁的脈絡中執行,讓攻擊者得以存取該頁面的安全資源。
XML#
可延伸標記語言(Extensible Markup Language, XML)是一種描述結構化文件格式的標記語言。它由 W3C 開發,衍生自標準通用標記語言(Standard Generalized Markup Language, SGML)。它與 HTML 有許多相似之處,但目標是在定義上更嚴格,以簡化解析器並減少安全問題。
XML 的基本組成有三種:
- 元素(element):主要的結構值。有名稱,可包含子元素或文字內容。單一文件中只允許一個根元素。
- 屬性(attribute):可指派給元素的額外名稱-值對,形式為
name="Value"。 - 文字(text):文字內容本身,是元素的子節點或屬性的值部分。
<value index="0">
<str>Hello World!</str>
<arr><value>A</value><value>B</value></arr>
</value>所有 XML 資料都是文字,XML 規格本身不提供型別資訊,因此解析器必須自行知道這些值代表什麼。XML Schema 之類的規格試圖彌補這個型別資訊上的不足,但處理 XML 內容時並不強制使用它們。XML 規格另外定義了一組格式良好(well-formed)準則,用來判斷文件是否達到最低限度的結構要求。
XML 被用在許多地方來定義協定中資訊的傳輸方式,例如 RSS(Rich Site Summary);它也可以是協定的一部分,例如 XMPP(Extensible Messaging and Presence Protocol)。