WebSocket(一)

news2025/1/29 14:05:10

一.什么是WebSocket

【1】WebSocket是一种协议,设计用于提供低延迟,全双工和长期运行的连接。

全双工:通信的两个参与方可以同时发送和接收数据,不需要等待对方的响应或传输完成。

【2】比较

传统通信(http协议):电子邮件,网页游览,存在延迟,需要用户主动请求来更新数据。
实时通信(websocket协议):即时消息传递,音视频通话,在线会议和实时数据传输等,可以实现即时的数据传输和交流,不需要用户主动请求或刷新来获取更新数据。

【3】WebSocket之前的世界(基于http):
(1)轮询:客户端定期向服务器发送请求
缺点--会产生大量的请求和响应,导致不必要的网络开销和延迟。
(2)长轮询:在客户端发出请求后,保持连接打开,等待新数据相应后再关闭连接。
缺点--虽然消灭了了无效轮询,但是还是需要频繁的建立和关闭连接。
(3)Comet:保持长连接,在返回请求后继续保持连接打开,并允许服务器通过流式传输,frame等推送技术来主动向客户端推送数据。
缺点--虽然模拟了事实通信,但还是基于http模型,使用推送技巧来实现的。

【4】那么怎么建立websocket连接呢?

需要通过HTTP发送一次常规的Get请求,并在请求头中带上Upgrade,告诉服务器,我想从HTTP升级成WebSocket,连接就建立成功了,之后客户端就可以像服务器发送信息。

二.入门案例

1.先搭建配置和程序的基本结构

【1】导入依赖(websocket和fastjson)

springboot集成websocket,因为springboot项目都继承父项目,所以不用写依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
在websocket中不能直接像controller那样直接将对象return,所以需要fastjson之类的工具将对象转化为json字符串再返回给前端。
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.62</version>
</dependency>

【2】开启springboot对websocket的支持

@Configuration
public class WebSocketConfig {
    @Bean
    //注入ServerEndpointExporter,自动注册使用@ServerEndpoint注解的
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

【3】定义EndPoint类实现(一个websocket的链接对应一个Endpoint类)

/**
 * @ServerEndpoint 注解的作用
 *
 * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
 * 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
 */
  
@Slf4j
@Component
@ServerEndpoint("/websocket/{name}")
public class WebSocket {
  
  /**
   * 与某个客户端的连接对话,需要通过它来给客户端发送消息
   */
  private Session session;
  
  /**
   * 标识当前连接客户端的用户名
   */
  private String name;
  
  /**
   * 用于存储每一个客户端对象对应的WebSocket对象,因为它是属于类的,所以要用static
   */
  private static ConcurrentHashMap<String,WebSocket> webSocketSet = new ConcurrentHashMap<>();
  
  //下面有三个生命周期
  //生命周期一:连接建立成功调用的方法
  //注意:这个方法的参数列表中只能使用@PathParam结束路径参数,而且必须使用至少一个@PathParam接收路径参数
  @OnOpen
  public void OnOpen(Session session, @PathParam(value = "name") String name){
    log.info("----------------------------------");
    this.session = session;
    this.name = name;
    // name是用来表示唯一客户端,如果需要指定发送,需要指定发送通过name来区分
    webSocketSet.put(name,this);
    log.info("[WebSocket] 连接成功,当前连接人数为:={}",webSocketSet.size());
    log.info("----------------------------------");
    log.info("");
    GroupSending(name+" 来了");
  }
  
  //生命周期二:连接建立关闭调用的方法
  @OnClose
  public void OnClose(){
    webSocketSet.remove(this.name);
    log.info("[WebSocket] 退出成功,当前连接人数为:={}",webSocketSet.size());
  
    GroupSending(name+" 走了");
  }

  //生命周期三:连接建立关闭调用的方法收到客户端消息后调用的方法
  @OnMessage
  public void OnMessage(String message_str){
    //只要某个客户端给服务端发送消息,就给它发送666
    AppointSending(this.name,"666");
  }
  
  //生命周期四:发生错误后调用的方法
  @OnError
  public void onError(Session session, Throwable error){
    log.info("发生错误");
    error.printStackTrace();
  }
  
  /**
   * 群发
   * @param message
   */
  public void GroupSending(String message){
    for (String name : webSocketSet.keySet()){
      try {
        webSocketSet.get(name).session.getBasicRemote().sendText(message);
      }catch (Exception e){
        e.printStackTrace();
      }
    }
  }
  
  /**
   * 指定发送
   * @param name
   * @param message
   */
  public void AppointSending(String name,String message){
    try {
      webSocketSet.get(name).session.getBasicRemote().sendText(message);
    }catch (Exception e){
      e.printStackTrace();
    }
  }
}

简单说就是每有一个客户端连接这个websocket,就会给生成一个websocket对象,并调用OnOpen方法打印日志和存储用户信息。然后连接保持,中间如果这个连接出错,调用OnError方法,如果客户端给服务端发送信息,就调用OnMessage,直到连接关闭,才调用OnClose方法。

然后服务端给客户端发送消息是通过你定义的EndPoint类的session.getBasicRemote().sendText(message)方法,如果你要返回json对象要用fastjson之类进行转换成json格式的字符串。

2.案例一:如果数据库对应的数据改变就向前端发送新的数据信息,解决长轮询的问题

场景:签到页面显示对应的签到信息,要根据信息的改变,如课程的签到状态,需要签到什么课程等信息的改变来在客户端实时更新这些信息。使用webSocket代替轮询解决这个问题。

下面是ui界面:

【1】在configure中增加代码,主要是为了通过配置类进而使得websocket获取请求头中的token
参考文章

@Configuration
@Slf4j
public class WebSocketConfig extends ServerEndpointConfig.Configurator {

    // 创建ServerEndpointExporter的Bean,用于自动注册WebSocket端点
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

    /**
     * 建立握手时,连接前的操作
     * 在这个方法中可以修改WebSocket握手时的配置信息,并将一些额外的属性添加到用户属性中
     */
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        // 获取用户属性
        final Map<String, Object> userProperties = sec.getUserProperties();

        // 获取HTTP请求头信息
        Map<String, List<String>> headers = request.getHeaders();

        // 通过"Authorization"键从请求头中获取对应的token,并存储在用户属性中
        List<String> header1 = headers.get("Authorization");
        userProperties.put("Authorization", header1.get(0));
    }

    /**
     * 初始化端点对象,也就是被@ServerEndpoint所标注的对象
     * 在这个方法中可以自定义实例化过程,比如通过Spring容器获取实例
     */
    @Override
    public <T> T getEndpointInstance(Class<T> clazz) throws InstantiationException {
        return super.getEndpointInstance(clazz);
    }

    /**
     * 获取指定会话的指定请求头的值
     *
     * @param session     WebSocket会话对象
     * @param headerName  请求头名称
     * @return 请求头的值
     */
    public static String getHeader(Session session, String headerName) {
        // 从会话的用户属性中获取指定请求头的值
        final String header = (String) session.getUserProperties().get(headerName);

        // 如果请求头的值为空或空白,则记录错误日志,并关闭会话
        if (StrUtil.isBlank(header)) {
            log.error("获取header失败,不安全的链接,即将关闭");
            try {
                session.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return header;
    }
}

【2】编写ServerEndpoint

这里主要有三个技术点:
技术点一:注入mapper或service
这里注入如果按controller的方式直接注入无法注入成功,需要设置成静态变量然后写个方法赋值。
技术点二:获取请求头中的token,通过配置可以使用session.getUserProperties().get("")获取请求头
技术点三:建立线程,每隔一段时间检查数据库并更新客户端的数据

@Slf4j
@Component
@ServerEndpoint(value = "/websocket/studentNowAttendances/{userId}",configurator = WebSocketConfig.class)
public class CourseAttendanceEndPoint {
    //技术点一:注入mapper或service
    //这里注入如果按controller的方式直接注入无法注入成功,需要设置成静态变量然后写个方法赋值。
    private static StudentMapper studentMapper;
    @Autowired
    public void setStudentMapper (StudentMapper studentMapper){
        CourseAttendanceEndPoint.studentMapper = studentMapper;
    }
    private static CourseAttendanceService courseAttendanceService;
    @Autowired
    public void setCourseAttendanceService (CourseAttendanceService courseAttendanceService){
        CourseAttendanceEndPoint.courseAttendanceService = courseAttendanceService;
    }
    private static RedisCache redisCache;
    @Autowired
    private void setRedisCache (RedisCache redisCache){
        CourseAttendanceEndPoint.redisCache = redisCache;
    }
    /**
     * 与某个客户端的连接对话,需要通过它来给客户端发送消息
     */
    private Session session;

    /**
     * 标识当前连接客户端的用userId
     */
    private String studentId;

    /**
     * 用于存所有的连接服务的客户端,这个对象存储是安全的
     * 注意这里的key和value,设计的很巧妙,value刚好是本类对象 (用来存放每个客户端对应的MyWebSocket对象)
     */
    private static ConcurrentHashMap<String, CourseAttendanceEndPoint> webSocketSet = new ConcurrentHashMap<>();
    //线程
    private ScheduledExecutorService scheduler;
    //存储该学生用户获取到的课程信息
    private StudentAttendanceNow lastCourseInfo = new StudentAttendanceNow();


    /**
     * 群发
     * @param message
     */
    public void groupSending(String message){
        for (String name : webSocketSet.keySet()){
            try {
                webSocketSet.get(name).session.getBasicRemote().sendText(message);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    /**
     * 指定发送
     * @param studentId
     * @param message
     */
    public void appointSending(String studentId,String message){
        System.out.println(webSocketSet.get(studentId).session);
        try {
            webSocketSet.get(studentId).session.getBasicRemote().sendText(message);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    /**
     * 连接建立成功调用的方法
     * session为与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    @OnOpen
    public void OnOpen(Session session, EndpointConfig config,@PathParam("userId") String userId){
        log.info("----------------------------------");
        //技术点二:获取请求头中的token,通过配置可以使用session.getUserProperties().get("")获取请求头
        // 获取用户属性
        //获取HttpSession对象
        final String Authorization = (String) session.getUserProperties().get("Authorization");
        System.out.println(Authorization);
        
        Claims claims= JwtUtil.parseJwt(Authorization);
        userId=claims.getSubject();
        QueryWrapper<Student> studentQueryWrapper=new QueryWrapper<Student>();
        studentQueryWrapper.eq("userid",userId);
        Student student=studentMapper.selectOne(studentQueryWrapper);
        //为这个websocket的studentId和session变量赋值,因为后面还要用到
        studentId=student.getId().toString();// studentId是用来表示唯一客户端,如果需要指定发送,需要指定发送通过studentId来区分
        this.session=session;//这句话一定要写,不然后面就会报错session为空
        
        //将studentId及其对应的websocket实例保存在属于类的webSocketSet中,便于后续发送信息时候可以知道要发送给哪个实例
        webSocketSet.put(studentId,this);
        //打印websocket信息
        log.info("[WebSocket] 连接成功,当前连接人数为:={}",webSocketSet.size());
        log.info("----------------------------------");
        log.info("");
        //技术点三:建立线程,每隔一段时间检查数据库并更新客户端的数据
        //建立线程,每个一段时间检查客户端对应在websocket中的数据有没有改变,有改变将信息重新发给客户端
        scheduler = Executors.newScheduledThreadPool(1);
        scheduler.scheduleAtFixedRate(this::checkDatabaseAndUpdateClients, 0, 5, TimeUnit.SECONDS);
    }

    //每个一段时间检查客户端对应在websocket中的数据有没有改变,有改变将信息重新发给客户端
    private void checkDatabaseAndUpdateClients() {
        // 模拟查询最新的课程信息
        // 假设从数据库或其他数据源中查询得到最新的课程信息
        System.out.println("666");
        try{
            //获取当前的信息
            StudentAttendanceNow currentCourseInfo = courseAttendanceService.getStudentAttendanceNow(studentId);
            //与之前的信息进行比较,如果说当前数据与上次存储的数据相比发生了改变,那就替换掉原来的数据并把新的数据发送给客户端
            if(lastCourseInfo.equals(currentCourseInfo)==false){
                lastCourseInfo=currentCourseInfo;
                System.out.println(studentId);
                appointSending(studentId, JSON.toJSONString(currentCourseInfo));
            }
        }catch (Exception e){
            //防止没有查询到数据等异常
            System.out.println(e);
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void OnClose(){
        //退出时将用户从记录中删除,并在log中打印退出信息
        webSocketSet.remove(this.studentId);
        log.info("[WebSocket] 退出成功,当前连接人数为:={}",webSocketSet.size());
        // 关闭定时任务
        scheduler.shutdown();
    }

    /**
     * 收到客户端消息后调用的方法
     */
    @OnMessage
    public void OnMessage(Session session,String message){
    }
    /**
     * 发生错误时调用
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error){
        log.info("发生错误");
        error.printStackTrace();
    }



}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/965149.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

QML与C++的交互操作

QML旨在通过C 代码轻松扩展。Qt QML模块中的类使QML对象能够从C 加载和操作&#xff0c;QML引擎与Qt元对象系统集成的本质使得C 功能可以直接从QML调用。这允许开发混合应用程序&#xff0c;这些应用程序是通过混合使用QML&#xff0c;JavaScript和C 代码实现的。除了从QML访问…

15年检测生涯转瞬即逝,复旦MBA助力邢国芒实现质量强国梦

日月光华&#xff0c;旦复旦兮&#xff01;复旦MBA如同一个巨大的磁场&#xff0c;吸引了诸多来自五湖四海、各行各业的职场精英。从初入职场的青涩懵懂到如今的独当一面专业干练&#xff0c;他们逐渐成长为职场的中坚力量&#xff0c;在各自领域内发光发热。作为新时代的青年&…

多线程应用——阻塞队列

阻塞队列 文章目录 阻塞队列1.队列的概念2.阻塞队列3.现实中的例子4.消息队列5.使用队列的优势1.解耦2.削峰填谷3.异步操作 6.实现 1.队列的概念 一种先进先出的数据结构 2.阻塞队列 队列写元素是从队尾插入&#xff0c;从对头取出 当插入元素时&#xff0c;先判断一下队列…

数据治理-数据管理框架

DMBOK2提出的想法和概念在不同的组织中都可以应用&#xff0c;组织所采用的数据管理方法取决于某些关键要素&#xff0c;如其所处行业、所应用的数据范围、企业文化、成熟度、战略、愿景以及待解决的问题和挑战。 战略一致性模型和阿姆斯特丹模型&#xff0c;展示了组织管理数…

算法通关村第十二关——字符串反转问题解析

前言 字符串反转是关于字符串算法里的重要问题&#xff0c;虽然不是太难&#xff0c;但需要考虑到一些边界问题。本篇文章就对几道字符串反转题目进行分析。 1.反转字符串 力扣344题&#xff0c;编写一个函数&#xff0c;其作用是将输入的字符串反转过来。输入字符串以字符数…

opencv 提取选中区域内指定hsv颜色的水印

基于《QT 插件化图像算法研究平台》做的功能插件。提取选中区域内指定hsv颜色的水印。 《QT 插件化图像算法研究平台》有个HSV COLOR PICK功能&#xff0c;可以很直观、方便地分析出水印 的hsv颜色&#xff0c;比如, 蓝色&#xff1a;100,180,0,255,100,255。 然后利用 opencv …

JavaScript中关于数组的小挑战

史蒂芬仍在建立他的小费计算器&#xff0c;使用的规则与以前一样&#xff1a; 如果账单价值在50到300之间&#xff0c;小费为账单的15%&#xff0c;如果价值不同&#xff0c;小费为20%。 编写一个函数’calcTip’&#xff0c;将任何账单值作为输入&#xff0c;并返回相应的小费…

【业务功能篇94】微服务-springcloud-springboot-认证服务-注册功能-第三方短信验证API

商城认证服务 一、搭建认证服务环境 结合我们前面介绍的商城的架构我们需要单独的搭建一个认证服务。 1.创建项目 首先创建一个SpringBoot项目&#xff0c;然后添加对应的依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"htt…

MySQL主从复制与读写分离 及其实例

目录 主从复制与读写分离 1、MySQL主从复制原理 1.1、MySQL的复制类型 1.2、MySQL主从复制的工作过程 1.3、mysq支持的复制类型 1.4、 数据流向 1.5、主从复制的工作过程 2、读写分离 2.1、什么是读写分离&#xff1f; 2.2、为什么要读写分离呢&#xff1f; 2.3、什么…

HFSS 3维曲线导入

HFSS 3维曲线导入 简介环境参考代码使用结果 简介 如图一所示&#xff0c;CST中可以通过导入和到出由任意点组成的曲线&#xff0c;但是HFSS中貌似不能导入&#xff08;如图二所示&#xff09;&#xff0c;如果我们要将matlab的产生的曲线的点的数据导入特变麻烦&#xff0c;特…

21.4 CSS 盒子模型

1. 边框样式 border-style属性: 指定元素的边框样式.常用属性值: - none: 无边框(默认值). - solid: 实线边框. - dotted: 点状边框. - dashed: 虚线边框. - double: 双线边框. - groove: 凹槽状边框. - ridge: 脊状边框. - inset: 内阴影边框. - outset: 外阴影边框.这些值可…

Loki日志系统

1、Loki是什么&#xff1f; Loki是一个开源的日志聚合系统&#xff0c;由Grafana Labs开发和维护。它旨在帮助用户收集、存储和查询大规模的日志数据&#xff0c;帮助用户更好地理解和监控他们的应用程序和系统。 Loki的设计灵感来自于Prometheus&#xff0c;它采用了类似的标…

【ROS 03】ROS通信机制进阶

上一章内容&#xff0c;主要介绍了ROS通信的实现&#xff0c;内容偏向于粗粒度的通信框架的讲解&#xff0c;没有详细介绍涉及的API&#xff0c;也没有封装代码&#xff0c;鉴于此&#xff0c;本章主要内容如下: ROS常用API介绍&#xff1b;ROS中自定义头文件与源文件的使用。…

springboot 与异步任务,定时任务,邮件任务

异步任务 在Java应用中&#xff0c;绝大多数情况下都是通过同步的方式来实现交互处理的&#xff1b;但是在处理与第三方系统交互的时候&#xff0c;容易造成响应迟缓的情况&#xff0c;之前大部分都是使用多线程来完成此类任务&#xff0c;其实&#xff0c;在Spring 3.x之后&a…

2023开学礼《乡村振兴战略下传统村落文化旅游设计》许少辉八一新书浙江师范大学图书馆

2023开学礼《乡村振兴战略下传统村落文化旅游设计》许少辉八一新书浙江师范大学图书馆

【GUI开发】用python爬YouTube博主信息,并开发成exe软件

文章目录 一、背景介绍二、代码讲解2.1 爬虫2.2 tkinter界面2.3 存日志 三、软件演示视频四、说明 一、背景介绍 你好&#xff0c;我是马哥python说&#xff0c;一名10年程序猿。 最近我用python开发了一个GUI桌面软件&#xff0c;目的是爬取相关YouTube博主的各种信息&#…

算法:图解位运算以及鸽巢原理应用

文章目录 实现原理基础位运算位图思想找最右侧数按位异或 算法思路典型例题基础位运算只出现一次的数字只出现一次的数字III 经典题型判断字符是否唯一两整数之和只出现一次的数字II消失的两个数字 鸽巢原理总结 本篇总结位运算中常见的算法题和思路&#xff0c;首先总结位运算…

边写代码边学习之TF Attention

1. 什么是Attention 注意力机制&#xff08;Attention Mechanism&#xff09;是机器学习和人工智能领域中的一个重要概念&#xff0c;用于模拟人类视觉或听觉等感知过程中的关注机制。注意力机制的目标是让模型能够在处理信息时&#xff0c;更加关注与任务相关的部分&#xff…

TDengine(2):wsl2+ubuntu20.04+TDengine安装

一、ubuntu系统下提供了三种安装TDengine的方式&#xff1a; 二、通过 apt 指令安装失败 因为是linux初学者&#xff0c;对apt 指令较为熟悉&#xff0c;因此首先使用了该方式进行安装。 wget -qO - http://repos.taosdata.com/tdengine.key | sudo apt-key add -echo "…

windows使用-设置windows的远程访问用户数量

文章目录 前言相关操作总结前言 作为IT工程师,使用服务器做相应的软件操作时常有的事。最近一段时间,我们的团队多个成员都需要远程登录到一台windows2003Server的服务器处理相应的业务。而默认情况下,Windows系统只允许一名用户远程到服务器上,这给小伙伴的工作造成一些不…