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 时就会留下严重的后患。

相关文章
相关标签/搜索