天天看点

搭建实时聊天系统之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技术的讲解就到这里了。

感谢您!花费这么多宝贵的时间观看!

继续阅读