xmppmini 項目詳解:一步一步從原理跟我學實用 xmpp 技術開發 4.字符串解碼祕笈與消息包

    這一節寫得比較長,不過若是您確實要手工解碼 xmpp 消息,仍是建議您仔細看看,並且事實上並不複雜。前端


    登陸成功後,咱們就能夠收發消息了。消息包的格式大體以下:

java

1 <message id="JgXN5-33" to="clq@127.0.0.1" from="ccc@127.0.0.1/Spark" type="chat">
2   <body>hi,你好啊。</body>
3 </message>


    其實消息包也有兼容性問題,最多的就是各個客戶端或者服務器會加入本身的一些擴展節點。其實從簡化協議出發,這些擴展放到消息體自己更好,還能夠兼容其餘通訊協議,咱們 xmppmini 項目就是這樣作的。不過這都是後話,咱們目前的當務之急是收到消息時如何解碼這個消息包。
    先給你們一個驚喜和定心丸:我保證只要一個函數就能夠完成這個解碼。真的!咱們先來看看傳統的消息解碼是怎樣的。通常的消息流解碼,特別是 xml 或者開發語言的源代碼解析用的比較多的會先從字節流中分隔出各個節點,而後根據各自的規則造成一個樹形結構。這種作法一是比較複雜,從開源的角度來看,我的開發者去實現太耗時了,這也是爲何只要是涉及到編解碼的通常都會上第三方庫。另外還有一個很是大的缺點,就是 xml 解碼的二義性太嚴重了,包括 json 解碼庫也是,包括不少著名的庫直到如今仍是有不少特殊狀況下沒法正確解碼的狀況。在寫這篇文章不久前我還看到一個 golang 開發組的 bug 報告,就是關於某種狀況下還沒法正確解碼 xml 的狀況。
    另外還有一種常見的方法就是上正則表達式,我我的是很是鄙視正則表達式的。先不說它的解碼錯誤和二義性也很嚴重,那一串每次重寫都要去查一下正則語法的規則字符串,我一看就倒胃口,能夠說用正則表達式實現的代碼維護性是很是差的。實際上這些看上去貌似很複雜的字符串,只要用一個字符串分隔函數就能夠實現。下面咱們就來具體介紹。
這個函數是我在多年前編寫郵件客戶端程序 《eEmail》 時實現的,目的就是用來解碼 smtp/pop3 的消息。其實也是能夠用於 xmpp/xml 包的。並且原理簡單易懂,很是值得給大夥仔細介紹一下
    首先,咱們考慮下收取到如下格式的消息時如何取得裏面的內容:

nginx

1 key=value
2 Key=value
3 key=value;
4 Key=value;


注意 「key」 中有大小寫的狀況,由於這在網絡包中是很是覺的現象,各個實現對某個標誌的大小寫並不一致。另外再注意 「value」 後有時會有 「;」 符號。有 web 前端開發的讀者應該很是熟悉這種狀況了。

而咱們要設計的這個處理函數須要達到如下的這種效果:

程序員

1 get_value('key=value', '=', ';');  //應當爲 value
2 get_value('key=value', '', '=');   //應當爲 key
3 get_value('key=value;', '=', ';'); //應當爲 value
4 get_value('key=value;', '', '=');  //應當爲 key


    實際上就是取兩個分隔符號之間的字符串,而這兩個分隔符號還不是相同的。同時就考慮了沒有第一個分隔符號或者是沒有第二個分隔符的狀況。你們先不用思考以上的結果,由於確實這時候仍是有點複雜,特別是沒有某個分隔符的狀況下是比較難處理的。
    最初我設計出這個函數後非常好用,基本上再配合一些常規的字符串查找、切割函數就能夠解決解碼問題了。但這個函數有個問題,就是設計得太過精巧,當多年後 golang 語言出現,我要移植代碼時發現,當年的處理思想我已經忘記了,從新再寫一次的時候,處理的結果並不徹底同樣!這顯然不合適,由於這個 xmpp 的庫還得出 C#、java、純C等等多個版本,我本身都實現很差,還怎麼介紹方法給別人實現?所以苦苦思索怎樣切分紅幾個簡單邏輯的處理函數去組合完成相同的任務。通過近兩天的折騰,功夫不負有心有,我確實發現它的處理過程能夠拆分紅幾個簡單的函數。更棒的這些簡單函數最後再均可以用一個更簡單的函數來組合完成。
    這層窗戶紙捅破一點也不稀奇,只要一個最簡單的字符串分隔函數就能夠了。好比將字符串 「123abc456」 分割成兩個字符串 「123」、」456」 就能夠了。很差!眼尖的讀者必定發現了什麼。您這個不就是字符串分割函數嘛,不用寫啦,全部的開發語言幾乎都有嘛!沒錯!不過這些分割函數是有不少問題的,並且相互之間竟然也有兼容性問題!
    以我最先實現的 Delphi 版本和最後實現的 golang 版本爲例。Delphi 的分隔函數默認狀況下會在你不知情的狀況下把空格、tab字符也當作分隔符號。因此當您用它的默認字符串分隔函數時會出現不少意料以外的結果讓你苦苦調試而不得其因此然。而 golang 的分隔也有這樣的問題。還有些語言是用正則表達式實現的,其結果有時更是天馬行空。緣由其實也很簡單,由於它們這些分隔函數自己是用來分隔多個字符串片斷,並且還考慮了經常使用的分隔符號的狀況。而咱們須要的是一個明確的只分隔指定分隔符,並且只將字符串分爲兩段的函數。
    再考慮如下字符串:golang

1,2,3,4,5


    通過咱們本身寫的函數後它須要分紅 「1」、」2,3,4,5」 兩個部分。而若是是 golang 的默認實現就有多是 「1」、」2」。然後面的不見了,由於它把第二個 「,」也當作了分隔符。
實際上咱們要實現的這個字符串分隔函數功能更簡單,它只用處理第一個分隔符就好了。因此手工實現是很是簡單的,任何一個程序員均可以作到。
    具體的實現那就很簡單了,先在源字符串中查找分隔符字符串的位置,而後切割後再來去掉分隔符號自己就能夠了。這隻要利用開發語言都會有的字符串查找和分隔功能就能夠完成了。很是的簡單,僞碼以下。不過要注意的是不一樣語言對字符串位置的表達並不徹底同樣,大多數語言將字符串的第一個起始位置定爲 0 ,而有些則是 1;在切割字符串的時候也要注意,有些開發語言在長度超過或者不足時會作出不一樣的處理,有些返回整個字符串,有些返回空,有些則是有多少就返回多少。具體的就須要你們實現時多留心了。


web

 1 //一個字符串根據分隔符的第一個位置分隔成兩個
 2 void sp_str_two(string in_s, sp, string out s_left, string out  s_right)
 3 {
 4   //開始複製的位置
 5   Int find_pos;      //查找到的位置
 6   Int left_last_pos; //左邊字符串的最後一個字符的位置
 7 
 8   find_pos = pos(lowercase(sp), lowercase(in_s)); //不要區分大小寫
 9 
10   if (Length(sp)<1 ) find_pos = 0; //沒有分隔符就當作沒找處處理
11 
12   if find_pos <= 0        //沒找到分隔符號,當即返回,這時左邊是原字符串,右邊是空字符串,相似於分隔成數組後的 【索引1】 和 【索引2】 中的內容
13   {
14     s_left = in_s;
15     s_right = '';
16     return;
17 
18   };
19 
20   left_last_pos = find_pos - 1; //由於結束符號自己是不須要的,因此查找到的位置向前移一位纔是咱們要的最後一個字符
21 
22   //取左邊
23   s_left = copy(in_s, 1, left_last_pos); 

/* 由於delphi 字符串位置是從 1 開始計算的,因此字符所在的位置就是包含它的整個字符串的長度了,不須要再加 1 或者減 1 這樣的計算
其它的語言要根據實際狀況修改這部分代碼。大多數開發語言通常是要從 0 開始計算字符串位置的。 */ 24 25 //---- 26 //取右邊 27 find_pos = find_pos + (length(sp)); //起始位置還要跳過度隔符號的長度 28 s_right = copy(in_s, find_pos, length(in_s)); //先去掉起始分隔符號以前的部分(分隔符自己也不要) 29 30 }


    這裏有個地方是值得注意的:這樣分隔出的字符串是再也不包含分隔符了的。但在實際的工做中,其實有時候是須要帶上分隔符號的。我本想加上一個默認參數來決定是否在結果中帶在分隔符。但在實際工做中發現這樣並不方便,首先多了一個參數,你在工做中看到這個函數時都會中斷一下斷思路心想這其中的區別(雖然很細微的停頓)。這在行雲流水的工做過程當中是個大忌(至少對我來講)。再說了,如今新興的語言好比 java、golang 等爲了不二義性是不支持默認參數的。固然能夠再拆分紅多個函數來解決,但這樣的話打斷思路的問題仍然是存在的。因此最後我決定仍是保持它的簡單性,分隔的時候咱們確定是知道分隔符是什麼的,在須要的地方再給它加回去就好了。雖然這種方法看上去有點傻,不過在實際的開發中得以保持了思惟邏輯上的清晰性和簡單性。
    有了這個函數,能夠很容易的實現出取一個字符串分隔符左邊部分的函數,以及一個取字符串分隔符右邊部分的函數。僞碼以下:

正則表達式

 1 //將字符串分隔成兩半,不要用系統自帶的分隔字符串爲數組的函數,由於那樣的話沒法處理字符串中有多個分隔符號的狀況
 2 //這個函數是在字符串第一次出現的地方進行分隔,其餘的地方再出現的話再也不理會,這樣才能處理 xml 這樣標記多層嵌套的狀況
 3 //b_get_left 取分隔後字符串的左邊仍是右邊
 4 string sp_str(string in_s, sp, bool b_get_left)
 5 {
 6   String s_left;         //左邊的字符串
 7   String s_right;        //右邊的字符串
 8 
 9   sp_str_two(in_s, sp, s_left, s_right);
10 
11   //----
12   result = s_left;
13   if (False = b_get_left)  result = s_right;
14 
15   return result;
16 };
17 
18 //分隔字符串取左邊
19 string sp_str_left(string in_s, sp)
20 {
21   return sp_str(in_s, sp, true);
22 
23 }
24 
25 //分隔字符串取右邊
26 string sp_str_right(string in_s, sp)
27 {
28   return sp_str(in_s, sp, false);
29 
30 }


好了,最後咱們要實現 get_value() 函數自己了。這裏是要特別注意的。有了前面的基礎函數後,要實現 get_value() 也是很簡單的。但完成後必定要用前述的函數操做預計的結果做爲測試用例來測試一下,如下的代碼中調用順序細微的變化就可能引發結果的不一樣。代碼以下:算法

 1 string get_value_sp(string in_s, b_sp, e_sp)
 2 {
 3   Result = in_s;
 4 
 5   if (Length(b_sp)<1) //左邊分隔符號爲空就表示只要右分隔符號以前的
 6   {
 7     Result = sp_str_left(Result, e_sp);
 8     Return result;
 9   };
10 
11   if (Length(e_sp)<1) //右邊分隔符號爲空就表示只要左分隔符號以後的
12   {
13     Result = sp_str_right(Result, b_sp);
14     Return result;
15   };
16 
17   //二者都有就取分隔符號之間的
18   Result = sp_str_right(Result, b_sp);
19   Result = sp_str_left(Result, e_sp);
20   //Result = sp_str_left(Result, b_sp);
21 
22   return result;
23 }


    有了這些函數後,讓咱們來看看如何簡單的就能夠解碼文章最開始時的那個消息包。
    首先咱們要肯定字符串中已經包括完整的消息包。這個用前幾章中的函數直接 FindStr()查找是否包含有子字符串 「/message>」 就能夠了。
    第二步,肯定緩衝區中的內容含有完整消息包,就能夠直接調用 get_value() 取得消息包了。數據庫

1 s = get_value(gRecvBuf, '<message', '</message >');
2 
3 msg = get_value(s, '<body>', '</body>');


這時 s 的內容就是編程

「
id="JgXN5-33" to="clq@127.0.0.1" from="ccc@127.0.0.1/Spark" type="chat">
  <body>hi,你好啊。</body>


而 msg 的內容則是

「
hi,你好啊。
」


要注意的是第一個調用位置的起始分隔符號是 「<message」,而不是 「<message>」 ,這是由於 message 包中還附帶有屬性節點。而這些地節點不存在的狀況下,用分隔符「<message 」也同樣能取得須要的字符串。這些節點包括髮送者的地址,使用 get_value() 函數也很容易取得:

1 from = get_value(s, ' from="',  '"');


    你們要仔細看這行代碼,第一個分隔符以前是必須有加上一個空格。由於不加的話就可能取到 「afrom」或者「bfrom」這些節點的內容。    能夠看到咱們很容易的就解碼了這一 xmpp 的消息節點。由於 xmpp 的消息比較規範整齊因此這樣處理就能夠了。若是是用來解碼手寫的 xml 文件的話則能夠加上一些預先處理:好比去除連續的空格;將 tab、回車、換行轉換爲空格等等,固然還要考慮 「message」 有多層次的狀況。其實也都不難,不過 xmpp 中並無這種狀況,咱們就按下不表了。    這種解碼方式其實還有一個問題:就是解碼效率。主要是字符切割再分配內存會影響一些處理速度。這裏一來咱們主要是說原理,二來讀者大部分確定開發的是客戶端,不必太優化執行速度。若是是服務端的開發者,那麼優化的方向就是直接實現出 get_value,不過若是是我本人優化我不會改用這種方式,由於我以爲代碼可維護性更重要。若是是 C 語言,能夠將以上用的函數都改成不須要再分配內存的版本,所有用指針來實現。相似於 golang 中的切片操做是基於同一塊內存的原理。    說到優化,忍不住有一些有趣的事情與你們分享。早年咱們剛開始學習編程和計算機時,一提到優化實際上大多數指的是對編譯出來的代碼的優化,那時候的優化大多會說什麼換哪一個彙編指令或者函數改內嵌會加快代碼執行等等這樣的。特別是看到那些折騰彙編的,一會兒感受這種工做距離本身好遙遠。這主要是因爲相關資料太少了,有也得全英文,對母語非英語的開發要去改動彙編優化代碼,可能性真的很小。工做多年後在工做中發現,其實不是這樣的,實際上一個算法或者處理方法的改動就有可能讓代碼執行速度有千百倍的躍升!真的,並且我仍是按保守說的。舉一個最簡單的例子,在國內(其實國外也是)不少開發並非計算機軟件或者相關專業出來的,有個很是常見的問題就是他們不知道什麼是二分查找(甚至沒據說過),這就讓他們在設計數據庫和容器數據結構時不明白索引和排序的重要性。在設計時就經常忽略掉,而給系統(特別是服務器類型的系統)加一個簡單的二分搜索就能指數級的提升性能。這些算法大可能是固定的,好比有網友分析 nginx 源碼時就說其中的紅黑樹算法(不太記得了,總之是一種二分樹)與經典教程中的如出一轍。這種類型的模塊是就象彙編同樣,不太可能去修改它的 – 你以爲你會寫出一個比快速排序更快的算法嗎?固然不是說這徹底不可能,而是說咱們的平常的開發中代碼優化的角度不該當放在這個地方。但也不是徹底就要按傳統的來,再舉 nginx 的例子,它的列表容器並非傳統的鏈表,而是分出的一大塊內存,在裏面存指針。這在 Delphi 中也是同樣的,當年我查看到 Delphi 的這部分代碼實現時驚訝得不得了,由於歷來沒有見過或是據說過是這樣實現列表的。這種列表在數量量不大(1萬如下)時,速度很是驚人,由於整塊操做這塊內存就是對整個列表進行操做了 – 多個操做只須要一個內存複製代碼。但多年後我負責從新維護一個 Delphi 版本的服務器時發現數目到 2 萬這個級別時性能會急劇降低,這時候想在裏面刪除一個元素會很是慢 – 由於這時候這塊內存已經太大了。Nginx 的解決辦法很簡單,它又回到了傳統算法上來 – 若是數目太多,它就再分配一塊內存,用鏈表鏈接起來,這樣它同時獲得了兩者的好處。不過最後我並無用 nginx 的作法,一來是複雜了點,更重要的是我當時只須要優化刪除的狀況。個人作法是將最後一個元素的位置與被刪除者交換就能夠了,由於總數已經減少了1,這個被移動到最後的元素是永遠不會被訪問到了的。我舉的這些例子是想告訴你們,優化沒有那麼難,大膽地去作。同時也要多學習更多專業的知識;同時也要明白本身不能作什麼;同時也要明白,雖然有不少如今我還不能作的,但在我能作的範圍內一樣是能讓性能成百上千萬倍的提高的。    讓咱們回到字符串優化的問題上來,爲何「專家」們操做字符串時都會說在同一塊內存上操做,不要用多個內存加來減去?大多數開發是知道這個優化方式的。不過原理是什麼呢,大多數人就不清楚了,並且更多的人不會知道,系統對內存分配上其實也是作有不少優化的,因此不少時候也不用太擔憂。學過操做系統,或者對操做系統運行有必定了解的應該會知道,分配內存就是操做系統的一項重要的基本操做。你們不知道的是,即使是發展到了這個時代,操做系統分配內存的速度其實真的不快。在開發語言中(至少 C/C++、delphi 確定是)都是先取一大塊內存,再在程序須要分配時提供的。至關於用本身的內存分配算法來代替了操做系統提供的內存分配函數。甚至有好幾個內存分配的 C/C++ 開源項目,目的就是爲了提升 malloc/new 操做的速度而已,可見提升分配內存速度的重要性。這固然也會形成不一樣系統下的速度可能會有很大差別,既然這麼困難,那我不分配內存不就是最快的了 – 沒錯!這就是字符串操做使用所謂不從新分配內存的 stringbuf 代替 string 的理論基礎。在 java 和新版本的 golang 中甚至有專門的這樣的「字符串緩衝類」。知道了這一點,咱們也能夠知道,並非全部的地方都須要替換,不會產生頻繁操做內存的地方也沒那個必要。並且現代的字符串實現中其實已經帶有緩衝了。    你們聽明白了嗎,其實我想說的是,通常咱們的開發環境中對內存分配已經作有優化,並且字符串也帶有必定的緩衝,因此咱們的代碼中直接用 string 其實問題也不是太大。    說到內存分配管理,忍不住再分享一個故事。仍是多年前,我供職於一家自稱是國內首屈一指的期貨軟件供應公司 – 它們的自稱有多是可信的,由於我以前在另一家自稱同行業號稱第一的公司裏據說過它們的軟件。有一天他們須要給程序加上先分配內存的功能,緣由是他們的客戶會運行不少的客戶端,這時候在多個客戶端切換時有可能會提示內存不足。若是剛好輪到咱們的客戶端時客戶就會投述說,大家的客戶端怎麼彈出對話框說內存不足了 … … 好了,爲了不這種狀況咱們老闆要求程序一進去就先把須要的內存都撈到手。這個看似無厘頭的功能實際上是可能實現的,研究了一番後我發現也不難,只要重寫內存分配器就能夠了。其實也不難,大概沒幾天吧就弄好了。可是有個大問題:速度太慢,說真的至少慢了 10 到 100 倍,特別是內存使用量大了之後。最後的解決辦法是仔仔細細研究了原版的內存分配器,其實就是按內存用量的大小統計大概在哪幾個區間,而後對用量比較大的區間分配好固定大小的好幾種內存塊就好了。而塊間的鏈接也是最簡單的雙向鏈表。其實折騰時間最多的就是內存區間的尺寸,好比第一檔應該是 100k 仍是 1m 這樣的。不能憑想象,得用統計結果進行配置才行。最後這個預分配內存的分配器速度和原版是同樣的(固然我是想讓它更快一些好虛榮一下的,不過確實原版的速度也已是很不錯了的)。有趣的是,機緣巧合後來我又回到了這家公司。發現他們看不懂這些代碼,已經放棄了。其實我編寫代碼時是很習慣把思路都所有寫清楚的 – 最主要的是我寫的代碼太多了,生怕本身之後也看不懂 – 我寫的註釋應該仍是有用的,至少他們很「輕鬆」的替換回了默認的內存分配機制。    因此優化與否,要看實際的狀況。也要結合自身的能力做出決定和選擇。    另外還有一點:測試用例真的很是重要。若是沒有以上的測試用例,我在改寫成其餘語言時就發現了不那些細微差別形成的錯誤了,這會產生嚴重的 bug !golang 的 mime 解碼模塊源碼中就帶有不少容易出錯的測試用例,這對於這樣複雜的功能的模塊修改是很是必要的,不然你作了一個自覺得很重大的改進結果卻產生 bug 時就會留下嚴重的後患。

相關文章
相關標籤/搜索