WebSocket技术讲解

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连接异常")
        }
    }
    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值