java中websocket的應用

在上一篇文章中,筆者簡要介紹了websocket的應用場景及優勢,戳這裏javascript

這篇文章主要來介紹一下在java項目中,特別是java web項目中websocket的應用。html

場景:我作了一個商城系統,跟大多數商城系統,分爲客戶端和後臺,客戶端供客戶瀏覽,下單,購買,後臺主要管理商品,處理訂單,發貨等。我如今要實現的功能是,當客戶端有客戶下單,而且支付完成之後,主動推送消息給後臺,讓後臺的人知道,好去處理髮貨等事宜。前端

首先,咱們要知道websocket是一個鏈接,這個鏈接是客戶端(頁面)與服務端之間的鏈接,因此咱們要分兩部分來完成這個鏈接,服務端代碼和客戶端代碼。java

1.首先,在pom.xml引入以下jar包。web

<!-- websocket -->
        <dependency>
            <groupId>org.java-websocket</groupId>
            <artifactId>Java-WebSocket</artifactId>
            <version>1.3.0</version>
        </dependency>

2.而後咱們要知道的是,websocket是客戶端和服務端之間創建了一個鏈接,創建完鏈接之後,會生成一個websocket對象,咱們能夠用這個對象來執行發送,接收等操做。可是這只是一個存在於客戶端與服務器之間的連接,換句話說,系統只能識別到這個websocket鏈接是對應於哪一個頁面(瀏覽器),而這個頁面在系統中是對應哪一個用戶(數據庫中的用戶,或者根本就沒有對應任何用戶,即未登陸,只是一個遊客),咱們是沒法從這個websocket對象中獲取的。因此咱們須要建立一個Map對象,用於將websocket對象和實際的user對象進行關聯,這樣爲咱們後續向特定的用戶推送消息作鋪墊。ajax

爲此,咱們建立一個WsPool,即websocket鏈接池的類,該類用於管理現實中的用戶和websocket對象之間的關聯。代碼以下。數據庫

package com.xdx.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 WsPool { private static final Map<WebSocket, String> wsUserMap = new HashMap<WebSocket, String>(); /** * 經過websocket鏈接獲取其對應的用戶 * * @param conn * @return
     */
    public static String getUserByWs(WebSocket conn) { return wsUserMap.get(conn); } /** * 根據userName獲取WebSocket,這是一個list,此處取第一個 * 由於有可能多個websocket對應一個userName(但通常是隻有一個,由於在close方法中,咱們將失效的websocket鏈接去除了) * * @param user */
    public static WebSocket getWsByUser(String userName) { Set<WebSocket> keySet = wsUserMap.keySet(); synchronized (keySet) { for (WebSocket conn : keySet) { String cuser = wsUserMap.get(conn); if (cuser.equals(userName)) { return conn; } } } return null; } /** * 向鏈接池中添加鏈接 * * @param inbound */
    public static void addUser(String userName, WebSocket conn) { wsUserMap.put(conn, userName); // 添加鏈接
 } /** * 獲取全部鏈接池中的用戶,由於set是不容許重複的,因此能夠獲得無重複的user數組 * * @return
     */
    public static Collection<String> getOnlineUser() { List<String> setUsers = new ArrayList<String>(); Collection<String> setUser = wsUserMap.values(); for (String u : setUser) { setUsers.add(u); } return setUsers; } /** * 移除鏈接池中的鏈接 * * @param inbound */
    public static boolean removeUser(WebSocket conn) { if (wsUserMap.containsKey(conn)) { wsUserMap.remove(conn); // 移除鏈接
            return true; } else { return false; } } /** * 向特定的用戶發送數據 * * @param user * @param message */
    public static void sendMessageToUser(WebSocket conn, String message) { if (null != conn && null != wsUserMap.get(conn)) { conn.send(message); } } /** * 向全部的用戶發送消息 * * @param message */
    public static void sendMessageToAll(String message) { Set<WebSocket> keySet = wsUserMap.keySet(); synchronized (keySet) { for (WebSocket conn : keySet) { String user = wsUserMap.get(conn); if (user != null) { conn.send(message); } } } } }

3.接下來咱們編寫websocket的主程序類,該類用於管理websocket的生命週期。該類繼承自WebSocketServer ,這是一個實現了runnable接口的類,他的構造函數須要傳入一個端口,因此咱們須要爲websocket服務指定一個端口,該類有四個要重載的方法,onOpen()方法在鏈接建立成功之後調用,onClose在鏈接關閉之後調用,onError方法在鏈接發生錯誤的時候調用(通常鏈接出錯之後觸發了onError,也會緊接着觸發onClose方法)。onMessage方法在收到客戶端發來消息的時候觸發。咱們能夠在這個方法中處理客戶端所傳遞過來的消息。json

package com.xdx.websocket; import java.net.InetSocketAddress; import org.java_websocket.WebSocket; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.server.WebSocketServer; public class WsServer extends WebSocketServer { public WsServer(int port) { super(new InetSocketAddress(port)); } public WsServer(InetSocketAddress address) { super(address); } @Override public void onOpen(WebSocket conn, ClientHandshake handshake) { // ws鏈接的時候觸發的代碼,onOpen中咱們不作任何操做
 } @Override public void onClose(WebSocket conn, int code, String reason, boolean remote) { //斷開鏈接時候觸發代碼
 userLeave(conn);
System.out.println(reason); } @Override
public void onMessage(WebSocket conn, String message) {
System.out.println(message);
if(null != message &&message.startsWith("online")){ String userName=message.replaceFirst("online", message);//用戶名 userJoin(conn,userName);//用戶加入 }else if(null != message && message.startsWith("offline")){ userLeave(conn); } } @Override public void onError(WebSocket conn, Exception ex) { //錯誤時候觸發的代碼 System.out.println("on error"); ex.printStackTrace(); } /** * 去除掉失效的websocket連接 * @param conn */ private void userLeave(WebSocket conn){ WsPool.removeUser(conn); } /** * 將websocket加入用戶池 * @param conn * @param userName */ private void userJoin(WebSocket conn,String userName){ WsPool.addUser(userName, conn); } }

上述onMessage()方法中,咱們接收到客戶端傳過來的一個message(消息),而這個客戶端對應的websocket鏈接也被當成一個參數一塊兒傳遞過來,咱們經過message中攜帶的信息來斷定這條信息對應是什麼操做,若是是以online開頭,則說明它是一條上線的是信息,咱們就把該websocket和其對應的userName存入ws鏈接池中,若是是以offline開頭,則說明websocket斷開了,咱們也沒有必要維護這個websocket對應的map鍵值對,把它去除掉就行了。數組

4.如何在服務端開啓這個socket呢。咱們上面有說到WsServer的父類WebSocketServer 實現了一個runnable方法,因而可知咱們須要在一個線程中運行這個WsServer,事實上,WebSocketServer 有個start()方法,其源碼以下。瀏覽器

 

    public void start() { if( selectorthread != null ) throw new IllegalStateException( getClass().getName() + " can only be started once." ); new Thread( this ).start();; }

 

很顯然,它開了一個線程。因此咱們能夠用下面這樣的方法來開啓一個websocket線程。(該方法只是針對普通的java項目,若是是web項目須要在項目啓動的時候運行websocket線程,後面第7點會講)

    public static void main(String args[]){ WebSocketImpl.DEBUG = false; int port = 8887; // 端口
        WsServer s = new WsServer(port); s.start(); }

咱們運行這個main方法,就開啓了websocket的服務端。

到目前爲止,咱們尚未編寫任何客戶端的代碼。咱們如何測試已經開了websocket服務端呢?網上有一個免費的測試工具。測試地址以下:http://www.blue-zero.com/WebSocket/

點擊進去,寫上咱們的websocket服務地址。點擊鏈接。如圖所示。

 

 若是鏈接成功,他就會顯示「鏈接已創建,正在等待數據……」

咱們再文本框中輸入onlinexdx,而後點回車試試。

 

 這就是模擬客戶端向服務端發送onlinexdx請求,按前面的介紹,它會觸發服務端的onMessage方法,咱們看一下服務端控制檯。除了咱們但願看到的onlinexdx這個message,還有一些@heart,果真觸發了這個方法。

@heart是這個免費頁面發送過來的心跳檢測包,目的是讓websocket一直處於鏈接狀態。

5.好了,接下來咱們要來完成客戶端部分的功能。我想要實現的是後臺用戶登陸之後,進入到後臺主頁的時候,執行websocket鏈接工做(就相似於上一步在免費頁面點擊鏈接按鈕),而後向服務端發送「online+userName」這條消息,用以觸發服務端的onMessage方法,就能夠將該userName加入到鏈接池了。咱們將這些代碼封裝到js中。

var websocket = ''; var ajaxPageNum = 1; var last_health; var health_timeout = 10; var tDates = [], tData = []; var rightIndex; if ($('body').attr('userName') != '' && $('body').attr('ws') == 'yes') { var userName = $('body').attr('userName'); if (window.WebSocket) { websocket = new WebSocket( encodeURI('ws://' + document.domain + ':8887')); websocket.onopen = function() { console.log('已鏈接'); websocket.send("online"+userName); heartbeat_timer = setInterval(function() { keepalive(websocket) }, 60000); }; websocket.onerror = function() { console.log('鏈接發生錯誤'); }; websocket.onclose = function() { console.log('已經斷開鏈接'); initWs(); }; // 消息接收
        websocket.onmessage = function(message) { console.log(message) showNotice("新訂單", "您有新的逸品訂單,請及時處理!") }; } else { alert("該瀏覽器不支持下單提醒。<br/>建議使用高版本的瀏覽器,<br/>如 IE十、火狐 、谷歌  、搜狗等"); } } var initWs = function() { if (window.WebSocket) { websocket = new WebSocket( encodeURI('ws://' + document.domain + ':8887')); websocket.onopen = function() { console.log('已鏈接'); websocket.send("online"+userName); heartbeat_timer = setInterval(function() { keepalive(websocket) }, 60000); }; websocket.onerror = function() { console.log('鏈接發生錯誤'); }; websocket.onclose = function() { console.log('已經斷開鏈接'); initWs(); }; // 消息接收
        websocket.onmessage = function(message) { console.log(message) showNotice("新訂單", "您有新的逸品訂單,請及時處理!") }; } else { alert("該瀏覽器不支持下單提醒。<br/>建議使用高版本的瀏覽器,<br/>如 IE十、火狐 、谷歌  、搜狗等"); } } var vadioTimeOut; function showNotice(title, content) { if (!title && !content) { title = "新訂單"; content = "您有新的訂單,請及時處理!"; } var iconUrl = "http://www.wonyen.com/favicon.ico"; $("#myaudio")[0].play();// 消息播放語音 var playTime = 1; var audio = document.createElement("myaudio"); clearTimeout(vadioTimeOut); audio.addEventListener('ended', function() { vadioTimeOut = setTimeout(function() { playTime = playTime + 1; playTime < 3 ? audio.play() : clearTimeout(vadioTimeOut); }, 500); }) if (Notification.permission == "granted") { var notification = new Notification(title, { body : content, icon : iconUrl }); notification.onclick = function() { notification.close(); }; } } // 心跳包
function keepalive(ws) { var time = new Date(); if (last_health != -1 && (time.getTime() - last_health > health_timeout)) { // ws.close();
    } else { if (ws.bufferedAmount == 0) { ws.send('~HC~'); } } }

頁面的主要代碼以下。首先是引入上述js.這個js必須放在頁面最後,由於它須要加載完頁面以獲取body的attr。

<!-- websocket -->
<script src="./static/js/OtherJs/ws.js" type="text/javascript"></script>

其次是在頁面的body處加入userName和ws屬性。做爲參數傳遞到js裏面。還須要加入語音附件。

<body  userName=${adminName} ws="yes">
<!-- 消息提示音 -->
   <audio id="myaudio" src="./static/new_order.wav"></audio>

上述js代碼清楚地展現了websocket在頁面端的生命週期,須要注意的是,咱們在onopen()方法中,先是向服務端發送online+userName進行上線處理,緊接着開始調用心跳包,避免websocket長時間閒置而失效。

onmessage方法中,咱們處理收到服務端推送過來的消息,而後以語音和彈出窗的形式提醒客戶端。固然,咱們在服務端能夠經過封裝message這個參數,把它變成一個json對象,給這個json對象一個msgType屬性,這樣就能夠根據msgType的不一樣來執行不一樣的前端代碼,好比msgType=newOrder表示有新的訂單,就執行新訂單到來的代碼,msgType=newUser表示有新的用戶註冊,就執行新用戶註冊的代碼。這邊我沒作區分,由於我在服務端只發送用戶購買訂單的消息,因此全部的消息,我都執行showNotice("新訂單", "您有新的逸品訂單,請及時處理!")這個方法。

6.最後一步,咱們來編寫從服務端向客戶端發送消息的方法。一旦有訂單到來,咱們就向全部的後臺用戶發送消息。咱們能夠模擬一下這個動做,寫一個controller方法,調用WsPool的sendMessageToAll方法。

@ResponseBody @RequestMapping("sendWs") public String sendWs(String message) { WsPool.sendMessageToAll(message); return message; }

7.對了,若是是web項目,咱們還須要在項目啓動的時候開啓websocket服務端線程,能夠把啓動的動做放在一個filter中,而後在web.xml裏面配置這個filter,使它在項目啓動時候運行。

package com.xdx.filter; import java.io.IOException; 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.xdx.websocket.WsServer; public class StartFilter implements Filter { public void destroy() { } public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2) throws IOException, ServletException { } public void init(FilterConfig arg0) throws ServletException { this.startWebsocketInstantMsg(); } /** * 啓動即時聊天服務 */
    public void startWebsocketInstantMsg() { WebSocketImpl.DEBUG = false; WsServer s; s = new WsServer(8887); s.start(); } }

在web.xml配置。

<!-- filter -->
    <filter>
        <filter-name>startFilter</filter-name>
        <filter-class>com.xdx.filter.StartFilter</filter-class>
    </filter>

至此,咱們完成了全部的代碼。

8.測試,先運行起項目。而後登錄之後,進入後臺主頁,能夠看到,已經鏈接成功了。

而後咱們從服務端向後臺發送一條消息,執行sendWs這個方法。在瀏覽器輸入http://192.168.1.185:8080/warrior/sendWs?message=xxx

會播放語音,而且彈出提示。證實成功了。

上述只是websocket的一個簡單的應用,在此基礎上,咱們還能夠作不少擴展的工做,好比作聊天室,股票實時價格顯示等,只要掌握了原理就好作了。

相關文章
相關標籤/搜索