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

“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 cn.com.taiji.controller;

import cn.com.taiji.util.StringUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;


/**
 * @ServerEndpoint 注解是一個類層次的注解,它的功能主要是將目前的類定義成一個websocket服務器端,
 * 注解的值將被用於監聽用戶連接的終端訪問URL地址,客戶端可以通過這個URL來連接到WebSocket服務器端
 */
@ServerEndpoint(value = "/websocket/{userJson}")
@Component
public class MyWebSocket {

    static Log log= LogFactory.getLog(MyWebSocket.class);

    //靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。
    private static int onlineCount = 0;

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

    //concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。若要實現服務端與單一客戶端通信的話,可以使用Map來存放,其中Key可以為用戶標識
//    private static CopyOnWriteArraySet<MyWebSocket> webSocketSet = new CopyOnWriteArraySet<MyWebSocket>();
    //記錄每個房間的
//    private static ConcurrentHashMap<String,MyWebSocket> webSocketMap = new ConcurrentHashMap<>();
    /**
     *  與某個客戶端的連接對話,需要通過它來給客戶端發送消息
     */
    private Session session;
    /**
     * 標識當前連接客戶端的用戶名
     */
    private String userName;
    /**
     * 標識當前連接客戶端的房間名稱
     */
    private String roomName;

    /**
     * 連接建立成功調用的方法
     * @param session  可選的參數。session為與某個客戶端的連接會話,需要通過它來給客戶端發送數據
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userJson") String userJson){
        try{
            //解析發送的報文
            String[] split = userJson.split(",");
            this.roomName =  split[0].trim();
            if(split.length>=1){
                this.userName = split[1].trim();
            }
            this.session = session;
            // 將session按照房間名來存儲,將各個房間的用戶隔離
            if (!rooms.containsKey(roomName)) {
                // 對應房間不存在時,創建房間
                ConcurrentHashMap<String,MyWebSocket> room = new ConcurrentHashMap<>();
                // 添加用戶
                if (StringUtil.isEmpty(this.userName)){
                    room.put(session.getId(),this);
                }else {
                    room.put(userName,this);
                }
                rooms.put(roomName, room);
            } else {
                // 房間已存在,直接添加用戶到相應的房間
                rooms.get(roomName).put(userName,this);
            }
            addOnlineCount();//在線數加1
            log.info("有新連接加入!當前在線人數為" + getOnlineCount());
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 收到客戶端消息后調用的方法
     * @param msg 客戶端發送過來的消息
     * @param session 可選的參數
     */
    @OnMessage
    public void receiveMsg(String msg, Session session) throws Exception {
        // 此處應該有html過濾
        // 接收到信息后進行廣播
        String roomName = this.roomName;
        broadcast(roomName, msg);
    }

    // 按照房間名進行廣播
    public static void broadcast(String roomName, String msg) throws Exception {
        ConcurrentHashMap<String, MyWebSocket> map = rooms.get(roomName);
        for(String key:map.keySet()){//keySet獲取map集合key的集合  然后在遍歷key即可
            try{
                MyWebSocket myWebSocket = map.get(key);
                myWebSocket.sendMessage(msg);//
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    /**
     * 連接關閉調用的方法
     */
    @OnClose
    public void onClose(@PathParam("roomName") String roomName, Session session){
        String userName = this.userName;
        if (StringUtil.isEmpty(userName)){
            rooms.get(roomName).remove(session.getId());
        }else {
            rooms.get(roomName).remove(userName);
        }
        subOnlineCount();
        log.info("用戶退出:"+userName+",當前在線人數為:" + getOnlineCount());
    }


    /**
     * 發生錯誤時調用
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error){
        log.error("發生錯誤");
        error.printStackTrace();
    }

    /**
     * 這個方法與上面幾個方法不一樣。沒有用注解,是根據自己需要添加的方法。
     * @param message
     * @throws IOException
     */
    public void sendMessage(String message) throws IOException{
        this.session.getBasicRemote().sendText(message);
        //this.session.getAsyncRemote().sendText(message);
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        MyWebSocket.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        MyWebSocket.onlineCount--;
    }

    private boolean isjson(String str) {
        try {
            JSONObject jsonStr = JSONObject.parseObject(str);
            return true;
        }catch (Exception e) {
            return false;
        }
    }
}

 

頁面代碼

<%@ page language="java" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<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="userName" type="hidden" name="userName" value="${userName}">
<input id="dsrdw" type="hidden" name="dsrdw" value="${dsrdw}">
<input id="input_roomName" type="text" value="${caseNum}">
<button onclick="initWebSocket()">進入聊天室</button>
<button onclick="closeWs()">退出聊天室</button>
<div class="msg_board"></div>
<input id="input_msg" size="43" maxlength="40">
<button onclick="send_msg()">發送</button>
</body>
<script type="text/javascript">
    var webSocket = null;

    function send_msg() {
        var t = "";
        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 = "";
            // var msg_board = document.getElementsByClassName("msg_board")[0];
            // var received_msg = input_msg;
            // var old_msg = msg_board.innerHTML;
            // msg_board.innerHTML = old_msg + received_msg + "<br>";
            // // 讓滾動塊往下移動
            // msg_board.scrollTop = msg_board.scrollTop + 40;
        } else {
            alert("您已掉線,請重新進入聊天室...");
        }
    };

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

    function initWebSocket() {
        var roomName = document.getElementById("input_roomName").value;
        var userName = document.getElementById("userName").value;
        // 房間名不能為空
        if (roomName == null || roomName == "") {
            alert("請輸入房間名");
            return;
        }
        var userJson = roomName+","+userName;
        if ("WebSocket" in window) {
            if (webSocket == null) {
                var url = "ws://127.0.0.1:8080/websocket/" + userJson;
                // 打開一個 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>

 


原文鏈接:https://blog.csdn.net/zhengholien/java/article/details/76696509


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM