关于Jabber客户端
来源:http://mcs.szu.edu.cn/user/shendasky/Article_16190Jabber客户端
现在网络中最流行的程序,莫过于即时通讯软件了,从ICQ到QQ,全世界约有7000万人每天在使用它们。人们利用它来沟通、交流,它是继电子邮件之后另一个最成功的通讯工具。如此成功的软件模式引出了一系列出色产品的诞生:ICQ,Yahoo! Messenger, AOL Instant Messenger,MSN Instant Messenger及中国人用的最多的QQ,而其中有一个较之其他通讯程序更璀璨夺目的明珠,那就是Jabber工程。
Jabber是一个基于开放模式的软件工程,现在的主要目的运用于即时通讯(Instant Messaging System),Jabber并非第一个发明者,但它拥有几个非同一般的特点:
*基于XML
*分布式构架
*开放式协议与代码库
*方便的、可扩展的组件模式
这些特点使得jabber一出世,便深受瞩目,可以毫不避讳的说,几大流行通讯软件(如Yahoo!,AOL Messager,还有tencent 的 QQ)都是从jabber的代码库中发展而来的。而它基于XML的通讯协议,使得跨平台很容易就能实现,现在的jabber已经可以使PC,Palm(掌上电脑类)以及SMS(短信息)、WAP互相沟通无碍了。总之,jabber的发展激动人心,它极有可能成为未来的即时通讯标准。
以下介绍的是Jabber的工作状态,依据版本为最新的1.4。
Jabber Session
整个 Jabber的交流是基于一个会话(session)过程的,Jabber会话开始后,就会同指定服务器的端口5222(或者是5223,如果使用SSL 进行加密的话)进行TCP连接。WellJabber作为一个演示性程序,没有将SSL选项包含在内——需要注意的是,并非所有的服务器端都支持SSL。
Jabber发送的数据流是由一个连续不断的XML文档构成的,它的根元素为<stream>,而只有当C/S两端都注销时——即发出 </stream>关闭标记,这个XML文档才算彻底结束。在<stream./>里的子元素们都是做为命令出现的,各自包括自己的属性、内嵌元素,以此作为参数。
服务器/客户端两边的连接都是异步传输模式,这不同于诸如POP这样的协议,在你发出一个命令时,不需要等待另一端的回应,可以直接发出下一个(命令)。此外,服务器可以随时向你发出指示(比如说,当你的一个好友上线或离线时),这就意味着你得随时作好处理这些指示的准备。
那如何将一个回应同特定的命令对应起来呢?这是id属性需要做的事。你发出一个命令时,需要包含这个属性 ——以一个恰当的唯一值出现,而从服务器返回的回应,也包括同样的属性值。具体处理时,可以用一个散列表(Hash Table)作为id值,以此来标识那些需要回应的请求等数据。当然,设置合适的id值是服务器端的事,客户端所要做的是随时接收server发来的指示,在编写代码时,可以开辟一条单独的线程或利用一个select/event来响应接收的信息。这些在C/C++中都拥有良好的支持,但PHP则不行,因为它毕竟是脚本语言,不能进行系统函数的调用,所以WellJabber暂时只能做到请求/回应的模式,不能做到随时处理主动接收的信息,也就是当好友发一个信息给你时,你没有办法去判断、接收它。因为这个至少需要一个循环来处理接收,但在脚本中出现这个循环意味着你的程序在信息到来之前始终不能完成,这是相当可怕的。对 WellJabber来说,这的确是个不小的遗憾。
Parsing XML
实际处理时,最困难的部分可能就是解析XML文档了,但幸运的是XML不同于HTML,它有着严格的语法定义和格式,比如所有标记和属性都是大小写敏感的,所有的属性结束时都要求明确的关闭标记,属性值、标记外的文本内容都不得与XML保留字相同(如<、 >、&、’、” 这样一些,如果需要可以用& entity;的形式代替),还有就是非ASCII码字符集的文档要求在<?xml>中明确标识,通常中文可以处理为:
<?xml version="1.0" encoding="GB2312" ?>
或<?xml version="1.0" encoding="BIG5" ?>
自己写出一个XML解析程序是完全有可能的,但幸运的是,有很多标准的XML解析程序(库)可供我们使用,比如使用PHP编写的WellJabber 实际上就是利用expat作为解析模块。这里要注意的一点是,你的解析程序必须能处理任何得到的XML数据片段,因为前面已经说过,jabber中传递的 XML数据并非完整的,一个彻底结束的数据流(以<stream/>结尾)要到程序注销时才能出现。
补充:Expat是一个很有名的XML解析程序,很多出色的软件工程使用它做为XML文档的解析模块,譬如PHP及Perl等。
Writing XML
写XML数据相对来说比较简单了,但仍要注意必须写得符合语法要求,少一个引号都可能引起服务器拒绝处理、甚至是断开连接。
Opening the session
当你打开位于5222(或5333)端口上的socket后,需要发送一个标准的XML头,以此来打开一个以<stream>开始的完整jabber会话。
<?xml version=”1.0” encoding =”UTF-8”?>
<stream:stream
to=”jabber.org”
xmlns=”jabber:client”
xmlns:stream=http://etherx.jabber.org/streams>
这样就可以唤醒服务器了,这时服务器会回应大致如下信息:
<?xml version=”1.0” encoding=”UTF-8”?>
<stream:stream
from=”jabber.org”
id=”39ABA7D2”
xmlns=”jabber:client”
xmlns:stream=http://etherx.jabber.org/streams>
这时XML解析器就可以发挥作用了,它提取并保留当前的id值,因为本次Jabber Session就依靠它来标识了。
我使用TCP Echo Client测试了一下,的确是如[JPO]中所述的一样,只是要手工输入XML代码比较麻烦。
Logging in
这时服务器等待你的身份验证,你使用<iq>询问[JPO 1.5]来发送你的认证信息,而服务器据此返回一个回应,指示你的登录是否成功。[JPO 1.5.3.3]
具体来说,询问使用jabber:iq:auth空间名称和<username>以及<resource>元素来标识用户的jabber ID和计算机的resource名[JPO 1.6.3] 。
在认证过程中,使用明文传递密码是不提倡的,比较安全的做法是配合使用一个包含已编码digest的<digest>元素。这个 digest是根据session ID和密码组合成的字符串,结合SHA-1算法生成一个20个字节的散列,然后再将其转变为40个字节的16进制形式。当发出这个登录询问后,就开始等待回应了。接下来你会收到<iq type=”reply”>的数据,至此,你已经成功登录了。假如回应中type="error",则表示登录失败,这时得立即关闭session了。
Logging Out
当要注销退出时,只需简单的发出”</stream:stream>”数据即可,这样就通知服务器,本次XML数据流结束,然后关闭 socket。有时候,socket会出乎意料的关闭,比如说当Jabber服务器进程中断时。比较严重的问题是,一般TCP Socket不能分辨清楚一个空闲连接和一个断开的连接(可能发生在计算机崩溃或网络断开时),这时不会收到任何数据,这对一个向jabber一样的实时通讯协议来说,是个棘手的问题:你可以想象一下,此时你仍然在线,而你的好友列表还是有效,但除非你试图发送一个讯息,否则你不会知道你已经断开连接了。更为严重的是,一些防火墙和路由器在发现你长时间没有动作后,会做自动断开连接。
有两种方法来解决这个问题,一种比较简单,就是每隔一分钟左右的时间就发送一个bit的XML数据,假如你的计算机没有得到服务器的回应,操作系统就将检查连接是否断开,并指出一个错误。还有一种复杂的方法就是设置你操作系统的网络API中套接字的”keep-alive”选项,只需将超时间隔调整到几分钟即可,这个方法是否可行,依赖于你的操作系统(在win32平台上,可以用 setsockopt函数配合SO_KEEPALIVE或SO_KEEPALIVE_VALS来修改这个选项)。
- 用户在线状态
即时通讯客户端程序中要处理的一个重要模块就是,通知服务器用户的在线状态。当你登录后或改变在线状态时,都需要通知服务器。(比如QQ中经常使用的“我在吃饭,请过一会儿再和我联系”,“我正在工作中”等等,这些都属于用户的在线状态)
要报告一个状态信息的改变,只需要发送一个<presence>元素[JPO 1.4]。它的类型属性不是available就是 unavailable。你不需要添加"from"或"to"属性,服务器在将你的状态信息发给你的好友时,会自动添加它们。[JPO 1.4.1.1, 1.4.1.5]unavailable状态可以很方便的使用户处于“隐身”:在你的好友看起来,你就象根本没有在线。
接受你好友的在线信息正好相反:你会收到他们发送的<presence>数据。当你登录后,你会收到每位好友这样的数据,以更新你好友列表的内容。这个元素可能包括一个使用jabber:x:delay命名空间的<x>标记,来通知你好友状态信息最后改变的时间。[JPO 1.6.18, JPG p.89]只要你在线,这些状态信息随时都会发送给你。
- 管理好友列表
好友资料(Roster)的管理是一个比较头疼的事情,至少从现在协议的描述来看。
How the roster works
好友资料的处理工作包括:用户的状态,好友的状态以及那些想加好友但尚未验证通过的请求。Jabber服务器存储用户的好友资料,并负责在如下情况下通知已登录的用户其好友资料的改变:用户添加或删除一个好友,其他用户在好友列表中添加或删除你,用户通过或拒绝加入好友的验证。这些都笼统的称作好友资料更新。这些更新通知都是作为<iq>元素(使用jabber:iq:roster命名空间)数据来发送的。当然,客户端也可以主动请求好友资料的更新:这个在登录后通常都应该进行一次,以更新本地客户端的好友资料。
Subscribing & unsubscribing buddies
添加或删除好友是通过<presence>元素来进行的,它的type属性是subscribe或unsubscribe。接收或拒绝都是通过<presence>元素来进行的(请求和应答都有同样的ID号)总之,当你的好友资料改变时,服务器就会主动通知你情况的改变。前面已经说过,PHP编写的WellJabber有很多限制,其中一条就是除非你主动要求更新好友资料,否则很难及时反映好友在线情况。
Manually updating the roster
如果你想更新服务器端的好友资料,可以发送<iq type=”set”>元素,你这样做并不是添加或删除好友,而是更新与好友相关的资料,比如他们的昵称或所属组名。[JPO 1.6.12]
More roster info
完整的好友信息可以在通过一个<iq>询问接受vCard资料时获得,前提是如果他们存储了这样的信息[JPO 1.6.26]。(关于vCard,实在又是一个很大的论题,所以作为演示例子的WellJabber没有包含它)
即时通讯客户端程序中要处理的一个重要模块就是,通知服务器用户的在线状态。当你登录后或改变在线状态时,都需要通知服务器。(比如QQ中经常使用的“我在吃饭,请过一会儿再和我联系”,“我正在工作中”等等,这些都属于用户的在线状态)
要报告一个状态信息的改变,只需要发送一个<presence>元素[JPO 1.4]。它的类型属性不是available就是 unavailable。你不需要添加"from"或"to"属性,服务器在将你的状态信息发给你的好友时,会自动添加它们。[JPO 1.4.1.1, 1.4.1.5]unavailable状态可以很方便的使用户处于“隐身”:在你的好友看起来,你就象根本没有在线。
接受你好友的在线信息正好相反:你会收到他们发送的<presence>数据。当你登录后,你会收到每位好友这样的数据,以更新你好友列表的内容。这个元素可能包括一个使用jabber:x:delay命名空间的<x>标记,来通知你好友状态信息最后改变的时间。[JPO 1.6.18, JPG p.89]只要你在线,这些状态信息随时都会发送给你。
好友资料(Roster)的管理是一个比较头疼的事情,至少从现在协议的描述来看。
How the roster works
好友资料的处理工作包括:用户的状态,好友的状态以及那些想加好友但尚未验证通过的请求。Jabber服务器存储用户的好友资料,并负责在如下情况下通知已登录的用户其好友资料的改变:用户添加或删除一个好友,其他用户在好友列表中添加或删除你,用户通过或拒绝加入好友的验证。这些都笼统的称作好友资料更新。这些更新通知都是作为<iq>元素(使用jabber:iq:roster命名空间)数据来发送的。当然,客户端也可以主动请求好友资料的更新:这个在登录后通常都应该进行一次,以更新本地客户端的好友资料。
Subscribing & unsubscribing buddies
添加或删除好友是通过<presence>元素来进行的,它的type属性是subscribe或unsubscribe。接收或拒绝都是通过<presence>元素来进行的(请求和应答都有同样的ID号)总之,当你的好友资料改变时,服务器就会主动通知你情况的改变。前面已经说过,PHP编写的WellJabber有很多限制,其中一条就是除非你主动要求更新好友资料,否则很难及时反映好友在线情况。
Manually updating the roster
如果你想更新服务器端的好友资料,可以发送<iq type=”set”>元素,你这样做并不是添加或删除好友,而是更新与好友相关的资料,比如他们的昵称或所属组名。[JPO 1.6.12]
More roster info
完整的好友信息可以在通过一个<iq>询问接受vCard资料时获得,前提是如果他们存储了这样的信息[JPO 1.6.26]。(关于vCard,实在又是一个很大的论题,所以作为演示例子的WellJabber没有包含它)
发送信息时,使用一个<message>元素[JPO 1.3],它使用”to”属性来标识接收者;反之,你接受包含”from”属性的<message>元素,它标识了发送者。
实际上,任何人都可以发送信息给别人,你不需要特定的权限就可以查看到别人的在线状态。这会造成信息的骚扰与泛滥吗?要解决这个情况,就要使程序有对信息进行筛选的能力,只允许从好友处来的信息,其他一律过滤掉。
Message attributes
我们收到的任何信息都包括一个<form>属性,它给出了信息的发送者。同电子邮件相比,它的认证更为可靠,因为这个属性是由jabber服务器端来添加的,这就减少了发送者进行欺诈行为的可能性。
一个信息还应该包括一个<subject>元素,它标识了本次信息的主题,但显示与否取决于接收者所使用的客户端程序。
一个信息还可以包括一个时间戳,这是用一个<x>元素来实现的,它使用了jabber:x:delay命名空间。
而使用jabber:x:envelope命名空间还可以提供群发的功能,这就象传统的电子邮件一样。[JPO 1.6.20]
The message body
一个信息总是用<body>元素来包含其具体内容的。[JPO 1.3.3.1]
当然也可以包含可选的元素<html>,它将提供HTML格式的信息。[JPO 1.3.3.3]但是需要注意的是,这个格式是基于XHTML的(w3.org制定的一种由HTML向XML过渡的格式)。
对于HTML的使用者来说,会发现XHTML与其有很大的不同,因为设计XHTML时就考虑了客户端类型的限制(譬如说手机),具体体现为缺少一些常用的HTML元素,如<b>,<i>及<font>,但它们在XHTML中都有等价替代元素,如< strong>代替了旧的<b>,但一般指定色彩或格式时,都使用CSS(Cascading Style Sheet)。
Jabber支持加密的信息传送,它使用包含jabber:x:encrypted命名空间的<x>元素来处理。[JPO 1.6.19]文档中对这段描述并不是很清楚,因此WellJabber并没有对加密提供支持。
Other types of content
与MIME不同,jabber信息并没有一个标准的格式来容纳图片或声音,这就意味着你无法在信息中包含一幅图片的数据,除非是使用超链接的形式来指示它。
你可以随信息一起发送文件,但是文件的数据不能包含在<message>中,而是采用超链接的方式指明可以下载的文件。
Message types and threads
发送的信息可以使用”type”属性来提示其显示方式,如果没有指明这个属性,信息将独立地显示在单独的窗口中。若”type=chat”则指明应使用 one-to-one(类似QQ的两人世界)聊天界面来显示。此外还有”type=groupchat”,详细参见[JPO 1.3.1.1—— 1.3.1.4]。
最后有可能出现”type=error”这样的属性值,它表明在发送一个信息时出错了(比较常见的是,发送信息给一个不存在的jabber地址)。这时的回应包含在一个<error>元素中。[JPO 1.3.1.3]
为帮助客户端显示信息在相应界面中,信息还可以包含一个<thread>元素,它包含一个指向信息流的唯一值,客户端发送的第一个信息就应该包括一个唯一的线程ID,而后继的信息都应该发送到此线程ID标识的同一个线程中。(JPO建议thread ID由发送者的jabber ID及当前时间以散列算法合成)
Message event
信息的发送者可以使用jabber:x:events命名空间来接受这样的通告,即信息的接收者是否已经查阅过本信息,或者他/她是否在进行回复。这是个全新的功能,在演示程序WellJabber中没有体现。
Message expiration
信息的发送者可以使用jabber:x:expire命名空间来确定信息的发送时效。[JPO 1.6.22]如果信息是离线存储的,当时效过去时,即使对方用户登录,该信息也不会发给他/她。
聊 天
Jabber的群组聊天或会议机制允许多人同时进行交流。
这种多人交流的方式在客户端实现时是比较复杂的,这是大家所公认的,因为有两套聊天协议在使用。群组聊天是最早采用的,而会议机制是新的,也更灵活(注意,现在只有jabber 1.4服务器版本才支持它——做为一个外接模块)。实际上,协议本身仍在不段变化,还没有最终形成标准。
Creating a chat room
在产生一个聊天室前,你需要有一个聊天室名和一个会议服务。服务可以由用户来制定,或者通过发送jabber:iq:browse请求来检索。聊天室名称可以自己输入,或者编程产生(比如,产生一个随机的数字作为名称)。
为确认聊天室名称是否已被使用,可以发送<iq type=”get”>(含xmlns=”jabber:iq:browse”的命名空间)到聊天室,如果它不存在,你会收到error 404(没有找到)错误,反之,如果其已存在,你就得重新为聊天室取个名称。QQ中体现在自建聊天室这个版块。
对于如何生成一个聊天室,有着不同的异议。编程者的实践经验是先发送presence到聊天室,如果已存在就加入它,没有则发送set请求来建立它(发送包含xmlns=”jabber:iq:browse”的<iq type=”set”>命令)
Joining a chat room
需要加入一个聊天室时(它的ID已经由用户指定或在接到聊天邀请时确定),首先发送一个<presence>元素。注意不要添加 resource名在发送中,这是老的groupchat的做法,现在的conference已经不采用了。如果你需要向下兼容性,可以发送 resource name。
接下来,发送包含xmlns=”jabber:iq:browse”的<iq type=”set”>,这个请求包含了一个或多个<nick>元素,它指明了你希望加入的会议的别名。一旦你接到一个成功回应,也就意味着你已经加入这个聊天室。
The chat’s roster
每个聊天室都有个人员列表,表明当前在聊天室中的人员。它会随着人员加入或离开而改变。
通知客户端聊天室人员的方法有很多种。首先,发给每个成员<presence>元素,在你加入这个聊天室或有其他成员改变在线状态时(更新状态、信息或是离开)。
此外,当成员列表改变时,自己会收到一个含有jabber:iq:conference命名空间的<iq>元素,它具体包括代理服务器上的 jabber ID以及当前成员的昵称。描述conference本身的是包含多个属性的<conference>元素,如果包含< user>子元素,则标识了当前的成员们,这时通常带有”jid” 属性和”name”属性。
又如,当一个成员加入、离开或是改变其昵称时,你就会收到一个类似的请求,它包含一个单独的<user>元素。
最后,服务器会发送一个类似“某某加入了”或“某某离开了”样式的消息。
随便说一下,如果你希望在聊天室查找某人,可以使用包含jabber:iq:borowse命名空间的<iq>元素来发送他/她的proxy JID。
Chat invitations
聊天邀请使用包含<xmlns=”jabber:x:conference”>的请求来实现。
Sending and receiving messages
要发送一个信息到聊天室,可以发送”type”为”groupchat”的<message>元素到聊天室地址。发送一个私人信息,可以到他们的proxy ID。
收到的聊天信息依靠”groupchat”类型来辨识,可以从”from”地址去处掉resource ID,此时剩下来的就是聊天室ID了。
Jabber还支持IRC的“表情”聊天方式,这使聊天者能做出类似舞台动作的行为。(这个在QQ中是很常见,也很有趣的)客户端通过前缀”/me”来辨识这样的信息,通常在显示前应该插入使用对象的昵称。
比如,可以发送这样的信息”/me 笑眯眯的望着大家”,则客户端就显示为:
Huwell 笑眯眯的望着大家
Leaving the conference
要离开聊天室时,你只需要简单的发送一个<presence type=”unavailable”>元素即可。
File Transfer
Jabber不直接支持文件的传送,而是依靠“带外数据”(out-of-band)即OOB机制通过URL来超链接文件。这种解决方案使得发送者的客户端要么上传文件到一个特定的FTP/HTTP/WebDAV服务器,要么打开另一个端口,运行一个常规服务在上面。这两种方法下URL都会发送给接受者,他们便使用这个超链接来下载文件。注意,后一种机制在发送者隐藏在防火墙或NAT Server后时会失效,接收者不受影响。这种P2P的文件传送功能非常有用。
OOB不是仅为文件转送来设计的,它可用来传送任何URL,比如一个到心爱站点的链接,尽管一个HTML消息也可以支持它。
有两种相似的方法来传送URL,一个就是将<x>嵌套在<message>元素中,并使用jabber:x:oob命名空间 [JPG p.92,JPO 1.6.23];第二种方法就是使用jabber:x:oob命名空间的<iq>元素请求[JPG p.53, JPO 1.6.9]后一种方法允许通过iq回应来确认。
[*}注册新用户
Jabber协议允许客户端登记一个新的用户,而不用通过web界面来或系统管理员来申请(当然,任何服务器都允许这样做)。登记新的帐户有多种方法。
登记时,首先连接到服务器并打开一个<stream>元素,这就好象是正常登录。只是发送的是使用jabber:iq:register命名空间的<iq type=”get”>元素。[JPG p57-62]
如果服务器不允许登记新用户的话,会回应一个错误。
下列资料可以做为编程者设计登记新用户界面的参考:
<key>,这是一个需要随着登记命令发送回服务器的认证字符串。
<instructions>,包含一个呈现给用户的介绍。
<username><nick><password><name><first><last><email>
<addtress><city><state><zip><phone><url> <date><misc><text>,这些都是用户的资料。属于jabber Server1.4所需要的,如果开发出自己服务器版本,就可以自己定义这些用户资料选项了。
当用户一切就绪后,就可以发送包含上述资料的<iq type=”set”>回应了,然后等待服务器的响应。
如果注册新用户成功,你会收到一个包含空的请求的回应。你需要关闭连接,这个连接不能再做为登录的连接重复使用了,你得另外打开一条新的。
如果注册失败了,你将得到错误的回应,如果错误代码是409(冲突),这意味着你注册的用户名是无效的。
Updating registration
如果要更新注册信息(如密码或电子邮件)可以通过发送<iq type=”set”>元素来完成。
Canceling an account
如果你需要终止一个帐号,你先得通过发送一个<iq type=”get”>元素来获得服务器的<key>,然后再发送包含<remove>子元素的<iq type=”set”>来达成。
[*}WellJabber功能模块
开发一个jabber的客户端不是一件简单的事,事实上标准的客户端应该包括:P2P聊天,聊天室,好友列表管理,资源管理,相关资料修改,信息加密处理,MSN网关(或者还包括Yahoo!,AOL等的信息转换),在线搜索,认证控制,离线信息转发,文件互传等。
一个比较成功的jabber客户端是由jabber.com提供的JIM,此外还有很多优秀的jabber客户端,由于jabber的通讯协议是建立在 XML基础上,而且是开放式的,所以任何语言都可以用来编写客户端,最常见的是Delphi,Java,C/C++,Perl,此外还有PHP, Python,JavaScript,甚至是Flash ActionScript都可以拿来编写这样的客户端,我觉得一个开放式的应用模式才是成功的,才会有长足的进步,这就象电子邮件,我们很难想象如果电子邮件的格式被一家所垄断,只能使用一家所编写的邮件程序去接收,那它还会这么普及、全球通用吗?在这方面国内的腾讯公司做的就很不好,显然易见,QQ的代码也是从jabber中剥离出来的,但是QQ却不肯公开它的通讯协议,一心只想做国内即时通信的老大,这会造成两个后果:一个是处在竞争压力小的情况下,它会停止不前,没有进步。
我们看到,现在的QQ客户端同以前的没有什么太大的区别,不过界面更花哨些而已,没有利益的驱动,腾讯现在连Linux,Palm等平台都没有推出,实际上很可悲。另一个是它只能在国内发展,走不出国门,一旦即时通讯的国际标准制定,那它再这么固步自封,就会成为不合格的产品,下场也好不到哪去。所以希望腾讯公司能赶快觉醒,不要垄断这个现在看起来很壮大的行业。QQ虽然很火,但比起Flash,Netscape又如何?这些世界闻名的软件都是公开格式,甚至是代码的,腾讯还不该向这些软件学习吗?当年的Netsacape几乎是独步天下,可还是被IE后来居上了,现在微软又在XP中捆绑了MSN,腾讯再不当心,可真要尴尬了。
WellJabber是使用PHP写的,是典型的B/S结构程序,之所以这样考虑,是想使WellJabber具有跨平台的特性,能够在win32, Linux,Unix,Mac等系统上都顺畅的运行,因为它是利用浏览器做为承载平台的。而且PHP发展到今天(最新的版本是Version 4)已经很强大了,它具有多种函数库,如同C语言一样,甚至融合的比C更体贴,就Jabber来看,PHP拥有必不可少的XML解析函数,还有网络连接函数,以及加密函数(Hash散列,Base_64等),总之使用PHP来写jabber的演示程序的确很方便,但PHP也不是十全十美的,毕竟,通过HTTP端口,很多有用的功能都实现不了,而且调用的不是系统级的函数(如connect),效率有所下降。
本程序只预计包含五个功能模块:用户注册,用户登录,获取好友列表,发送信息,用户注销。
考虑到面向对象的特性,WellJabber程序采用了类的定义,以方便脚本调用,支持类是PHP中很有用的特点,特别是数据库操作,如果能把一般SQL行为用类来封装,那么在修改数据库类型时将会很方便,做到以做少的改动支持最大的兼容性。
WellJabber中的类定义放在jabber.inc中,一共有6个行为,分别是:
jabber->connect (server[, port])
jabber->Login (username, password, resource,server[, port])
jabber->messages (recipient, subject, body, type)
jabber->register(username, password, email, resource, server[, port])
jabber->GetRoster()
jabber->_display_error_message()
在定义好这几个类以后,脚本就可以很方便的实现jabber的通讯功能,而不必重复代码。配合模版的设计,使得PHP版的Jabber更为方便灵活。
根据jabber文档中的描述,WellJabber类中定义了以下的成员变量,其值与特定含义分别是:
name——用户的真实姓名;
email——用户的电子邮件;
password——用户选用的密码;
username——用户登录名称;
resource——用户的Location辨识名;
sid——本次session的唯一标识
server——登录服务器名称;
port——登录服务器端口名称;
error_code——发送错误的代码;
error——发送的错误(描述)
connect——本次连接的文件指针
roster——好友资料数组
首先,看一看内部的出错处理函数:_display_error_message():
这里error的错误描述实际上就是[JPO]的附录所指明的错误描述代码,这是标准的,由服务器发回的。
Jabber类中要处理的错误列表为:
Bad Request
这个表明jabber客户端发送的数据不能为server端理解,通
常是由于数据流不符合jabber协议而引起的。(譬如,jabber客户端发送了一个subscriptioj给自己,或者是发送了一个不含to属性的数据流。
Unauthorized
这个表明客户端的身份请求验证失败,当客户端发出错误的密
码或者是不存在的用户名时会发生这种情况。
Service Unavailable
这个错误主要发生在服务器无法处理客户端的请求时,譬如,
当我们要发送一个消息给离线好友,但接收者的服务器不支持离线信息存储的机制,就会返回这个错误。
Remote Server Timeout
当试图连接一个服务器而超时时就会发生这个错误。比如说一
个不正确的服务器名称被指定时。
Payment Required
这个错误是为未来使用制定的,现在不会发生。
Forbidden
这个错误发生时表明,服务器理解客户端的请求,但拒绝处理
它。现在主要发生在当注册时密码存储错误时。
Not Found
这个主要是在服务器无法找到与这个客户端送来的数据包匹
配的JabberID时发生的。
Not Allowed
本错误主要是当服务器根据该数据包中的JabberID判定本次
Jabber行为无效时产生的,譬如当非管理员用户向服务器发送一个管理员数据命令时。
Registration Required
这个错误现在尚未开始使用。
Internal Server Error
当服务器发生了未知错误时,就会返回这个error,要防止这
种情况发生主要是从客户端入手,要保证发送的数据包的正确性。
Invalid Parameter
无效的参数错误。
在发生上述错误时,_display_error_message()都会做出正确的处理,实在有未知的错误发生时,也会提示与管理员联系。
下面一一分析成员函数,先看用户登录时所用的:
function connect ($server, $port = "5222")
注意这里默认的端口为5222。如果你使用了SSL登录也可以改为5223。函数里首先是验证传递过来的参数的合法性。也就是server,username,password及resource不能为空,否则就报告错误。
接着是与server端的5222端口相连接,这里使用了fsockopen函数,这个函数功能很强大,它与服务器做了一个TCP连接,并且它返回了一个文件指针,可用于其他的文件函数(如fgets、fgetss、fputs、fclose或feof等)。可以说没有它,jabber的功能就实现不了,因为jabber主要是依靠与server的连接,交互数据流来实现的,用其他语言如C/C++可以很方便的调用connect函数(以及之后的 send、receive函数),同样PHP有fsockopen()也很不错。
登录时使用Login ($username, $ password, $resource, $server, $port = "5222"),当jabber->connect()打开连接后,开始向server端发送数据,譬如登录时发送的XML数据包,随后要读入返回的流,这时会产生一个错误,因为使用fread或fgets等PHP文件操作函数,都要求读入一定的量字符数(按照参数),或者是读到行尾或文件尾,但是由服务器返回的数据是一个完整XML流的一部分,没有所谓的行分隔,我们预先也无法知道此次返回多少字节,如果写成fgets($fp, 1024)这样的,就会使该脚本陷入延时,因为fgets行为就是想读入1024个字节或者是到行尾/文件尾,等如果本次数据量小于1024字节,就会陷入这个函数,不能正确返回值。
在查阅了php.net的最新函数后发现,我们可以依靠socket_get_status()函数的unread_bytes特性来间接处理,说实话,这个方法有点勉强,但由于PHP语言的限制,实在没有其他方法来很好的处理它,如果是C/C++,那就很方便了,recv()函数自己知道收回多少数据,再不然配合Peek参数也可以预知本次数据量。
而使用socket_get_status()方法,就要分两步做,首先使用fgets()类型的函数读取一次数据(可以读一个字节),然后再用 unread_bytes得知本次未读数据,依据这个准确的字节数,再调用fgets()一次就可以全部读取了。由于要分两步做,所以效率不是很高的。然后拼接两次得到的字符串,就有了本次回应的数据流了。
收到XML数据后就要来解析它,PHP有很强大的XML解析函数,因为它是依靠expat做后台模块的。首先要创建一个XML解析器,就好象与MySql数据库做连接一样,都是准备工作:
xml_parser_create();//使用缺省编码ISO=8859-1
在下面的函数中都要用到这个解析器,然后调用xml_set_element_handler()来设置起始及结束元素的处理,第一个参数就是上面说到的解析器,第二个和第三个是XML特有规定的函数处理格式的名称,主要是:
StartElementHandler(int parser, string name, string attr)
第一个参数也是解析器,第二个用于保存XML元素名称,缺省情况下,它们会以大写形式出现。第三个是数组,用以保存当前元素的属性及对应值。有了它,可以利用PHP特有的each逐个读出来。
我们在StartElementHandler中将本次要用到的元素属性赋值,以便下面的调用判断,如登录中就是要对$jabber_type值是否为result进行判断,如果是表明登录成功,如果不是那就是登录失败了。
接下来是GetRoster()行为,使用它可以获得当前用户的好友列表,我们发送:
<iq type="get"><query xmlns="jabber:iq:roster"/></iq>
给服务器,即索要当前会话用户的好友列表,然后服务器会返回一系列数据流,里面包括了好友的名称,JID(jabber唯一标识,就好象是QQ中的数字号码)以及认证状态,如果还没有通过好友的认证,那subscription属性就会为none,WellJabber中采用了$jabber-> roster成员变量来接收这一系列的值。需要注意的是每次成员函数调用时都使用同一个连接,i.e.$jabber->connect,所以单个行为不要调用fclose来关闭它,可以在类的析构函数中调用。
SenMessage()发送消息给好友,这里比较简单,当获取好友的JID时,发送相应的数据流即可,这里要注意的是,发送人不需要自己填写,在经过服务器处理后,会由服务器来添加“from”属性,这个是为了防止发送垃圾信息,前面已经说过了。
最后是登记新的用户帐号,这里分四步:
首先,要向服务器发送一个连接请求,就如同登录时所发送的一样;
接着,客户端会收到回应的数据流,这里包含了重要的id,是标识本次会话的唯一值;这时,我们要发送本次想注册的用户名,resource名及密码,注意这里的<iq>请求要包含上面得到的id,而且密码应该采用加密的形式,但WellJabber只是一个演示程序,所以采用了明文发送的形式;
最后,服务器返回<iq di=’sesseion id’type=’result’>
代表本次登记注册成功。这样就完成了一个新用户的注册。
然后就可以使用该帐号进行登录了。注意,这里要重新与服务器打开一个连接,原先的连接已经不能用来登录了。
PHP版的WellJabber所拥有的功能已经描述完了。当然,从它来看Jabber工程只能是管中窥豹,Jabber中许多有用的思想和特点它都没有体现,譬如说实时接收、文件交换、邮件转发、聊天室系统甚至是跨平台交流(如mobile)。但由于Jabber开放和易用的特性,我们看到,任何人都可以用自己喜欢的语言去处理jabber、去理解jabber,这么博大包容的特性也许就是它最吸引人的地方,Jabber的前途将无可限量。