xmpp 协议的这个名称就来自于它的消息包格式借鉴了 xml 的格式,但并不是我们常规编程中的那种完整的 xml 信息包。因此就不能用普通的 xml 解码库来操作这些消息,一般的情况下是需要另外开发的,而实际的开发中通常使用第三方库。但就目前的第三库现状来说,可以说是不怎么好用,所以我们来看看如何手工自己进行消息的解码。
首先第一句话就是一个坑,以我们前面提到的从服务器中收到的第一句为例:
<?xml version='1.0'?> <stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' from='xumatomacbook-pro.local' id='675c6847-c13d-4710-9844-d9339e4df087' version='1.0' xml:lang='en'>
乍一看上去,你会以为第一句话是 “<?xml version='1.0'?>”,好我们开始解码这句话吧。答案是 NO !这句话是可有可无的,所以我们解码的第一个工作就是判断起始句是 xml 部分还是后面的 stream 部分。
实际上我们的做法是根本不解码!读者一定瞠目结舌不敢相信,对的,实际上我们对 xmpp 协议进行操作根本不需要 xml 解码器,只使用几个字符串查找和截取函数就可以了!
能够这样做的根本原因就在于 xmpp 协议并不是完整的 xml 包,实际上它和我们前面文章中的 SMTP/POP3 协议一样是服务器与客户端的一问一答式的问答包(当然也还不完全是,我们后面会说到),只是借助了 xml 的封装形式而已。
在 xmpp 协议中,服务器发出第一句话之后就马上要发送登录方法说明包,即
<stream:features><mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism>PLAIN</mechanism></mechanisms></stream:features>
类似于这样的格式。实际上我们在操作中直接将它们当做一个包处理就行了,就是说可以直接忽略掉第一个包,不管它有没有“<?xml version='1.0'?>”。在编程上就是一直读取服务器 socket 中的内容,我们将当前读取到而没有处理的内容当做一个字符串(而不是 xml 流),给它一个变量名为gRecvBuf。
然后每当 socket 接口上有数据来到事件时就去检测gRecvBuf 中是否已经包含了字符串“mechanism”,如果有,就说明已经收完了第一个包,服务器告诉我们可以发送登录信息了。伪码如下:
if FindStr('mechanism', gRecvBuf) { [做登录的各种动作] }
但这样其实也不对!因为,含有“mechanism”并不能说明整个登录包结束了,还记得我们前面学习 SMTP/POP3 是如何判断一个包什么时候结束了吗?一般情况下是判断是否有“\r\n”,xmpp 协议其实也是判断是否有某个结束符字符串,只不过这个结束符号不是固定的,而是针对不同的命令而不同而已!这就是 xmpp 协议操作中最关键的部分。实际上不光 xmpp 包括 SMTP/POP3 以及我们还计划要解说的 http 协议的操作中最关键的就是要找到一个包结束的位置和方法,解决了这个问题就可以说成功了一大半。
而对于我们现在的这个包,它的结束符号是 “</mechanism>”,这里就要涉及到 xml 格式的知识了,如果一个 xml 节点中还有子节点的话,它必须包括一个 </[节点名]> 这样的尾部。所以我们直接利用这一点,在gRecvBuf 中查找到这个字符串就是它的包尾了。
了解 xml 格式的读者可能就会提问说,如果mechanism 节点里还有mechanism 子节点,这个方法就不行了。没错,但是刚好 xmpp 协议中是没有这种情况的,所以我们可以放心地使用这种方式。其实就算有这种情况,我们再做进一步处理即可,只是根据 xmpp 协议的特点,没有必要那样做。所以实际上正确的操作伪码应该是:
if FindStr('</mechanism>', gRecvBuf) { [做登录的各种动作] }
这其中要注意的是,查找字符串函数应该是忽略大小写的。因为节点名是有可能含有大小写混用的情况的,出于兼容性的考虑,能忽略大小写是最好的。
这是非常重要的一个章节,建议大家仔细反复研究一下,虽然内容不多,但这是我们整个 xmpp 协议消息包处理的中心思想,非常的关键,也非常的简洁有效。
--------------------------------------------------
版权声明:
本系列文章已授权百家号 "clq的程序员学前班" . 文章编排上略有差异.