基於springboot的websocket聊天室


一、概述

1.Http

#http簡介
HTTP是一個應用層協議,無狀態的,端口號為80。主要的版本有1.0/1.1/2.0.

#http1.0/1.1/2.0
1.HTTP/1.* 一次請求-響應,建立一個連接,用完關閉;
2.HTTP/1.1 串行化單線程處理,可以同時在同一個tcp鏈接上發送多個請求,但是只有響應是有順序的,只有上一個請求完成后,下一個才能響應。一旦有任務處理超時等,后續任務只能被阻塞(線頭阻塞);
3.HTTP/2 並行執行。某任務耗時嚴重,不會影響到任務正常執行

2.WebSocket

#WebSocket簡介
Websocket是html5提出的一個協議規范,是為解決客戶端與服務端實時通信。本質上是一個基於tcp,先通過HTTP/HTTPS協議發起一條特殊的http請求進行握手后創建一個用於交換數據的TCP連接.WebSocket同HTTP一樣也是應用層的協議,但是它是一種雙向通信協議。

#WebSocket連接過程
1. 瀏覽器、服務器建立TCP連接,三次握手。這是通信的基礎,傳輸控制層,若失敗后續都不執行。
2. TCP連接成功后,瀏覽器通過HTTP協議向服務器傳送WebSocket支持的版本號等信息。(開始前的HTTP握手)
3. 服務器收到客戶端的握手請求后,同樣采用HTTP協議回饋數據。
4. 當收到了連接成功的消息后,通過TCP通道進行傳輸通信。

#WebSocket優勢
瀏覽器和服務器只需要要做一個握手的動作,然后單獨建立一條TCP的通信通道進行數據的傳送, 在
建立連接之后,雙方可以在任意時刻,相互推送信息。同時,服務器與客戶端之間交換的頭信息很小。

3.Socket

#socket簡介
Socket並不是一個協議,而是為了方便使用TCP或UDP而抽象出來的一層,是位於應用層和傳輸控制層之間的一組接口。

Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。

#socket作用
當兩台主機通信時,必須通過Socket連接,Socket則利用TCP/IP協議建立TCP連接。TCP連接則更依靠於底層的IP協議,IP協議的連接則依賴於鏈路層等更低層次。

4.WebSocket 和 Http

#相同點
1. 都是一樣基於TCP的,都是可靠性傳輸協議。
2. 都是應用層協議。

#不同點
1. WebSocket是雙向通信協議,模擬Socket協議,可以雙向發送或接受信息。HTTP是單向的。
2. WebSocket是需要握手進行建立連接的

#聯系
WebSocket在建立握手時,數據是通過HTTP傳輸的。但是建立之后,在真正傳輸時候是不需要HTTP協議的。

5.WebSocket 和 Socket

#區別
Socket是傳輸控制層協議,WebSocket是應用層協議。

6.長連接,短連接

#短連接
連接->傳輸數據->關閉連接
HTTP是無狀態的,瀏覽器和服務器每進行一次HTTP操作,就建立一次連接,但任務結束就中斷連接。
也可以這樣說:短連接是指SOCKET連接后發送后接收完數據后馬上斷開連接。
#長連接
連接->傳輸數據->保持連接 -> 傳輸數據-> ... ->關閉連接。
長連接指建立SOCKET連接后不管是否使用都保持連接,但安全性較差。

7.http和websocket的長連接區別

HTTP1.1通過使用Connection:keep-alive進行長連接,HTTP 1.1默認進行持久連接。在一次 TCP 連接中可以完成多個 HTTP 請求,但是對每個請求仍然要單獨發 header,Keep-Alive不會永久保持連接,它有一個保持時間,可以在不同的服務器軟件(如Apache)中設定這個時間。這種長連接是一種“偽鏈接”

websocket的長連接,是一個真的全雙工。長連接第一次tcp鏈路建立之后,后續數據可以雙方都進行發送,不需要發送請求頭。
 
keep-alive雙方並沒有建立正真的連接會話,服務端可以在任何一次請求完成后關閉。WebSocket 它本身就規定了是正真的、雙工的長連接,兩邊都必須要維持住連接的狀態

傳統 HTTP 請求響應客戶端服務器交互圖

WebSocket 模式客戶端與服務器的交互圖

二、WebSocket請求報文和Http的差別

1.響應頭

HTTP/1.1 101 
Upgrade: websocket
Connection: upgrade #協議升級成功 
Sec-WebSocket-Accept: IiuAbVCofO973oDeSggnpHFjLeU= #服務端處理之后的key 
Sec-WebSocket-Extensions: permessage-deflate #擴展協議
Date: Fri, 02 Aug 2019 03:29:14 GMT

2.請求頭

Host: xiajibagao.top:8080 #升級協議的服務主機地址端口
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13 #當前WebSocket協議版本號
Origin: http://xiajibagao.top:8080
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: wJfIzjPgehJrCXmKSvL8cQ== #連接憑證,客戶端將這個key發送給服務器,服務器將這個key進行處理,將處理后的key返回給客戶端,客戶端根據這個key是否正確來判斷是否建立連接。
Connection: keep-alive, Upgrade
Cookie: UM_distinctid=16c31bfc8219-0079038beea7fb-4c312c7c-144000-16c31bfc8226a3; CNZZDATA1277674304=827412020-1564198032-%7C1564319195
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket #通知服務器協議升級為WebSocket

三、基於springboot的簡單聊天室

1.新建index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>這是一個聊天室</title>

        <meta name="viewport" content="width=device-width, initial-scale=1">
        <!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
        <!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

        <style>
            #showarea{
                border:1px solid darkgrey;
                border-radius:10px;
                min-height: 350px;
                padding: 10px;
            }
        </style>

    </head>
    <body>

        <div class="container">
            <div class="row">
                <br />
                <div class="col-sm-4">
                    <div class="form-group">
                        <!--用戶名-->
                        <input type="text" class="form-control" value="匿名" id="username">
                        <br />
                        <!--聊天內容-->
                        <textarea value="輸入聊天內容" rows="10" class="form-control" id="sendarea"></textarea>
                        <br />
                        <!--提交按鈕-->
                        <button class="btn btn-block btn-success" onclick="submitmsg()">發送</button>
                    </div>
                </div>

                <div class="col-sm-8">
                    <!--消息列表展示區-->
                    <div id="showarea">

                    </div>

                </div>

            </div>
        </div>

    </body>
</html>

2.頁面的js

<script>
    //當打開頁面時新建WebSocket連接
    var host = window.location.host;
    var url = "ws://"+host+"/webchat/chat";//訪問到后台項目注解所在的類
    var ws = new WebSocket(url); //new一個新ws對象,new完即新建立一條“管道”

	/**
    *ws.onopen方法,當連接建立成功后觸發
    */
    ws.onopen=function(){
        onsole.log("連接成功!");
    };

    /**
    *發送消息給服務器
    */
    function submitmsg() {
            //獲取用戶名和要發送的消息  
            var username = document.getElementById("username").value;
            var sendarea = document.getElementById("sendarea").value;
            //轉換為json字符串    
            var jsonmsg ={
                username:username,
                sendarea:sendarea,
                time:new Date()
            }
            //發送消息
            ws.send(JSON.stringify(jsonmsg));
        }

    /**
    *從服務器接收消息
    *ws.onmessage方法,當后台接受發送信息時觸發
    */
	ws.onmessage = function(evn){
        //轉換為json字符串
        var jsonobj = eval(JSON.parse(evn.data));
	    //往頁面插入消息
        var msg = document.createElement("h4");
        context = jsonobj.username+'&nbsp;&nbsp;'+getDate(jsonobj.time)+'<br />'+jsonobj.sendarea
        msg.innerHTML=context;
        var showareadiv = document.getElementById("showarea");
        showareadiv.appendChild(msg);
    };

    /**
    *ws.onclose方法,當窗口關閉,會話結束時觸發
    */
    ws.onclose = function(){
        onsole.log("關閉連接");
    };

    /*日期轉換*/
    function getDate(time){
        var date = new Date(time);
        Year = date.getFullYear();
        Month = date.getMonth();
        Day = date.getDay();
        time = Year+"-"+getZero(Month)+"-"+getZero(Month);
        return time;
    }
    /*日期補零*/
    function getZero(num){

        if(parseInt(num) < 10 ){
        num = "0" + num;
        }

        return num;
    }

</script>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <!-- 點擊按鈕,提交文本框內內容 -->
    <button onclick="send()">open</button>
    <input type="text" value="輸入點什么" id="textarea">

    <script type="text/javascript">
        
        var host = window.location.host; //獲取地址
        var url = "ws://"+host+"/websocket/echo";//訪問到后台項目注解所在的類
        var ws = new WebSocket(url);//new一個新ws對象,new完即新建立一條“管道”
        
        //當連接建立成功后觸發
        ws.onopen=function(){
            onsole.log("連接成功!");
        };
        //當窗口關閉,會話結束時觸發
        ws.onclose = function(){
            onsole.log("關閉連接");
        };
        //當后台接受發送信息時觸發
        ws.onmessage = function(evn){
            alert(evn.data);
        };
        
		//當點擊按鈕時提交信息
        function send() {
            var msg = document.getElementById("textarea").value;
            ws.send(msg);
        }


    </script>

</body>
</html>

3.添加依賴

<dependencies>
    <!--使用自帶的tomcat-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <scope>provided</scope>
    </dependency>

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.58</version>
    </dependency>
</dependencies>

4.Controller

package com.huang.socketserver;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.RestController;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

/**
 * Author:huang
 * Date:2019-08-02 22:35
 * Description:<描述>
 */
//
@ServerEndpoint("/chat")
@RestController
public class ChatSocket {

    //定義一個全局變量集合sockets,用戶存放每個登錄用戶的通信管道
    private static Set<ChatSocket> sockets = new HashSet<ChatSocket>();
    //定義一個全局變量Session,用於存放登錄用戶
    private Session session;

    /**
    *@OnOpen注解
    *注解下的方法會在連接建立時運行
    */
    @OnOpen
    public void open(Session session){
        System.out.println("建立了一個socket通道" + session.getId());
        this.session = session;
        //將當前連接上的用戶session信息全部存到scokets中
        sockets.add(this);
    }

    /**
    *@OnMessage注解
    *注解下的方法會在前台傳來消息時觸發
    */
    @OnMessage
    public void getmes(Session session,String jsonmsg){

        broadcast(sockets,jsonmsg);

    }

    /**
    *@OnClose注解
    *注解下的方法會在連接關閉時運行
    */
    @OnClose
    public void close(Session session){
        //移除退出登錄用戶的通信管道
        sockets.remove(this);
        System.out.println(session.getId()+"退出了會話!");

    }


    /**
    *廣播消息
    */
    public void broadcast(Set<ChatSocket> sockets , String msg){
        //遍歷當前所有的連接管道,將通知信息發送給每一個管道
        for(ChatSocket socket : sockets){
            try {
                //通過session發送信息
                System.out.println("發送給管道"+socket.session.getId());
                socket.session.getBasicRemote().sendText(msg);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

package com.huang.websocket;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;

@ServerEndpoint(value = "/echo")
public class echoSocket {

    public echoSocket() {
        System.out.println("新對象!");
    }

    /**
    *@OnOpen注解
    *注解下的方法會在連接建立時運行
    */
    @OnOpen
    public void open(Session session){
        //一個session代表一個通信會話
        //通過sessionid區分每一個會話
        System.out.println("連接成功");
        System.out.println(session.getId());
    }

    /**
    *@OnClose注解
    *注解下的方法會在連接關閉時運行
    */
    @OnClose
    public void close(Session session){
        System.out.println("關閉"+session.getId());
    }

    /**
    *@OnMessage注解
    *注解下的方法會在前台傳來消息是觸發
    */
    @OnMessage
    public void getmes(Session session,String msg){
        System.out.println("客戶端:"+msg);
        
        try {
            //向請求服務器的會話發送消息
            session.getBasicRemote().sendText("服務器:你發來的消息是-"+msg);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

五、運行效果

搞定!

=====================================

參考:https://www.cnblogs.com/ricklz/p/11108320.html

參考:https://www.cnblogs.com/Javi/p/9303020.html

=====================================


免責聲明!

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



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