Websocket初識與聊天室Demo

寫在前面

前段時間有個面試,被問到如果要做web端登錄保持一端可用,即在多個瀏覽器登錄時,要將前一次登錄的信息及時踢出。當時我說了兩種方案:第一種 是用ajax輪詢服務器,第二種就是websocket。第一種是我剛畢業那會的實施方案,有過相關經驗,第二種是之前瞭解過,但沒有實際的開發經驗。面試完後,我就想深入去了解一下websocket,然後寫一些案例鞏固一下。
ajax輪詢與websocket對比

什麼是websocket

WebSocket是一種在單個TCP連接上進行全雙工通信的協議。WebSocket通信協議於2011年被IETF定爲標準RFC 6455,並由RFC7936補充規範。WebSocket API也被W3C定爲標準。
WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在WebSocket API中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸。(百度百科)

** 其實,websocket就是http5 後升級出來的一個協議,它即有http協議的特性,也有socket協議的特性。是兩者的交集。http是無狀態的協議,且只能主動去請求服務器的數據,websocket則保持連接,服務器可以主動推送數據給瀏覽器。**

WebSocket案例一:簡單聊天室

在大學學JAVA的時候,那時候剛接觸了socket編碼,然後就寫了簡易的聊天室。不過基本上是通過win 的cmd命令來進行,今天,也準備用這個小案來實現一下。

主要技術棧爲:

  • JDK 1.8
  • maven 3.3.9
  • springboot 2.2.7.RELEASE
  • springboot websocket

Maven 依賴

SpringBoot2.0 + 對WebSocket 已經支持 ,直接添加相關依賴即可:

<dependency>  
           <groupId>org.springframework.boot</groupId>  
           <artifactId>spring-boot-starter-websocket</artifactId>  
 </dependency> 

WebSocketConfig配置

package com.keyingbo.websocket.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

WebSocketChatRoomServer

服務端接收請求的主要配置類:

  1. 因爲WebSocket是類似客戶端服務端的形式(採用ws協議),那麼這裏的WebSocketServer其實就相當於一個ws協議的Controller

  2. 直接@ServerEndpoint("/websocketChatRoom/{userId}")@Component啓用即可,然後在裏面實現@OnOpen開啓連接,@onClose關閉連接,@onMessage接收消息等方法。

  3. 新建一個ConcurrentHashMap webSocketMap 用於接收當前userId的WebSocket,方便之間對userId進行推送消息。

package com.keyingbo.websocket.server;

import com.alibaba.fastjson.JSON;
import com.keyingbo.websocket.dto.UserMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;


/**
 * 簡易聊天室服務端代碼
 * @ServerEndpoint
 * 註解是一個類層次的註解,它的功能主要是將目前的類定義成一個websocket服務器端,
 * 註解的值將被用於監聽用戶連接的終端訪問URL地址,客戶端可以通過這個URL來連接到WebSocket服務器端
 */
@ServerEndpoint("/websocketChatRoom/{userId}")
@Component
public class WebSocketChatRoomServer {
    static Logger logger = LoggerFactory.getLogger(WebSocketChatRoomServer.class);
    //靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。
    private static final AtomicInteger OnlineCount = new AtomicInteger(0);
    //concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。若要實現服務端與單一客戶端通信的話,可以使用Map來存放,其中Key可以爲用戶標識
    private static ConcurrentHashMap<String, WebSocketChatRoomServer> webSocketSet = new ConcurrentHashMap<String, WebSocketChatRoomServer>();
    //與某個客戶端的連接會話,需要通過它來給客戶端發送數據
    private Session WebSocketsession;
    //當前發消息的人員userId
    private String userId = "";

    /**
     * 連接建立成功調用的方法*/
    @OnOpen
    public void onOpen(@PathParam(value = "userId") String param, Session WebSocketsession, EndpointConfig config) {
        userId = param;
        //log.info("authKey:{}",authKey);
        this.WebSocketsession = WebSocketsession;
        webSocketSet.put(param, this);//加入map中
        int cnt = OnlineCount.incrementAndGet(); // 在線數加1
        logger.info("有連接加入,當前連接數爲:{}", cnt);
        UserMessage userMessage = new UserMessage();
        userMessage.setType("system");
        userMessage.setMessage("連接成功");
        sendMessage(this.WebSocketsession, userMessage);
    }

    /**
     * 連接關閉調用的方法
     */
    @OnClose
    public void onClose() {
        if (!userId.equals("")){
            webSocketSet.remove(userId);//從set中刪除
            int cnt = OnlineCount.decrementAndGet();
            logger.info("有連接關閉,當前連接數爲:{}", cnt);
        }
    }

    /**
     * 收到客戶端消息後調用的方法
     *
     * @param message 客戶端發送過來的消息*/
    @OnMessage
    public void onMessage(String message, Session session) {
        logger.info("來自客戶端的消息:{}",message);
        UserMessage userMessage = JSON.parseObject(message , UserMessage.class);
        userMessage.setType("usermsg");
        broadCastInfo(userMessage);
    }

    /**
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        logger.error("發生錯誤:{},Session ID: {}",error.getMessage(),session.getId());
        error.printStackTrace();
    }

    /**
     * 發送消息,實踐表明,每次瀏覽器刷新,session會發生變化。
     * @param message
     */
    public void sendMessage(Session session, UserMessage message) {
        try {
            session.getBasicRemote().sendText(JSON.toJSONString(message));
            //session.getBasicRemote().sendText(String.format("%s",message));
        } catch (IOException e) {
            logger.error("發送消息出錯:{}", e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 羣發消息
     * @param message
     * @throws IOException
     */
    public void broadCastInfo(UserMessage message) {
        for (String key : webSocketSet.keySet()) {
            Session session = webSocketSet.get(key).WebSocketsession;
            //&& !userId.equals(key)
            if(session != null && session.isOpen() ){
                sendMessage(session, message);
            }
        }
    }

    /**
     * 指定Session發送消息
     * @param message
     * @throws IOException
     */
    public void sendToUser(String userId, UserMessage message) {
        WebSocketChatRoomServer webSocketServer = webSocketSet.get(userId);
        if ( webSocketServer != null && webSocketServer.WebSocketsession.isOpen()){
            sendMessage(webSocketServer.WebSocketsession, message);
        }
        else{
            logger.warn("當前用戶不在線:{}",userId);
        }
    }
}

webSocketDemo.html

然後,就是客戶端的demo:
主要的邏輯就是建立websocket連接,以及綁定相關的事件。直接看代碼:

<!DOCTYPE html>
<html>
<head>
    <title>簡易聊天Demo</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, user-scalable=no">
    <link href="https://cdn.bootcss.com/bootstrap/3.3.2/css/bootstrap.min.css" rel="stylesheet">
    <link href="../css/webSocketDemo.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <div class="title">簡易聊天demo</div>
    <div class="content">
        <div class="show-area"></div>
        <div class="write-area">
            <div>
                <button class="btn btn-default send">發送</button>
            </div>
            <div><input name="name" id="name" type="text" placeholder="input your name"></div>
            <div>
                <textarea name="message" id="message" cols="38" rows="4" placeholder="input your message..."></textarea>
            </div>
        </div>
    </div>
</div>

<script src="http://libs.baidu.com/jquery/1.9.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
<script>
    $(function () {
        var lockReconnect = false; //避免重複連接
        var userId = "user" + new Date().getMilliseconds();
        var wsurl = 'ws://127.0.0.1:8080/websocketChatRoom/user' + userId;
        var websocket;
        var i = 0;
        if (window.WebSocket) {
            doConnect();//連接
            function send() {
                var name = $('#name').val();
                var message = $('#message').val();
                if (!name) {
                    alert('請輸入用戶名!');
                    return false;
                }
                if (!message) {
                    alert('發送消息不能爲空!');
                    return false;
                }
                var msg = {
                    message: message,
                    name: name
                };
                try {
                    websocket.send(JSON.stringify(msg));
                } catch (ex) {
                    console.log(ex);
                }
            }

            //按下enter鍵發送消息
            $(window).keydown(function (event) {
                if (event.keyCode == 13) {
                    console.log('user enter');
                    send();
                }
            });

            //點發送按鈕發送消息
            $('.send').bind('click', function () {
                send();
            });

        }
        else {
            alert('該瀏覽器不支持web socket');
        }

        function getJSON(str) {
            if (typeof str == 'string') {
                try {
                    return JSON.parse(str);
                } catch(e) {
                    return null;
                }
            }
        }

        function reconnect() {
            if (lockReconnect) return;
            lockReconnect = true;
            //沒連接上會一直重連,設置延遲避免請求過多
            setTimeout(function() {
                try {
                    doConnect();
                    lockReconnect = false;
                } catch (e) {
                    reconnect()
                }
            }, 2000);
        }

        function  doConnect() {
            websocket = new WebSocket(wsurl);
            //連接建立
            websocket.onopen = function (evevt) {
                console.log("Connected to WebSocket server.");
                $('.show-area').append('<p class="bg-info message"><i class="glyphicon glyphicon-info-sign"></i>Connected to WebSocket server!</p>');
            }
            //收到消息
            websocket.onmessage = function (event) {
                var msg = getJSON(event.data); //解析收到的json消息數據
                var type = msg.type; // 消息類型
                var umsg = msg.message; //消息文本
                var uname = msg.name; //發送人
                i++;
                if (type == 'usermsg') {
                    $('.show-area').append('<p class="bg-success message"><i class="glyphicon glyphicon-user"></i><a name="' + i + '"></a><span class="label label-primary">' + uname + ' : </span>' + umsg + '</p>');
                }
                if (type == 'system') {
                    $('.show-area').append('<p class="bg-warning message"><a name="' + i + '"></a><i class="glyphicon glyphicon-info-sign"></i>' + umsg + '</p>');
                }

                $('#message').val('');
                window.location.hash = '#' + i;
            }

            //發生錯誤
            websocket.onerror = function (event) {
                i++;
                console.log("Connected to WebSocket server error");
                $('.show-area').append('<p class="bg-danger message"><a name="' + i + '"></a><i class="glyphicon glyphicon-info-sign"></i>Connect to WebSocket server error.</p>');
                //重連
                reconnect();
            }

            //連接關閉
            websocket.onclose = function (event) {
                i++;
                console.log('websocket Connection Closed. ');
                $('.show-area').append('<p class="bg-warning message"><a name="' + i + '"></a><i class="glyphicon glyphicon-info-sign"></i>websocket Connection Closed.</p>');
                //斷線重連
                reconnect();
                //window.location.hash = '#' + i;
            }
        }
    });


</script>
</body>
</html>

結語

以上案例,我本地已經跑通,具體的源碼地址爲:
源碼地址
啓動後,本案例訪問步驟說明:

  1. 打開多個瀏覽器或者多個頁籤輸入:http://localhost:8080/html/webSocketDemo.html
  2. 在各自頁面輸入相關的信息,點擊發送可以看到效果。
    最後說下:本案的代碼有參考網上的大佬,而且時間比較倉促,寫的有點粗糙,見諒,有問題私聊,謝謝。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章