極簡WebSocket聊天室



最近看到了WebSocket,難免想作些什麼小功能,而後就選擇了聊天室,首先固然先介紹什麼是WebSockethtml


1. WebSocket

WebSocket 是 HTML5 開始提供的可在單個 TCP 鏈接上進行全雙工通信的協議,其容許服務端主動向客戶端推送數據,瀏覽器和服務器只須要完成一次握手,二者之間就直接能夠建立持久性的鏈接,並進行雙向數據傳輸前端


注意:WebSocket 和 HTTP 的區別,WebSocket雖創建在HTTP上,但屬於新的獨立協議,只是其創建鏈接的過程須要用到HTTP協議java


爲何須要WebSocket?web

解決HTTP協議的某些缺陷 ---- 通訊只能由客戶端發起。不少網站爲了實現推送技術,使用Ajax輪詢,這樣在沒有新消息的狀況下客戶端也要發送請求,勢必形成服務器的負擔,而WebSokcet能夠主動向客戶端推送消息,是全雙工通信,能更好的節省服務器資源和帶寬ajax


特色:

  • 協議標識符爲ws:好比 ws://www.baidu.com
  • 無同源策略限制
  • 更好的二進制支持:能夠發送字符串和二進制
  • 握手階段用HTTP
  • 數據格式輕量:WebSocket的服務端到客戶端的數據包頭只有2到10字節、HTTP每次都須要攜帶完整頭部,

鏈接過程:

一:客服端請求協議升級spring

GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:8080
Connection: Upgrade  					    // 表示要升級協議
Upgrade: websocket    						// 表示升級的協議是websocket
Sec-WebSocket-Version: 13  					// websocket版本號
Sec-WebSocket-Key: w4v7O6xFTi36lqcgctw==    // 隨機生成,防止非故意的錯誤,鏈接錯了

二:服務器響應apache

HTTP/1.1 101 Switching Protocols
Upgrade: websocket          						 // 表示能夠升級對應的協議
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUmm5OPpG2HaGWk=      // 根據客戶端key用函數計算出來

三:此後開始使用WebSocket協議api


補充:

ajax輪詢:讓瀏覽器間隔幾秒就發送一次請求,來獲取最新的響應瀏覽器

long poll:保持長鏈接來阻塞輪詢。客戶端發起請求不會馬上響應,而是有數據才返回而後關閉鏈接,而後客戶端再次發起long poll周而復始tomcat







2. 實現

這個代碼是極簡的,適合入門理解。WebSocket是一套已經規範好的標準的API,Tomcat、Spring等都實現了這套API,下面筆者用Springboot來操做


2.1 導入依賴

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

2.2 目錄結構


2.3 ServerConfig

@Configuration  // 配置類,用來註冊服務
public class serverConfig {
    @Bean  // 返回的bean會自動註冊進容器
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

2.4 MyServer

重點就在這裏,先說明一下:

  • Endpoint爲端點,可理解爲服務器接收端,WebSocket是端對端的通訊
  • Session爲會話,表示兩個端點間的交互,要和cookie和session這個區分開來
  • 方法上的註解:@OnOpen表示成功創建鏈接後調用的方法,其他類推
@Component // 註解雖然單例,但仍是會建立多例
@ServerEndpoint(value = "/wechat/{username}")  // 聲明爲服務器端點
public class MyServer {

    // 成員變量
    private Session session;
    private String username;

    // 類變量
    // 類變量涉及同步問題,用線程安全類
    // 能夠用<String room,<String username,MyServer> >來造成房間
    private static AtomicInteger onlineCount = new AtomicInteger(0);
    private static ConcurrentHashMap<String, MyServer> map = new ConcurrentHashMap<>();

    // 鏈接
    @OnOpen
    public void onOpen(@PathParam("username") String username, Session session) throws IOException {
        this.session = session;
        this.username = username;
        map.put(username, this);
        addOnlineCount();
        sendMessageAll(username + "加入了房間,當前在線人數:" + getOnlineCount());
    }

    // 關閉
    @OnClose
    public void onClose() throws IOException {
        subOnlineCount();
        map.remove(username);
        sendMessageAll(username + "退出了房間,當前在線人數:" + getOnlineCount());
    }

    // 發送錯誤
    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
    }

    // 默認羣發
    @OnMessage
    public void onMessage(String message) throws IOException {
        sendMessageAll(username + ":" + message);
    }

    // 羣發
    private void sendMessageAll(String message) throws IOException {
        for (MyServer value : map.values()) {
            value.session.getBasicRemote().sendText(message);    // 阻塞式
            // this.session.getAsyncRemote().sendText(message);  // 非阻塞式
        }
    }

    // 私發
    private void sendMessageTo(String message, String to) throws IOException {
        MyServer toUser = map.get(to);
        toUser.session.getAsyncRemote().sendText(message);
    }

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

    public static synchronized void addOnlineCount() {
        MyServer.onlineCount.getAndIncrement();
    }

    public static synchronized void subOnlineCount() {
        MyServer.onlineCount.getAndDecrement();
    }
}

2.5 index.html

筆者寫的前端不太靠譜,知道什麼意思便可~

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport"
            content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>登陸頁</title>
    </head>

    // 輸入名字,url傳參省事
    <body>
        <label for="username">Username:</label>
        <input id="username" type="text" placeholder="請輸入暱稱">
        <button id="submit" >ENTER</button>
    </body>

    <script>
        var submit = document.getElementById('submit');
        submit.addEventListener('click',function(){
            window.location.href = 'homepage.html?username=' + document.getElementById('username').value;
        })
    </script>
</html>

2.6 homepage.html

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>房間</title>
    </head>

    <body>
        <button onclick="wsClose()">退出房間</button>
        <br/><br/>
        <div id="showMessage"></div>
        <br/><br/>
        <input id="sendMessage" type="text"/>
        <button onclick="sendMessage()">發送消息</button>
    </body>

    <script>
        // 獲取url參數的暱稱
        function getQueryVariable(variable) {
            var query = window.location.search.substring(1);
            var vars = query.split("&");
            for (var i=0;i<vars.length;i++) {
                var pair = vars[i].split("=");
                if(pair[0] == variable){return pair[1];}
            }
            return(false);
        }
        var conn = "ws://localhost:8080/wechat/" + getQueryVariable("username");

        // webSocket鏈接
        var ws = new WebSocket(conn);

        // 鏈接錯誤要作什麼呢?
        ws.onerror = function () {
            showMessageInnerHTML("發生未知錯誤錯誤");
        }
        // 客戶端鏈接須要幹什麼呢?
        ws.onopen = function () {
            showMessageInnerHTML("--------------------------");
        }

        // 客戶端關閉須要幹什麼呢?
        ws.onclose = function () {
            showMessageInnerHTML("退出了當前房間");
        }

        // 收到消息
        ws.onmessage = function (even) {
            showMessageInnerHTML(even.data);
        }

        // 關閉瀏覽器時
        window.onbeforeunload = function () {
            ws.wsClose();
        }

        // 網頁上顯示消息
        function showMessageInnerHTML(msg) {
            document.getElementById('showMessage').innerHTML += msg + '<br/>';
        }

        // 發送消息
        function sendMessage() {
            var msg = document.getElementById('sendMessage').value;
            ws.send(msg);
            document.getElementById('sendMessage').value = '';
        }

        // 關閉鏈接
        function wsClose() {
            ws.close();
        }
    </script>
</html>

2.7 截圖

不想弄前端,湊合着看吧




參考

tomcat、Spring官網均有簡介及API的詳細介紹。推薦使用後者,後者符合spring規範並且更加優雅

http://tomcat.apache.org/tomcat-9.0-doc/websocketapi/index.html

https://spring.io/guides/gs/messaging-stomp-websocket/

相關文章
相關標籤/搜索