天天看點

搭建實時聊天系統之WebSocket技術講解

作者:名字就叫小明同學

一、簡介

1. 什麼是websocket?

WebSocket協定是基于TCP的一種新的網絡協定。實作了浏覽器和伺服器的全雙工(full-duplex)通信。允許伺服器主動發送資訊給用戶端。

WebSocket使得用戶端和伺服器之間的資料交換變得更加簡單,允許服務端主動向用戶端推送資料。在WebSocket API中,浏覽器和伺服器隻需要完成一次握手,兩者之間就直接可以建立持久性的連接配接,并進行雙向資料傳輸。

注意:Websocket 通過HTTP/1.1 協定的101狀态碼進行握手。

2. 為什麼需要websocket?

  • 全雙工 服務端可以主動向用戶端推送資訊,用戶端也可以主動向服務端發送資訊
  • 與HTTP共享端口,基于HTTP完成握手
  • 資料傳輸基于幀,支援發送文本類型資料和二進制資料
  • 沒有同源限制,用戶端可以與任意服務端通信
  • 支援協定辨別(ws或wss)與尋址,通過URL指定服務

3.适用場景有哪些?

  • 社交訂閱
  • 多玩家遊戲
  • 協同編輯/程式設計
  • 收集點選流資料
  • 股票基金報價
  • 體育實況更新
  • 聊天室
  • 基于位置的應用
  • 線上教育
  • 論壇的消息廣播
  • 視訊彈幕

二、原理講解

首先,WebSocket 是一個持久化的協定,是相對于 HTTP 這種非持久的協定來說的。HTTP 的生命周期通過 Request 來界定,也就是一個 Request 一個 Response ,那麼在 HTTP1.0 中,這次 HTTP 請求就結束了。

在 HTTP1.1 中進行了改進,使得有一個 keep-alive,也就是說,在一個 HTTP 連接配接中,可以發送多個 Request,接收多個 Response。但是請記住 Request = Response, 在 HTTP 中永遠是這樣,也就是說一個 Request 隻能有一個 Response。而且這個 Response 也是被動的,不能主動發起。

三、前端websocket類的介紹(目前浏覽器基本上都已經内置該類)

1. websocket的屬性:

  1. webSocket.onopen:用于指定連接配接成功後的回調函數。
  2. webSocket.onmessage:用于從伺服器接收到資訊時的回調函數。
  3. webSocket.onerror:用于指定連接配接失敗後的回調函數。
  4. webSocket.onclose:用于指定連接配接關閉後的回調函數。
  5. webSocket.binaryType: 使用二進制的資料類型連接配接。
  6. webSocket.url :WebSocket 的絕對路徑(隻讀)
  7. webSocket.protocol:伺服器選擇的下屬協定(隻讀)
  8. webSocket.bufferedAmount: 未發送至伺服器的位元組數。(隻讀)
  9. webSocket.readyState : 執行個體對象的目前狀态, 共有 4 種(隻讀)

2. websocket 方法:

  1. webSocket.close([code[, reason]]) :關閉目前連結,
  2. code: 可選,一個數字狀态碼,它解釋了連接配接關閉的原因。如果沒有傳這個參數,預設使用 1005,抛出異常:INVALID_ACCESS_ERR,無效的 code.
  3. reason 可選,一個人類可讀的字元串,它解釋了連接配接關閉的原因。這個 UTF-8 編碼的字元串不能超過 123 個位元組,抛出異常:SYNTAX_ERR,超出 123個位元組。
  4. webSocket.send(data) :發送資料到伺服器。

3. websocket 事件:

  1. open:連接配接成功時觸發。 也可以通過 onopen 屬性來設定。
  2. message:收到資料時觸發。 也可以通過 onmessage 屬性來設定。
  3. error:連接配接因錯誤而關閉時觸發,例如無法發送資料時。 也可以通過 onerror 屬性來設定。
  4. close:連接配接被關閉時觸發。 也可以通過 onclose 屬性來設定。

四、基礎部分

本項目基于SpringBoot架構實作,模闆引擎采用freemarker

1.maven依賴

<!-- webSocket架構 -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-websocket</artifactId>
         </dependency>           

2.服務端的代碼

2.1 WebSocketConfig類

該配置類,隻有在SpringBoot環境下需要配置。将該類放在config包下。

package org.example.ssm.config;
 
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.socket.server.standard.ServerEndpointExporter;
 
 /**
  * websocket配置類
  * 配置類,讓用戶端能連接配接上,隻是springboot環境下才需要
  */
 @Configuration
 public class WebSocketConfig {
     @Bean
     public ServerEndpointExporter serverEndpointExporter() {
         return new ServerEndpointExporter();
     }
 }
            

2.2 MyWebSocket類

該類是websocket服務端的核心類,主要是對websocket的onopen,onmessage,onclose,onerror事件進行重寫處理。建議放在ws包下。

package com.chinasoft.websocket.ws;
 
 
 import org.springframework.stereotype.Component;
 
 import javax.websocket.*;
 import javax.websocket.server.ServerEndpoint;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 @ServerEndpoint("/websocket")
 @Component
 public class MyWebSocket {
     //記錄目前線上連接配接數
     public static int onlineCount=0;
 
     // concurrent包的線程安全Set,用來存放每個用戶端對應的MyWebSocket對象。
     public static CopyOnWriteArrayList<MyWebSocket> user=new CopyOnWriteArrayList<MyWebSocket>();
 
     // 與某個用戶端的連接配接會話,需要通過它來給用戶端發送資料,這是websocket專屬的session不是HttpSession
     public Session session;
 
     //打開連接配接
     @OnOpen
     public void onOpen(Session session) throws  Exception{
         this.session=session;
         user.add(this);
         String id=session.getId();
         System.out.println("使用者名:"+id+"打開了連接配接~~~~");
         session.getBasicRemote().sendText("data:"+id+"使用者連接配接");
     }
 
     //關閉連接配接
     @OnClose
     public void onClose(Session session) throws  Exception{
         user.remove(this);
         System.out.println("使用者名:"+session.getId()+"關閉了連接配接~~~~");
     }
 
     //收到用戶端消息後調用的方法(接受方法)
     @OnMessage
     public void onMessage(String message,Session session) throws  Exception{
         System.out.println("sessionMessage:"+session);
         System.out.println("接受前端發送回來的消息"+message);
         for(int i=0;i<user.size();i++){
             MyWebSocket d=user.get(i);
             d.session.getBasicRemote().sendText(session.getId());
         }
         System.out.println("我群發了消息:hello");
         for(int i=0;i<user.size();i++){
             MyWebSocket d=user.get(i);
             d.session.getBasicRemote().sendText("datas:"+session.getId()+":"+message);
         }
     }
 
     @OnError
     public void onError(Session session,Throwable error){
         System.out.println("發生error");
         error.printStackTrace();
     }
 
 
     //給用戶端傳遞消息
     public void sendMessage(String message){
         try{
             this.session.getBasicRemote().sendText("message");
         }catch (Exception e){
             e.printStackTrace();
         }
     }
 
 
 }
            

2.3 WebSocketController類

該類主要是處理index相關的請求

import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
 
 @Controller
 public class WebSocketController {
 
     @GetMapping("index")
     public String index(){
         return "index";
     }
 }
            

3.前端代碼

index.ftl

<!DOCTYPE html>
 <html lang="en">
 <head>
     <meta charset="UTF-8">
     <title>Title</title>
     <script>
         let ws;//建立webSocker執行個體
         let lockReconnect=false;//避免重複連接配接
         let wsUrl="ws://localhost:8888/ssm/websocket";
 
         function getConn(){
             createWebSocket(wsUrl);
         }
 
         //建立WebSocket對象
         function createWebSocket(url){
             //給個辨別
             console.log('建立WebSocket對象');
             try{
                 ws=new WebSocket(url);
                 initEventHandle();//初始化事件的方法
             }catch(e){
                 console.log('建立WebSocket對象出錯了');
                 reconnect(url);
                 console.log('WebSocket重新連接配接');
                 //給個錯誤辨別
                 //然後重連
             }
         }
 
         //關閉連接配接
         function closeWebSocket(){
             //給個辨別關閉連接配接
             console.log('關閉WebSocket對象出錯了');
                ws.close();
         }
 
         //初始化WebSocket的事件
         function initEventHandle(){
             ws.onclose=function(){
                 console.log("連接配接關閉--重連");
                 reconnect(wsUrl);
             }
             ws.onerror=function(){
                 console.log("連接配接異常--重連");
                 reconnect(wsUrl);
             }
             ws.onopen=function(){
                 console.log("開啟連接配接");
                 heartCheck.reset().start();
             }
             ws.onmessage=function(event){
                 console.log("事件:"+event.data);
                 document.getElementById("msg").innerHTML = event.data;
                 heartCheck.reset().start();
             }
         }
 
         function reconnect(url){
             console.log('正在重連');
             if(lockReconnect)return;
             lockReconnect=true;
             //沒連接配接上會一直重連,設定延遲避免請求過多
             setTimeout(function(){
                 createWebSocket(url);
                 lockReconnect=false;
             },2000);
 
         }
 
         let heartCheck={
             timeout: 6000,//6秒
             timeoutObj: null,
             reset: function(){
                 console.log('接收到消息,檢測正常');
                 clearTimeout(this.timeoutObj);
                 return this;
             },
             start: function(){
                 this.timeoutObj=setTimeout(function(){
 
                 // ws.send('HeartBeat');
                 // console.log('發送一個心跳');
                 },this.timeout);
             }
         }
 
         function sendInfo(){
             ws.send(document.getElementById("message").value.trim());
         }
     </script>
 </head>
 <body>
 <p>第一步:取得連接配接:<input type="button" value="點選我建立連接配接" onclick="getConn();"></p>
 <p>第二布:輸入内容:<input type="text" id="message"><input type="button" value="群發" onclick="sendInfo();"></p>
 <p id="msg"></p>
 
 </body>
 </html>           

五、進階部分

視訊彈幕項目的簡單實作

1.maven依賴

<?xml version="1.0" encoding="UTF-8"?>
 <project xmlns="http://maven.apache.org/POM/4.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
 
     <groupId>com.chinasoft</groupId>
     <artifactId>websocket</artifactId>
     <version>1.0-SNAPSHOT</version>
     <!--所有的springboot項目都需要繼承父類-->
     <parent>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-parent</artifactId>
         <version>2.0.6.RELEASE</version>
     </parent>
     <dependencies>
         <!--web項目的啟動器-->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
         <!--freemarker前端頁面-->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-freemarker</artifactId>
         </dependency>
         <!--引入WebSocket pom依賴-->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-websocket</artifactId>
         </dependency>
     </dependencies>
 </project>           

2.服務端的代碼

2.1 WebSocketConfig類

該配置類,隻有在SpringBoot環境下需要配置。将該類放在config包下。

package org.example.ssm.config;
 
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.socket.server.standard.ServerEndpointExporter;
 
 /**
  * websocket配置類
  * 配置類,讓用戶端能連接配接上,隻是springboot環境下才需要
  */
 @Configuration
 public class WebSocketConfig {
     @Bean
     public ServerEndpointExporter serverEndpointExporter() {
         return new ServerEndpointExporter();
     }
 }
            

2.2 WebMvcConfig類

該類是SpringMVC的配置類,主要是對資源檔案進行配置。采用外部資源導入項目中,外部資源存放位置根據個人情況修改。将該類放在config包下

package org.example.ssm.config;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
 
 /**
  * 外部資源配置類
  */
 @Configuration
 public class WebMvcConfig extends WebMvcConfigurationSupport {
     @Override
     public void addResourceHandlers(ResourceHandlerRegistry registry) {
  registry.addResourceHandler("/css/**").addResourceLocations("file:E:/static/css/");
         registry.addResourceHandler("/js/**").addResourceLocations("file:E:/static/js/");
       
     }
 }
            

2.3 WebSocketController類

該類主要是處理index相關的請求

import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
 
 @Controller
 public class WebSocketController {
 
     @GetMapping("index")
     public String index(){
         return "index";
     }
 }
            

2.4 ChatWebSocket類

該類是websocket服務端的核心類,主要是對websocket的onopen,onmessage,onclose,onerror事件進行重寫處理。建議放在ws包下

import com.alibaba.fastjson.JSONObject;
 import org.example.ssm.entity.VO.MessageVO;
 import org.springframework.stereotype.Component;
 
 import javax.websocket.*;
 import javax.websocket.server.ServerEndpoint;
 import java.io.IOException;
 import java.util.concurrent.CopyOnWriteArraySet;
 
 /**
  * 這是伺服器端的websocket,接收來自用戶端的websocket請求
  * 相當于一個controller
  * 聊天室的實作
  * 單人聊天業務實作:
  * 1.擷取到加入聊天的使用者資訊
  * 2.會話内容采用浏覽器本地存儲,存在sessionStrage裡面
  */
 @ServerEndpoint("/chat")
 @Component
 public class ChatWebSocket {
     // 靜态變量,用來記錄目前線上連接配接數。應該把它設計成線程安全的。
     private static int onlineCount = 0;
 
     // concurrent包的線程安全Set,用來存放每個用戶端對應的MyWebSocket對象。
     private static CopyOnWriteArraySet<ChatWebSocket> webSocketSet = new CopyOnWriteArraySet<ChatWebSocket>();
 
     // 與某個用戶端的連接配接會話,需要通過它來給用戶端發送資料,這是websocket專屬的session不是HttpSession
     private Session session;
 
 
     /**
      * 連接配接建立成功調用的方法
      */
     @OnOpen
     public void onOpen(Session session) {
         this.session = session;
         webSocketSet.add(this);     //加入set中
         addOnlineCount();           //線上數加1
         System.out.println("有新連接配接加入!目前線上人數為" + getOnlineCount());
         try {
             // 連接配接預處理
 
 
         } catch (Exception e) {
             e.getCause();
         }
     }
 
     /**
      * 連接配接關閉調用的方法
      */
     @OnClose
     public void onClose() {
         webSocketSet.remove(this);  //從set中删除
         subOnlineCount();           //線上數減1
         System.out.println("有一連接配接關閉!目前線上人數為" + getOnlineCount());
     }
 
     /**
      * 收到用戶端消息後調用的方法
      * @param message 用戶端發送過來的消息
      */
     @OnMessage
     public void onMessage(String message,Session session) {
         JSONObject jsonObject = JSONObject.parseObject(message);
 
         // 群發消息
         for (ChatWebSocket item : webSocketSet) {
             try {
                 item.sendMessage(jsonObject.toJSONString());
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
     }
 
 
     @OnError
     public void onError(Throwable error) {
         System.out.println("發生錯誤");
         error.printStackTrace();
     }
 
     public void sendMessage(String message) throws IOException {
         this.session.getBasicRemote().sendText(message);
     }
 
     public static void sendInfo(String message){
         for (ChatWebSocket item : webSocketSet) {
             try {
                 item.sendMessage(message);
             } catch (IOException e) {
                 continue;
             }
         }
     }
 
     public static synchronized int getOnlineCount() {
         return onlineCount;
     }
 
     public static synchronized void addOnlineCount() {
         ChatWebSocket.onlineCount++;
     }
 
     public static synchronized void subOnlineCount() {
         ChatWebSocket.onlineCount--;
     }
 }
 
 
            

3.前端代碼

3.1 index.ftl

視訊的路徑自行選擇本地視訊

注意:需要自行下載下傳bootstrap4前端架構。官網:Bootstrap中文網 (bootcss.com)

<!DOCTYPE html>
 <html lang="en">
 <head>
     <meta charset="UTF-8">
     <title>視訊彈幕</title>
     <link href="css/bootstrap.css" rel="stylesheet">
     <script src="js/jquery-3.6.0.min.js"></script>
     <style>
         .bullet {
             width: 800px;
             height: 550px;
             padding: 15px;
             margin: 50px auto;
             border-radius: 10px;
             box-shadow: 3px 4px 5px 6px grey;
         }
         .input-group{
             margin-top: 10px;
         }
 
     </style>
 </head>
 <body>
 <div class="container">
     <div class="row">
         <div class="col">
             <#-- 視訊彈幕,固定擷取視訊 -->
             <div class="bullet">
                 <div class="video">
                     <video src="../video/test.mp4" width="100%" height="90%" controls></video>
                 </div>
                 <div class="input-group">
                     <input type="text" class="form-control"  placeholder="你也來發個彈幕呗!!!" id="canve">
                     <div class="input-group-append">
                         <button class="btn btn-outline-success" type="button"  onclick="sendBullet($('#canve').val());">點選我發送</button>
                     </div>
                 </div>
             </div>
         </div>
     </div>
 </div>
 <script src="js/websocket.js"></script>
 <script>
     var wsUrl = "ws://localhost:8888/ssm/bullet"
     var ws = createConnect(wsUrl)
     // 開始連接配接,擷取已有的彈幕,進行播放
     ws.onopen = function () {
         console.log("擷取已有的彈幕")
     }
 
     // 接收消息事件
     ws.onmessage = function (e) {
         var str = JSON.parse(e.data)
         console.log(str)
         createEle(str)
     }
 
     // 發送彈幕
     function sendBullet(message) {
         if (message == null || message == "" || message == undefined) {
             alert("請輸入内容" + message)
         } else {
             ws.send(JSON.stringify(message))
         }
     }
 
     //1.擷取元素
     //擷取.box元素
     var bullet = document.querySelector(".bullet")
     //擷取box的寬度
     var width = bullet.offsetWidth
     //擷取box的高度
     var heigth = bullet.offsetHeight * 0.8
 
     // 建立彈幕元素
     function createEle(txt) {
         //動态生成span标簽
         //建立标簽
         var span = document.createElement('span')
         //接收參數txt并且生成替換内容
         span.innerHTML = txt
         //初始化生成位置x
         //動态生成span标簽
         span.style.left = width + 'px'
         span.style.position = "absolute"
         //把标簽塞到oBox裡面
         bullet.appendChild(span)
 
         roll.call(span, {
             // call改變函數内部this的指向
             timing: ['linear'][~~(Math.random() * 2)],
             color: '#' + (~~(Math.random() * (1 << 24))).toString(16),
             top: random(10, heigth),
             fontSize: random(10, 24)
         })
     }
 
     function roll(opt) {
         //彈幕滾動
         //如果對象中不存在timing 初始化
         opt.timing = opt.timing || 'linear'
         opt.color = opt.color || '#fff'
         opt.top = opt.top || 0
         opt.fontSize = opt.fontSize || 16
         this._left = parseInt(this.offsetLeft)  //擷取目前left的值
         this.style.color = opt.color   //初始化顔色
         this.style.top = opt.top + 'px'
         this.style.fontSize = opt.fontSize + 'px'
         this.timer = setInterval(function () {
             if (this._left <= 100) {
                 clearInterval(this.timer)   //終止定時器
                 this.parentNode.removeChild(this)
                 return   //終止函數
             }
             switch (opt.timing) {
                 case 'linear':   //如果勻速
                     this._left += -2
                     break
                 // case 'ease-out':   //
                 //     this._left += (0 - this._left) * .01;
                 //     break;
             }
             this.style.left = this._left + 'px'
         }.bind(this), 1000 / 60)
     }
 
     function random(start, end) {
         //随機數封裝
         return start + ~~(Math.random() * (end - start))
     }
 </script>
 </body>
 </html>
            

3.2 websocket.js封裝代碼

// 定義websocket心跳,避免長時間連接配接加重伺服器的負擔
     var heartCheck = {
         // 心跳間隔1分鐘
         timeout: 60 * 1000,
         // 心跳對象
         timeoutObj: null,
         reset: function () {
             console.log('接收到消息,檢測正常')
             clearTimeout(this.timeoutObj)
             return this
         },
         start: function () {
             console.log("開始心跳")
             this.timeoutObj = setTimeout(function () {
             }, this.timeout)
         }
     }
 
     // 建立websocket連接配接
     function createConnect(wsUrl) {
         // 判斷浏覽器是否支援websocket協定
         if (typeof WebSocket != 'undefined') {
             console.log("您的浏覽器支援Websocket通信協定")
             // 建立websocket的執行個體,建立該執行個體後會立即執行websocket請求
             var webSocket = new WebSocket(wsUrl)
             // 初始化websocket事件
             initEvent(webSocket)
             return webSocket
         } else {
             alert("您的浏覽器不支援Websocket通信協定,請使用Chrome或者Firefox浏覽器!")
             return null
         }
     }
 
     // 初始化websocket事件
     function initEvent(ws) {
         // 開始連接配接事件
         ws.onopen = function () {
             console.log("建立websocket連接配接")
             // 開始心跳,判斷使用者是否線上
             heartCheck.reset().start()
         }
 
         // 接收來自伺服器的消息,傳入event參數。
         // 擷取websocket響應消息,此處業務邏輯需要根據實際情況進行書寫
         ws.onmessage = function (event) {
             console.log("收到來自伺服器的消息:" + event.data)
             // 重置心跳并開始心跳
             heartCheck.reset().start()
         }
 
         // 斷開websocket連接配接前的操作
         ws.onclose = function () {
             console.log("websocket連接配接已斷開")
         }
 
         // websocket連接配接錯誤事件
         ws.onerror = function () {
             console.log("websocket連接配接異常")
         }
     }
     
            

好了,關于websocket技術的講解就到這裡了。

感謝您!花費這麼多寶貴的時間觀看!

繼續閱讀