1. 程式人生 > >websocket實現多房間聊天室

websocket實現多房間聊天室

眾所周知,Web 應用的互動過程通常是客戶端通過瀏覽器發出一個請求,伺服器端接收請求後進行處理並返回結果給客戶端,客端瀏覽器將資訊呈現。但是對於實時性要求較高、海量併發的應用,比如金融證券的實時資訊,web導航應用中地理位置獲取,社交網路的實時訊息推送等。

方案一:輪詢,客戶端用js程式碼每隔一定時間向伺服器傳送請求,這樣會造成資源浪費(浪費頻寬),在高併發的情況下還可能造成伺服器奔潰。

方案二:基於Flash、AdobeFlash,通過socket實現資料資訊互動,再利用Flash暴露的介面供js呼叫,但是Flash在移動網際網路上的支援不好,IOS和Android都不支援Flash了。

方案三

:WebSocket,2014年開始,各大應用伺服器和瀏覽器廠商逐步統一,J2EE7也實現了WebSocket協議,無論客戶端還是伺服器都提供了對其的支援。


WebSocket介紹與原理

WebSocket 是 HTML5 一種新的協議。它實現了瀏覽器與伺服器全雙工通訊,能更好的節省伺服器資源和頻寬並達到實時通訊,它建立在 TCP 之上,同 HTTP 一樣通過 TCP 來傳輸資料,但是它和HTTP 最大不同是:

WebSocket 是一種雙向通訊協議,在基於http建立連線後,WebSocket 伺服器和 browser都能主動向對方傳送或接收資料,就像 Socket 一樣;WebSocket 需要類似 TCP 的客戶端和伺服器端通過

握手連線,連線成功後才能相互通訊,實現長連線。

WebSocket 客戶端連線報文

GET /webfin/websocket/ HTTP/1.1

Host: localhost

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==

Origin: http://localhost:8080

Sec-WebSocket-Version: 13

可以看到,客戶端發起的 WebSocket 連線報文類似傳統 HTTP 報文,”Upgrade:websocket”引數值表明這是 WebSocket 型別請求,
“Sec-WebSocket-Key”是 WebSocket 客戶端傳送的一個 base64 編碼的密文,要求服務端必須返回一個對應加密的“Sec-WebSocket-Accept”應答,否則客戶端會丟擲“Error during WebSocket handshake”錯誤,並關閉連線。

WebSocket 服務端響應報文

HTTP/1.1 101 Switching Protocols

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=

“Sec-WebSocket-Accept”的值是服務端採用與客戶端一致的金鑰計算出來後返回客戶端的,“HTTP/1.1 101 Switching Protocols”表示服務端接受 WebSocket 協議的客戶端連線,經過這樣的請求-響應處理後,客戶端服務端的 WebSocket 連線握手成功, 後續就可以進行 TCP 通訊了。

下載javax.websocket.jar,使用註解方式實現了一個簡單的多房間聊天demo,demo只有一個服務端類和一個前端chat.html頁面,開啟多個chat.html頁面,輸入相同的房間名,進入房間後可以相互通訊,不同房間不能互相通訊,不同使用者我用websocket的session自己分配的id來區分,因為一個使用者連線到webSocket伺服器就對應一個session,實際開發可以用http的session中登入使用者名稱來區分,連線到伺服器的url中,roomName是一個路徑引數,即在chat.html中獲取到房間名。多房間的原理其實就是把多個用(session)放在roomName對應的set集合中,每次廣播資訊只在房間名對應的set集合中廣播,實現房間聊天資訊的隔離。

程式碼如下:

package webSocketTest;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.HashSet;
import java.util.Set;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * writer: holien
 * Time: 2017-08-01 13:00
 * Intent: webSocket伺服器
 */
@ServerEndpoint("/webSocket/chat/{roomName}")
public class WsServer {

    // 使用map來收集session,key為roomName,value為同一個房間的使用者集合
    // concurrentMap的key不存在時報錯,不是返回null
    private static final Map<String, Set<Session>> rooms = new ConcurrentHashMap();

    @OnOpen
    public void connect(@PathParam("roomName") String roomName, Session session) throws Exception {
        // 將session按照房間名來儲存,將各個房間的使用者隔離
        if (!rooms.containsKey(roomName)) {
            // 建立房間不存在時,建立房間
            Set<Session> room = new HashSet<>();
            // 新增使用者
            room.add(session);
            rooms.put(roomName, room);
        } else {
            // 房間已存在,直接新增使用者到相應的房間
            rooms.get(roomName).add(session);
        }
        System.out.println("a client has connected!");
    }

    @OnClose
    public void disConnect(@PathParam("roomName") String roomName, Session session) {
        rooms.get(roomName).remove(session);
        System.out.println("a client has disconnected!");
    }

    @OnMessage
    public void receiveMsg(@PathParam("roomName") String roomName,
                           String msg, Session session) throws Exception {
        // 此處應該有html過濾
        msg = session.getId() + ":" + msg;
        System.out.println(msg);
        // 接收到資訊後進行廣播
        broadcast(roomName, msg);
    }

    // 按照房間名進行廣播
    public static void broadcast(String roomName, String msg) throws Exception {
        for (Session session : rooms.get(roomName)) {
                session.getBasicRemote().sendText(msg);
        }
    }

}
<!DOCTYPE html><html lang="en">
<head>
    <meta charset="UTF-8">
    <title>網路聊天室</title>
</head>
<style type="text/css">
    .msg_board {
        width: 322px;
        height: 100px;
        border: solid 1px darkcyan;
        padding: 5px;
        overflow-y: scroll;
        // 文字長度大於div寬度時換行顯示
        word-break: break-all;
    }
    /*set srcoll start*/
    ::-webkit-scrollbar
    {
        width: 10px;
        height: 10px;
        background-color: #D6F2FD;
    }
    ::-webkit-scrollbar-track
    {
        -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
        /*border-radius: 5px;*/
        background-color: #D6F2FD;
    }
    ::-webkit-scrollbar-thumb
    {
        height: 20px;
        /*border-radius: 10px;*/
        -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
        background-color: #89D7F7;
    }
    /*set srcoll end*/
</style>
<body>
<label>房間名</label>
<input id="input_roomName" size="10" maxlength="10">
<input type="button"  value="進入聊天室" onclick="initWebSocket()" />
<input type="button" value="退出聊天室" onclick="closeWs()" /><br>
<div class="msg_board"></div>
<input id="input_msg" size="43" maxlength="40">
<input type="button" value="傳送" onclick="send_msg()" />
</body>
<script type="text/javascript">
    var webSocket;

    function send_msg() {
        if (webSocket != null) {
            var input_msg = document.getElementById("input_msg").value.trim();
            if (input_msg == "") {
                return;
            }
            webSocket.send(input_msg);
            // 清除input框裡的資訊
            document.getElementById("input_msg").value = "";
        } else {
            alert("您已掉線,請重新進入聊天室...");
        }
    };

    function closeWs() {
        webSocket.close();
    };

    function initWebSocket() {
        var roomName = document.getElementById("input_roomName").value;
        // 房間名不能為空
        if (roomName == null || roomName == "") {
            alert("請輸入房間名");
            return;
        }
        if ("WebSocket" in window) {
//            alert("您的瀏覽器支援 WebSocket!");
            if (webSocket == null) {
                var url = "ws://localhost:8080/webSocket/chat/" + roomName;
                // 開啟一個 web socket
                webSocket = new WebSocket(url);
            } else {
                alert("您已進入聊天室...");
            }

            webSocket.onopen = function () {
                alert("已進入聊天室,暢聊吧...");
            };

            webSocket.onmessage = function (evt) {
                var msg_board = document.getElementsByClassName("msg_board")[0];
                var received_msg = evt.data;
                var old_msg = msg_board.innerHTML;
                msg_board.innerHTML = old_msg + received_msg + "<br>";
                // 讓滾動塊往下移動
                msg_board.scrollTop = msg_board.scrollTop + 40;
            };

            webSocket.onclose = function () {
                // 關閉 websocket,清空資訊板
                alert("連線已關閉...");
                webSocket = null;
                document.getElementsByClassName("msg_board")[0].innerHTML = "";
            };
        }
        else {
            // 瀏覽器不支援 WebSocket
            alert("您的瀏覽器不支援 WebSocket!");
        }
    }
</script>
</html>

同時開啟3個chat.html頁面,2個頁面進入名為“技術分享會”的房間,剩下一個進入名為“spring原始碼分析”的房間,只有相同房間的資訊可以互通,截圖如下:




發覺要更好的學習一種技術,得把該技術運用在具體功能實現上,這樣會迫使我們去找資料和API,使得對該技術的認識更深刻...