近日工作閑暇之余,對IM系統產生了興趣,轉而研究了IM的內容。找了半天,知道比較流行的是Openfire的系統,Openfire有許多平台實現,由於我是做Web的,所以當然是希望尋找Web的實現。Openfire的以前的Web實現,是基於Http-bind的一種長輪詢機制,當然也沒什么不好,只是我現在HTML5都開始了,當然希望能夠來個基於Websocket的機制了。然而Google&百度了好久,也沒有找到什么教程,發現這個東西並不是很火熱的樣子,那只好自己開始研究了。於是有了這一篇文章。
對了,我知道xmpp的JS框架有幾個,但那些框架似乎都是按照原本的長輪詢的機制來做的,並不是使用Websocket來做的。
另外,做軟件2年了,當我剛涉足這個行業時,我被告知:“不要重復造輪子!”,曾經我對此深信不疑,但這兩年的工作讓我越發認識,不要重復造輪子,僅僅是在於做項目當中,做一個商業產品的時候,考慮到開發速度和產品后期的穩定性及維護性,確實需要采用成熟的技術,但!這不代表作為一個程序員,不應該抱着一種從0開始的研究精神,如果真的熱愛這個行業,這個領域,就應該嘗試着,根據RCF文檔協議,從基礎協議的層次開始做軟件。
好了,廢話不多說。恩,開始正文吧。
So 得先搞一個服務端是吧?
肯定是去 Openfire 的官網下載最新的Openfire服務端程序啦,Openfire的是開源的,可以免費下載。如圖:
- Openfire 的主程序,主程序是服務端程序,這個主程序不是一個框架,不是一個半成品,而是一個很完整的項目,怎么個很完整?就是你下載下來,直接雙擊運行(Windows),或到其程序的根目錄下bin文件夾,然后執行 openfire start 命令(Linux),Mac就更簡單了,直接在“設置”當中會有一個專門的管理窗口。然后它有一個Web版的管理后台,所有設置都可以在這個后台完成,包括安裝插件之類的。
- 這個火花(Spark)是Openfire搭配的客戶端,也是多平台支持的。
- 這個地方可以下載Openfire的源代碼,你可以把他放到你的IDE中,然后簡單配置一下,就可以run起來,這個教程網上很多,我就不多廢話了。
安裝WebSocket插件
這就不說了。
建立Websocket連接
安裝了WebSocket插件之后,有兩個原本用於http-bind的端口,就被WebSocket占用了(有了WebSocket,還需要Http-bind做啥= =)。這兩個接口分別是7443和7070,前者是用於HTTPS安全連接,后者是非安全連接。
建立鏈接:
1 var connectionState = ["正在連接..", "連接已建立", "正在關閉..", "已經關閉"]; 2 var host = "ws://127.0.0.1:7070/ws/"; 3 if (window.WebSocket != 'undefined') { 4 //OpenFire是實現了WebSocket的子協議 5 var connection = new WebSocket(host, "xmpp"); 6 console.log(connectionState[connection.readyState]); 7 //注冊連接建立時的方法 8 connection.onopen = wsOpen; 9 //注冊連接關閉時的方法 10 connection.onclose = wsClose; 11 //注冊收到消息時的方法 12 connection.onmessage = wsMsg; 13 }
如果要使用Https加密信道,就把Host改成:
var host="wss://127.0.0.1:7443/ws/
恩,建立安全連接還需要添加安全證書到Keystore,這個在Openfire的根目錄下,有一個resource/security文件夾,里面有keyStore文件,當然,這部分我還不是很懂,關於Https的加密信道,TSL/SSl證書的概念,還沒完全弄明白,不過這也不是這篇文章的重點。暫時我就先用非加密的方式來做,至於加密的連接,除了host的區別,其他也沒有區別。
當Websocket握手之后,我們要做的第一件事,就是發起一個建立流的請求,
1 function wsOpen(event) { 2 //打印鏈接狀態 3 console.log(connectionState[connection.readyState]); 4 //發送建立流請求 5 var steam = "<open to='127.0.0.1' from='wuxinzhe@127.0.0.1' xmlns='urn:ietf:params:xml:ns:xmpp-framing' xml:lang='zh' version='1.0'/>"; 6 connection.send(steam); 7 }
Websocket下,建立流不像其他平台那樣,使用<stream:stream/>標簽,而是使用<open/>標簽,其中to屬性是域名,from是你的JID。
發出請求之后,會立刻收到服務器的響應:
第一條響應式服務器同意建立流,第二條是告訴你,安全驗證的幾種方式,其中最簡單的方式是PLAIN方式,這種方式僅僅是將你的賬號密碼進行BASE64加密后傳輸,可以說是很不安全。當然,你也可以選擇SCRAM-SHA-1的安全加密方式,只不過你的js庫要支持SHA-1加密,我因為是剛開始探路,所以一切從簡,SHA-1的加密方式請求流程跟PLAIN會有一點區別,回頭咱們再說。
另外我這邊寫了一個當收到來自服務端信息的方法:
function wsMsg(event) { console.log("Server: " + event.data); }
打印出來而已。就像上面console面板中的信息一樣。那個是chrome的調試器,其他瀏覽器也有對應的。
發起登錄驗證
剛才說了,我先用最基本的PLAIN的方式登錄:
function auth() { //Base64編碼 var token = window.btoa("wuxinzhe@127.0.0.1\0wuxinzhe"); var message = "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>" + token + "</auth>"; console.log("Client: " + message); connection.send(message); }
其中,window.btoa()是自帶的方法,不需要額外加載任何庫,所以我才說這個真的是最快最簡單的方法,然后我們看方法體內的字符串,這個字符串格式是:jid+password,以\0作為分隔符,后端java程序是一個DefaultAuthProvider提供驗證的,當然你們也可以實現自己的驗證方式,默認的方式是以\0作為分隔符,注意的是,我一開始密碼是123456,發現\0123456這樣到后端密碼會被分割成23456,\01整個會被轉譯,恩,解決的辦法要嘛就換一個分隔符(我是說后端的openfire那邊),要嘛就禁止以數字開頭的密碼。
發起安全驗證之后,會受到服務端的響應, 如果成功了,如圖:
至於要是失敗了,會返回這個錯誤,當然這個錯誤信息很少,到底是用戶名找不到,還是密碼錯誤了,單憑這個錯誤信息是看不出來的,不過也沒辦法咯,要不,去修改一下服務端唄:
當我們發起安全驗證成功了以后,緊接着就要開啟一個新的流,新的流服務端會給予一些新的XML節點權限(<iq/>、<presence/>),這樣才能發送一些其他功能的信息,比如發送消息,獲取聯系人列表,再剛開始建立的第一個流失不能發送這些節點的。
建立新流同樣適用<open/>標簽,但有一個地方與之前不同,就是這次是需要攜帶id屬性的,什么是id屬性?我們回顧第一個流建立時,服務端返回的<open/>信息,是不是就有一個id,沒錯,這個id據我的理解,每次建立websocket連接時,都會為每個連接生成一個獨一無二的id,這個id代表了這個連接,所以后續我們會在很多很多地方都需要使用這個id。
發起新的流:
1 <open xmlns='jabber:client' to='127.0.0.1' version='1.0' from='wuxinzhe@127.0.0.1' id='70tvu3ooiu' xml:lang='zh'/>
服務端會返回兩條信息,第一條,是同意打開新流,第二條,是告訴你,接下來要做的是bind操作,就是要綁定客戶端:
發起綁定也要用到剛才說的id屬性:
<iq type='set' id='6ps7q3ideb'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>
綁定的時候,還可以加入一些標簽,來對當前的客戶端,做一些比較具有語義的說明,用來描述你的客戶端類型:
<iq id="wSBRk-4" type="set"> <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"> <resource>Showings</resource> </bind> </iq>
我們看看這兩種bind之間的區別:
這是bind的客戶端請求內容和服務端響應內容,上面的是沒有加入<resource/>標簽的,下面是加入了以后。我們可以看到,服務端返回信息中的<jid/>是有區別的,當不加描述節點的時候,是將前面的id屬性直接用作后綴拼接如JID的,加入了以后自然會更具語義化。
這個過程似乎是Openfire用於區分登錄的客戶端類型的方式。這樣如果同一個賬號,在不同的客戶端登錄,也會有所區別。
然后我們要獲取session。
<iq xmlns="jabber:client" id="ak014gz6x7" type="set"><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></iq>
這部分我暫時還不知道用來干嘛的,畢竟我還沒熟悉openfire的xmpp協議的整個過程,所以有些部分不是很清楚,待我整個看過以后,到時候再來看看這個步驟是做什么用的。
此時我們進入Openfire的后台,看看用戶在線的情況:
誒?怎么有連接,卻是離線呢?不着急,因為我們雖然登陸了,但是我們還沒有“出席”。就像QQ你可以設置不同的登錄狀態,有在線、不在電腦、忙、離線,這些狀態,所以我們如果要在線,只需要發送出席請求就行了:
<presence id="ak014gz6x7"><status>Online</status><priority>1</priority></presence>
你們看,到處都要用到這個ID,當然,前面我們做了綁定動作,此刻不用id屬性,換成from="jid"應該也是有效的。
此時我們再看后台狀態:
OK了。
So我們還要下線呢,關閉連接,此時要用<open/>對應的標簽<close/>
<close xmlns="urn:ietf:params:xml:ns:xmpp-framing"/>
這樣就行了。
其實使用Websocket建立連接與XMPP協議在其他的客戶端里是沒有什么太大的區別,可能就是<open/><close/>這兩個標簽的區別。我們現在能夠順利登錄了,基本上,就是有一個好的開始了。