來源:http://mcs.szu.edu.cn/user/shendasky/Article_16190
Jabber客戶端
現在網絡中最流行的程序,莫過于即時通訊軟件了,從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前介
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數據相對來說比較簡單了,但仍要注意必須寫得符合語法要求,少一個引號都可能引起服務器拒絕處理、甚至是斷開連接。
登錄/注銷Jabber會話
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的前途將無可限量。