麗楓,鄭力新,王佳斌
(華僑大學 工學院,福建 泉州 362021)
摘要:隨著互聯網技術的不斷發展,Web技術在各個領域得到了不同程度的運用,人們對于Web應用的實時性提出了更高的要求,HTML5WebSocket協議因此得到了廣泛的關注。通過對基于HTTP的傳統Web實時通信方案進行分析,針對其中的不足與缺點,深入介紹了基于HTML5 WebSocket協議的實時通信機制以及相對于傳統方案的優勢,并通過使用Node.js的Express框架和HTML5 WebSocket協議的第三方應用程序編程接口Socket.io類庫實現了一個基于WebSocket協議的Web應用。經實驗表明,所描述的研究能成功地在客戶端和服務器端完成基于HTML5 WebSocket協議的實時通信過程并建立連接。
0引言
隨著互聯網技術的高速發展,人們對Web應用的實時性要求越來越高,傳統的Web實時通信方案已經無法滿足一些現實應用的需求。在長期的Web應用過程中該傳統方案逐漸露出資源浪費、實時性不高等問題,這些問題的出現對一些實時性要求較高的Web應用(如在線游戲、在線證券、設備監控等)造成了不好的用戶體驗。除此之外,這些不足還會制約Web實時通信的性能,對通信效率造成影響。面對這種情況,HTML5規范中定義了WebSocket協議來實現更好的用戶體驗和實時通信功能,并針對傳統的Web實時通信方案在實際運用中產生的資源浪費問題進行改善,提高通信效率。
目前,WebSocket協議的實現主要分為客戶端和服務器端兩部分。對于其客戶端而言,許多的主流瀏覽器(包括個人電腦和移動終端)如谷歌、火狐、IE等都在不同的版本上支持WebSocket客戶端應用程序編程接口。而對于其服務器端而言,也有許多常見的應用服務器如WebSphere、WebLogic、Tomcat等在不同的版本上支持WebSocket服務器端應用程序編程接口。綜上所述,本文從傳統的Web實時通信方案出發,針對其在Web應用中所體現的不足與缺點,深入研究WebSocket協議在Web實時通信方面的原理與優勢,并根據該協議的通信機制進行實現。
1傳統的Web實時通信方案
1.1輪詢
在早期的Web應用中,所采用的Web實時通信方案是輪詢。在使用輪詢時,客戶端需要頻繁地向服務器端發送HTTP請求來保持客戶端和服務器端的同步以便不斷地刷新客戶端所要呈現的信息。在這個過程中,客戶端無法確定合適的時間間隔向服務器端發送HTTP請求。若間隔的時間太短,客戶端頻繁的請求將會給服務器端造成巨大的壓力;若間隔的時間太長,就無法滿足客戶端和服務器端實時通信的要求。由于客戶端在頻繁地發送請求時服務器端的數據可能還未進行更新,導致服務器端返回的大部分應答包中的數據域為空,因而產生了很多無謂的網絡傳輸,浪費了大量的帶寬資源和其他網絡資源。對于圖1客戶端與服務器端的交互圖每次的HTTP請求而言,過長的HTTP頭信息也會占用不必要的帶寬資源。因此,這是一種缺乏靈活性又低效的Web實時通信方案。其中客戶端和服務器端的交互過程如圖1(a)所示。
1.2Comet技術
目前,Comet技術[1]的實現方式包括基于異步JavaScript和可擴展標記語言(Asynchronous JavaScript and Extensible Markup Language,AJAX)的長輪詢方式和基于Iframe的流方式。這兩種方式針對輪詢都做出了較大的改進。
1.2.1基于AJAX的長輪詢方式
基于AJAX的長輪詢方式[2]通過采用AJAX技術讓客戶端向服務器端發送HTTP請求,進而與服務器端建立連接,且該連接會在服務器端保持一段時間。若服務器端檢測到有新數據產生,那么它會將這些數據通過連接發送至客戶端,然后關閉連接;若服務器端在連接存在期間都沒有產生新的數據發送至客戶端,那么它將會向客戶端發送一個超時信息,然后關閉連接。無論服務器端的數據是否還在更新,在連接關閉之后,客戶端都需要重新向服務器端發送HTTP請求來建立連接。其中客戶端和服務器端的交互過程如圖1(b)所示。
雖然這種方式能夠對客戶端的部分頁面進行更新,減少服務器端發送的數據量,降低客戶端請求的頻率,減少無效的網絡傳輸,但當服務器端更新數據的速度較快時,基于AJAX的長輪詢方式將變成普通的輪詢,不僅會降低其性能,而且還會對服務器端造成較大的處理壓力。除此之外,為了保持HTTP連接長時間處于打開狀態,服務器端也需要消耗一定的服務器資源。因此,使用基于AJAX的長輪詢方式會產生資源浪費的問題。
1.2.2基于Iframe的流方式
基于Iframe的流方式[3]通過客戶端頁面上內嵌的一個Iframe標簽向服務器端發送HTTP請求,服務器端在響應該請求后與客戶端建立一條長連接。連接建立后,服務器端通過不斷地更新該連接的狀態以保持其不過期。當服務器端檢測到有新數據產生時,它會將新數據通過該連接發送給客戶端;當客戶端和服務器端之間的通信出現問題導致連接出現錯誤或者關閉時,客戶端會立即發出連接請求與服務器端重新建立連接,否則該連接會一直持續,不會關閉。其中客戶端和服務器端的交互過程如圖1(c)所示。
雖然這種方式有利于減少客戶端的請求次數,減輕客戶端和服務器端之間的網絡負擔,避免因頻繁的建立連接和關閉連接所帶來的資源浪費,但由于基于Iframe的流方式在連接過程中始終只維持一個長連接,因此客戶端頁面會一直處于加載過程中而無法顯示頁面加載完成,從而影響用戶體驗。且當有多個客戶端同時向服務器端發送HTTP請求時,由于服務器端長期只維持一個連接,因此會導致服務器端在這種高并發狀態下的處理能力降低,造成大量的服務器資源和其他網絡資源被消耗。
由于基于AJAX的長輪詢方式和基于Iframe的流方式在通信過程中一直采用HTTP作為通信協議,因此每次的HTTP請求和應答所攜帶的完整的HTTP頭信息不僅增加了實時更新信息時的數據傳輸量,還造成帶寬資源的浪費。此外,為了維持和協調通信過程中HTTP連接隨時處于可用狀態,服務器端也需要消耗資源。對于HTTP連接的建立和關閉過程而言,服務器端新產生的數據有可能會因為無法及時發送到客戶端而導致客戶端的數據丟失。由于這兩種方式對Web應用中的實時信息和非實時信息的請求/響應方式都未發生改變,因此,當實時信息的請求較為頻繁時,可能會造成服務器端較大的處理壓力,從而影響非實時信息的呈現。其中基于HTTP的Web實時應用模型如圖2所示。
2傳統的Web實時通信方案
WebSocket協議[45]是HTML5規范中的一種新的通信協議,是能夠在客戶端和服務器端進行異步通信的一種方法。它支持客戶端與服務器端通過全雙工通信的方式實現實時通信,本質上是一個基于傳輸控制協議的協議。因此,WebSocket連接的建立過程與傳輸控制協議連接的建立過程有些相似,客戶端和服務器端需要通過“握手”來建立WebSocket連接。
首先由客戶端向服務器端發送一個HTTP請求,該請求不同于一般的HTTP請求,它包含了一些附加的HTTP頭信息,其中一條信息“Upgrade:WebSocket”表明這是一個申請將當前HTTP協議升級為WebSocket協議的HTTP請求。若服務器端收到該請求后能正確解讀其HTTP頭信息,那么它會返回一個基于HTTP的應答報文給客戶端,此時連接建立成功[6],之后,客戶端和服務器端便可以通過該連接主動向對方發送或者接收數據,直到其中一方主動關閉該連接。其中客戶端和服務器端的交互過程如圖3所示。
通過WebSocket協議,客戶端和服務器端之間只要做一個“握手”的動作就可以建立一條雙向通信的通道。這不僅讓服務器端可以主動與客戶端互發信息,而且還避免了因客戶端頻繁請求而造成的網絡資源浪費、實時通信效率低、服務器處理壓力大等問題[7]。由于WebSocket連接采用WebSocket協議作為通信協議,因此在傳輸過程中數據幀的頭部信息所占的字節數將大大降低,從而有效地減小了通信過程中傳輸的數據量和網絡負載,節約了帶寬資源。在基于WebSocket協議的實時通信方案中,Web應用中的實時部分和非實時部分被加以區分。客戶端使用WebSocket協議獲取實時內容,使用HTTP協議獲取非實時內容。而服務器端則采用兩種不同的模塊來處理實時的WebSocket請求和非實時的HTTP請求,其應用模型如圖4所示。
通過上述模型可以看出,該實時通信方案使服務器端的結構更加明確,不僅讓WebSocket協議和HTTP協議各司其職、互不干擾,而且還降低了系統的耦合性,在最大程度上發揮了兩個模塊的功能。此外,由于采用以傳輸控制協議為基礎的WebSocket協議來處理實時服務,因此可以保證傳輸數據過程中的穩定性和及時性,在較大程度上提高了實時通信的性能。相對于傳統方案來說,該方案不僅減小了對服務器資源的浪費,也減輕了服務器端的處理壓力。
3基于WebSocket的Web實時通信應用實例
本文采用基于Node.js[8]的Express框架和Socket.io類庫來實現基于WebSocket的Web實時通信應用。其中,Node.js是一個JavaScript運行平臺,可用于構建響應速度快、容易擴展的網絡程序。但由于Node.js中只提供了大量的低端功能,因此文中將使用Express框架進行Web實時通信應用的開發。Express是一個能夠在Node.js中使用的 Web應用程序開發框架,它提供的一系列強大的特性,能夠讓Web應用程序的開發變得更加方便、快速。
Socket.io是一個開源、跨平臺且支持客戶端和服務器端進行實時雙向通信的WebSocket庫[9-10]。它包括客戶端的JavaScript庫和服務器端的Node.js模塊。它能夠根據不同的客戶端自動在一些實時通信機制中選擇合適的一個來實現Web實時應用。當使用支持HTML5技術的瀏覽器客戶端進行實時通信時,Socket.io會選譯效率最高、消耗服務器資源最少的WebSocket協議來實現實時通信,并在瀏覽器客戶端發生變化時自動選擇其他方式進行通信。因此,Socket.io能有效解決跨平臺的實時通信問題。
3.1在線聊天室的設計
在線聊天室的設計分為客戶端與服務器端兩個部分,其實時通信過程如圖5所示。
3.2在線聊天室的實現
在線聊天室的實現也分為客戶端和服務器端兩個部分。其中客戶端通過使用HTML5、層疊樣式表以及JavaScript來實現用戶名的驗證功能、消息顯示功能和數據傳送功能。服務器端通過JavaScript來實現與客戶端的實時通信功能、廣播功能以及在線用戶列表的管理功能。圖8用戶登錄成功時客戶端與服務器端的交互圖3.2.1客戶端的實現過程
當有新的客戶端用戶加入聊天室時,已在聊天室的用戶將會接收到新用戶加入聊天室的消息且用戶列表會被即時更新以顯示新加入的用戶名。新用戶所在頁面也會被更新以顯示所有在線用戶。當有客戶端用戶在聊天室發送聊天消息時,該消息會被即時廣播給所有在線用戶。當有客戶端用戶退出聊天室時,其他在線用戶將會接收到該用戶退出聊天室的消息且用戶列表會被實時更新以移除下線用戶的用戶名。下線用戶所在的頁面也會進行相應的調整。若用戶在聊天過程中直接退出聊天室頁面,則所有在線用戶都會收到該用戶退出聊天室的消息。客戶端的具體實現流程如圖6所示。
3.2.2服務器端的實現過程
當有多個客戶端用戶存在時,服務器端的主要功能包括管理所有在線用戶的用戶列表以及廣播它們之間的聊天消息。服務器端的具體實現流程如圖7所示。
3.2.3客戶端和服務器端的交互過程
本文主要針對用戶成功登錄進聊天室的情況進行介紹。當用戶成功登錄在線聊天室時,客戶端和服務器端通過觸發事件進行實時交互,其具體交互過程如圖8所示。
4結論
傳統的Web實時通信方案是在長期的應用實踐中發展出來的,其中比較常用的是基于AJAX的長輪詢方式和基于Iframe的流方式。但由于這兩種方案都是采用基于HTTP的通信方式,因此當Web實時應用采用這兩種方案時會產生難以解決的問題。而WebSocket協議的出現適時地提供了一種新的Web實時通信方案,它能夠更加快捷有效地構建出簡單高效的Web實時應用。因此,本文通過分析傳統的Web實時通信方案的不足之處,不僅從理論層面分析了基于WebSocket的Web實時通信方案的優勢,而且還通過使用HTML5、層疊樣式表和JavaScript編寫了具體的應用實例簡單的實現了該方案。隨著WebSocket協議的不斷發展,基于WebSocket的Web實時通信方案將會被廣泛應用。
參考文獻
[1] 蔡驥然,曹海傳.B/S架構下基于OPC與Comet技術的實時監控系統[J].計算機應用,2012,32(z2):214216.
[2] 文愛平,文德民.基于IE瀏覽器的Ajax Comet架構[J].電腦知識與技術,2010,6(17):46464648.
[3] 張家愛,孫飛.Comet技術在Web開發中的研究與應用[J].煤炭技術,2011,30(12):153154.
[4] 陸晨,馮向陽,蘇厚勤.HTML5 WebSocket握手協議的研究與實現[J].計算機應用與軟件,2015,32(1):128131,178.
[5] 李代立,陳榕.WebSocket在Web實時通信領域的研究[J].電腦知識與技術,2010,6(28):79237925,7935.
[6] 周東仿,孟寧.基于WebSocket的網絡設備自發現機制[J].計算機工程與設計,2013,34(2):392396,438.
[7] 溫照松,易仁偉,姚寒冰.基于WebSocket的實時Web應用解決方案[J].電腦知識與技術,2012,8(16):38263828.
[8] 王金龍,宋斌,丁銳.Node.js:一種新的Web應用構建技術[J].現代電子技術,2015,38(6):7073.
[9] 李廣文.基于Socket.io的互動教學即時反饋系統的設計與實現[J].中國現代教育裝備,2012(18):1012.
[10] 黃經贏.基于Socket.io+Node.js+Redis構建高效即時通訊系統[J].現代計算機(專業版),2014(19):6264,69.
于html5的Websocket網頁即時通訊技術,前端開發采用ExtJS前端框架
JavaEE框架:Mybatis、SpringMVC
先去官網下載ExtJS框架的資料文件:
https://www.sencha.com/products/extjs/evaluate/
可以參考中文翻譯過來的官網查看API:
http://extjs-doc-cn.github.io/ext4api/
下載集成的jar:
websocket.css:
@CHARSET "UTF-8"; .l-im-message-warn { font-family: "微軟雅黑"; cursor: default; width: 100%; padding: 5px 0px 5px 25px; -webkit-user-select : none; background: url("../images/information.png") no-repeat 5; } .l-im-message { font-family: "微軟雅黑"; cursor: default; width: 100%; } .l-im-message-over { background-color: rgba(233, 233, 233, 0.5); } .l-im-message-selected { background-color: rgba(250, 218, 90, 0.5); } .l-im-message-header { font-size: 12px; padding: 5px 0px 5px 10px; } .l-im-message-header-self { color: green; } .l-im-message-header-remote { color: blue; } .l-im-message-body { font-size: 12px; padding: 2px 0px 2px 20px; } .user-win { background-image: url( ../images/user_win.png ) !important; } .user-online { background-image: url( ../images/group.png ) !important; } .user { background-image: url( ../images/user.gif ) !important; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
websocket.js:
var websocket; var isCreatw = false; var title=""; var win; var input; var isQj = true; var toUser=""; function toUserMsg(toU){ if((!isQj && toUser == toU) || toU == user){ win.setTitle(title + " (已連接) 【現在全局對話】"); isQj = true; toUser = ""; }else{ win.setTitle(title + " (已連接) 【現在單獨與"+toU+"對話】"); isQj = false; toUser = toU; } } function creatw() { if(isCreatw){ alert("已經啟動"); return; }else{ isCreatw = true; } //創建用戶輸入框 input = Ext.create('Ext.form.field.HtmlEditor', { region : 'south', height : 120, enableFont : false, enableSourceEdit : false, enableAlignments : false, listeners : { initialize : function() { Ext.EventManager.on(me.input.getDoc(), { keyup : function(e) { if (e.ctrlKey === true && e.keyCode == 13) { e.preventDefault(); e.stopPropagation(); send(); } } }); } } }); //創建消息展示容器 var output = Ext.create('MessageContainer', { region : 'center' }); var dialog = Ext.create('Ext.panel.Panel', { region : 'center', layout : 'border', items : [input, output], buttons : [{ text : '發送', handler : send }] }); //初始話WebSocket function initWebSocket() { if (window.WebSocket) { websocket = new WebSocket(encodeURI('ws://'+wimadress)); websocket.onopen = function() { //連接成功 win.setTitle(title + ' (已連接) 【現在全局對話】'); websocket.send('admin'+user); } websocket.onerror = function() { //連接失敗 win.setTitle(title + ' (連接發生錯誤)'); } websocket.onclose = function() { //連接斷開 win.setTitle(title + ' (已經斷開連接)'); } //消息接收 websocket.onmessage = function(message) { var message = JSON.parse(message.data); //接收用戶發送的消息 if (message.type == 'message') { output.receive(message); } else if (message.type == 'get_online_user') { //獲取在線用戶列表 var root = onlineUser.getRootNode(); Ext.each(message.list,function(user){ var node = root.createNode({ id : user, text : user, iconCls : 'user', leaf : true }); root.appendChild(node); }); } else if (message.type == 'user_join') { //用戶上線 var root = onlineUser.getRootNode(); var user = message.user; var node = root.createNode({ id : user, text : user, iconCls : 'user', leaf : true }); root.appendChild(node); } else if (message.type == 'user_leave') { //用戶下線 var root = onlineUser.getRootNode(); var user = message.user; var node = root.findChild('id',user); root.removeChild(node); } } } }; //在線用戶樹 var onlineUser = Ext.create('Ext.tree.Panel', { title : '在線用戶', rootVisible : false, region : 'east', width : 150, lines : false, useArrows : true, autoScroll : true, split : true, iconCls : 'user-online', store : Ext.create('Ext.data.TreeStore', { root : { text : '在線用戶', expanded : true, children : [] } }) }); title = '歡迎您:' + user; //展示窗口 win = Ext.create('Ext.window.Window', { title : title + ' (未連接)', layout : 'border', iconCls : 'user-win', minWidth : 650, minHeight : 460, width : 650, animateTarget : 'websocket_button', height : 460, items : [dialog,onlineUser], border : false, listeners : { render : function() { initWebSocket(); } } }); win.show(); win.on("close",function(){ websocket.send('LeaveAdmin'); isCreatw = false; }); //發送消息 function send() { var content = input.getValue(); if(toUser != ""){content = "admin886"+toUser+"admin888" + content;} var message = {}; if (websocket != null) { if (input.getValue()) { Ext.apply(message, { from : user, content : content, timestamp : new Date().getTime(), type : 'message' }); websocket.send(JSON.stringify(message)); //output.receive(message); input.setValue(''); } } else { Ext.Msg.alert('提示', '您已經掉線,無法發送消息!'); } } }; //用于展示用戶的聊天信息 Ext.define('MessageContainer', { extend : 'Ext.view.View', trackOver : true, multiSelect : false, itemCls : 'l-im-message', itemSelector : 'div.l-im-message', overItemCls : 'l-im-message-over', selectedItemCls : 'l-im-message-selected', style : { overflow : 'auto', backgroundColor : '#fff' }, tpl : [ '<div class="l-im-message-warn">?歡迎使用即時通訊系統。</div>', '<tpl for=".">', '<div class="l-im-message">', '<div class="l-im-message-header l-im-message-header-{source}">{from} {timestamp}</div>', '<div class="l-im-message-body">{content}</div>', '</div>', '</tpl>'], messages : [], initComponent : function() { var me = this; me.messageModel = Ext.define('Leetop.im.MessageModel', { extend : 'Ext.data.Model', fields : ['from', 'timestamp', 'content', 'source'] }); me.store = Ext.create('Ext.data.Store', { model : 'Leetop.im.MessageModel', data : me.messages }); me.callParent(); }, //將服務器推送的信息展示到頁面中 receive : function(message) { var me = this; message['timestamp'] = Ext.Date.format(new Date(message['timestamp']), 'H:i:s'); if(message.from == user){ message.source = 'self'; }else{ message.source = 'remote'; } me.store.add(message); if (me.el.dom) { me.el.dom.scrollTop = me.el.dom.scrollHeight; } } }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
業務代碼編寫:
ChatServer.java
package com.appms.websocket; import java.io.IOException; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.util.Date; import net.sf.json.JSONObject; import org.java_websocket.WebSocket; import org.java_websocket.WebSocketImpl; import org.java_websocket.framing.Framedata; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.server.WebSocketServer; /** * 即時通訊 */ public class ChatServer extends WebSocketServer{ public ChatServer(int port) throws UnknownHostException { super(new InetSocketAddress(port)); } public ChatServer(InetSocketAddress address) { super(address); } /** * 觸發連接事件 */ @Override public void onOpen( WebSocket conn, ClientHandshake handshake ) { } /** * 觸發關閉事件 */ @Override public void onClose( WebSocket conn, int code, String reason, boolean remote ) { userLeave(conn); } /** * 客戶端發送消息到服務器時觸發事件 */ @Override public void onMessage(WebSocket conn, String message){ message = message.toString(); if(null != message && message.startsWith("admin")){ this.userjoin(message.replaceFirst("admin", ""),conn); }if(null != message && message.startsWith("LeaveAdmin")){ this.userLeave(conn); }if(null != message && message.contains("admin886")){ String toUser = message.substring(message.indexOf("admin886")+8, message.indexOf("admin888")); message = message.substring(0, message.indexOf("admin886")) +"[私信] "+ message.substring(message.indexOf("admin888")+8, message.length()); ChatServerPool.sendMessageToUser(ChatServerPool.getWebSocketByUser(toUser),message);//向所某用戶發送消息 ChatServerPool.sendMessageToUser(conn, message);//同時向本人發送消息 }else{ ChatServerPool.sendMessage(message.toString());//向所有在線用戶發送消息 } } public void onFragment( WebSocket conn, Framedata fragment ) { } /** * 觸發異常事件 */ @Override public void onError( WebSocket conn, Exception ex ) { ex.printStackTrace(); if( conn != null ) { //some errors like port binding failed may not be assignable to a specific websocket } } /** * 用戶加入處理 * @param user */ public void userjoin(String user, WebSocket conn){ JSONObject result = new JSONObject(); result.element("type", "user_join"); result.element("user", "<a onclick=\"toUserMsg('"+user+"');\">"+user+"</a>"); ChatServerPool.sendMessage(result.toString()); //把當前用戶加入到所有在線用戶列表中 String joinMsg = "{\"from\":\"[系統]\",\"content\":\""+user+"上線了\",\"timestamp\":"+new Date().getTime()+",\"type\":\"message\"}"; ChatServerPool.sendMessage(joinMsg); //向所有在線用戶推送當前用戶上線的消息 result = new JSONObject(); result.element("type", "get_online_user"); ChatServerPool.addUser(user,conn); //向連接池添加當前的連接對象 result.element("list", ChatServerPool.getOnlineUser()); ChatServerPool.sendMessageToUser(conn, result.toString()); //向當前連接發送當前在線用戶的列表 } /** * 用戶下線處理 * @param user */ public void userLeave(WebSocket conn){ String user = ChatServerPool.getUserByKey(conn); boolean b = ChatServerPool.removeUser(conn); //在連接池中移除連接 if(b){ JSONObject result = new JSONObject(); result.element("type", "user_leave"); result.element("user", "<a onclick=\"toUserMsg('"+user+"');\">"+user+"</a>"); ChatServerPool.sendMessage(result.toString()); //把當前用戶從所有在線用戶列表中刪除 String joinMsg = "{\"from\":\"[系統]\",\"content\":\""+user+"下線了\",\"timestamp\":"+new Date().getTime()+",\"type\":\"message\"}"; ChatServerPool.sendMessage(joinMsg); //向在線用戶發送當前用戶退出的消息 } } public static void main( String[] args ) throws InterruptedException , IOException { WebSocketImpl.DEBUG = false; int port = 8887; //端口 ChatServer s = new ChatServer(port); s.start(); System.out.println( "服務器的端口" + s.getPort() ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
ChatServerPool.java:
package com.appms.websocket; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.java_websocket.WebSocket; /** * 即時通訊 */ public class ChatServerPool { private static final Map<WebSocket,String> userconnections = new HashMap<WebSocket,String>(); /** * 獲取用戶名 * @param session */ public static String getUserByKey(WebSocket conn){ return userconnections.get(conn); } /** * 獲取WebSocket * @param user */ public static WebSocket getWebSocketByUser(String user){ Set<WebSocket> keySet = userconnections.keySet(); synchronized (keySet) { for (WebSocket conn : keySet) { String cuser = userconnections.get(conn); if(cuser.equals(user)){ return conn; } } } return null; } /** * 向連接池中添加連接 * @param inbound */ public static void addUser(String user, WebSocket conn){ userconnections.put(conn,user); //添加連接 } /** * 獲取所有的在線用戶 * @return */ public static Collection<String> getOnlineUser(){ List<String> setUsers = new ArrayList<String>(); Collection<String> setUser = userconnections.values(); for(String u:setUser){ setUsers.add("<a onclick=\"toUserMsg('"+u+"');\">"+u+"</a>"); } return setUsers; } /** * 移除連接池中的連接 * @param inbound */ public static boolean removeUser(WebSocket conn){ if(userconnections.containsKey(conn)){ userconnections.remove(conn); //移除連接 return true; }else{ return false; } } /** * 向特定的用戶發送數據 * @param user * @param message */ public static void sendMessageToUser(WebSocket conn,String message){ if(null != conn && null != userconnections.get(conn)){ conn.send(message); } } /** * 向所有的用戶發送消息 * @param message */ public static void sendMessage(String message){ Set<WebSocket> keySet = userconnections.keySet(); synchronized (keySet) { for (WebSocket conn : keySet) { String user = userconnections.get(conn); if(user != null){ conn.send(message); } } } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
寫個過濾器,在項目執行時啟動:
package com.appms.filter; import java.io.IOException; import java.net.UnknownHostException; import java.util.Calendar; import java.util.Date; import java.util.Timer; import java.util.TimerTask; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import org.java_websocket.WebSocketImpl; import com.appms.base.BaseController; import com.appms.base.Const; import com.appms.utils.Tools; import com.appms.websocket.ChatServer; import com.appms.websocket.OnlineChatServer; public class StartFilter extends BaseController implements Filter{ /** * 初始化 */ public void init(FilterConfig fc) throws ServletException { this.startWebsocketInstantMsg(); this.startWebsocketOnline(); } /** * 啟動即時聊天服務 */ public void startWebsocketInstantMsg(){ WebSocketImpl.DEBUG = false; ChatServer s = null; try { String strWEBSOCKET = Tools.readTxtFile(Const.WEBSOCKET);//讀取WEBSOCKET配置,獲取端口配置 if(null != strWEBSOCKET && !"".equals(strWEBSOCKET)){ String strIW[] = strWEBSOCKET.split(",fh,"); if(strIW.length == 4){ s = new ChatServer(Integer.parseInt(strIW[1])); s.start(); } } System.out.println( "websocket服務器啟動,端口" + s.getPort() ); } catch (UnknownHostException e) { e.printStackTrace(); } } /** * 啟動在線管理服務 */ public void startWebsocketOnline(){ WebSocketImpl.DEBUG = false; OnlineChatServer s = null; try { String strWEBSOCKET = Tools.readTxtFile(Const.WEBSOCKET);//讀取WEBSOCKET配置,獲取端口配置 if(null != strWEBSOCKET && !"".equals(strWEBSOCKET)){ String strIW[] = strWEBSOCKET.split(",fh,"); if(strIW.length == 4){ s = new OnlineChatServer(Integer.parseInt(strIW[3])); s.start(); } } System.out.println( "websocket服務器啟動,端口" + s.getPort() ); } catch (UnknownHostException e) { e.printStackTrace(); } } //計時器 public void timer() { Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.HOUR_OF_DAY, 9); // 控制時 calendar.set(Calendar.MINUTE, 0); // 控制分 calendar.set(Calendar.SECOND, 0); // 控制秒 Date time = calendar.getTime(); // 得出執行任務的時間 Timer timer = new Timer(); timer.scheduleAtFixedRate(new TimerTask() { public void run() { //PersonService personService = (PersonService)ApplicationContext.getBean("personService"); } }, time, 1000*60*60*24);// 這里設定將延時每天固定執行 } public void destroy() { // TODO Auto-generated method stub } public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2) throws IOException, ServletException { // TODO Auto-generated method stub } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
在web.xml里配置:
<filter> <filter-name>startFilter</filter-name> <filter-class>com.appms.filter.StartFilter</filter-class> </filter> 1 2 3 4
在jsp頁面進行調用:
<script type="text/javascript">var wimadress="127.0.0.1:8887";</script> <script type="text/javascript">var oladress="127.0.0.1:8889";</script> <link rel="stylesheet" type="text/css" href="plugins/websocket/ext4/resources/css/ext-all.css"> <link rel="stylesheet" type="text/css" href="plugins/websocket/css/websocket.css" /> <script type="text/javascript" src="plugins/websocket/ext4/ext-all-debug.js"></script> <script type="text/javascript" src="plugins/websocket/websocket.js"></script> <!--引入屬于此頁面的js --> <script type="text/javascript" src="source/js/jquery-1.8.3.js"></script> 1 2 3 4 5 6 7 8 9
點擊li標簽跳出聊天頁面
ul class="am-avg-sm-1 am-avg-md-4 am-margin am-padding am-text-center admin-content-list "> <li onclick="creatw();"><a href="javascript:;"><span class="am-icon-btn am-icon-file-text"></span><br/>即時通訊<br/></a></li> <li><a href="fusioncharts/index.do" class="am-text-warning"><span class="am-icon-btn am-icon-briefcase"></span><br/>圖表統計<br/></a></li> <li><a href="#" class="am-text-danger"><span class="am-icon-btn am-icon-recycle"></span><br/>昨日訪問<br/>80082</a></li> <li><a href="#" class="am-text-secondary"><span class="am-icon-btn am-icon-user-md"></span><br/>在線用戶<br/>3000</a></li> </ul> 1 2 3 4 5 6
私聊:
群聊:
基于ExtJS前端框架的Websocket即時通訊系統
ventSource 是 HTML5 新增的特性,用于實現服務器端數據實時推送到客戶端。
使用 EventSource 可以這樣實現服務端數據實時推送:
服務器:
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class StreamServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/event-stream");
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.write("data: some data\n\n");
writer.flush();
}
}
客戶端:
<script>
var source = new EventSource('/stream');
source.onmessage = function(e) {
console.log(e.data);
};
</script>
然后在客戶端使用 EventSource 連接到 http://yourserver/stream
工作原理:
這里的服務端使用了 Servlet 實現數據推送,reactor 模式下,每次調用 writer.write() 都會觸發響應被刷新,實現了數據的實時推送。
EventSource 特性兼容 IE10 以上瀏覽器,其他瀏覽器也都有較好的支持,所以在實際場景中比較方便使用。希望這個示例能幫你明白 EventSource 的工作原理和基本使用方法。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。