websocket原理及简单应用

news2024/12/25 9:09:38

websocket是什么?

一般做系统开发前后端交互使用最多的就是http协议,但http协议是无状态协议每一次前端发起的请求都认为是一次单独的请求和之前的请求无任何关系,所以我们需要http协议分别用户信息时,就需要使用cookie、session或者现在常用的token等让后端自己实现用户识别等,如果前后端交互非常频繁每次携带公有信息会占用更多的服务器资源,好处是协议本身不需要做信息保存相对可以减少资源开销;其次前端每请求一次后端返回一次信息且只能由客户端发起请求,没办法后端主动推送数据给前端,数据应答之后http链接结束断开,那么想要及时感知后端数据的变化一般做法是前端定时轮询,后端不管数据是否变化都应答请求,但会出现数据更新到前端延迟的问题并占用服务器资源,另外一种做法是长轮询也就是客户端发起http请求之后服务端收到请求后如果数据发生变化则立即应答数据否则先在服务端保留下这个请求的引用当数据发生变化时或超过hold时间再应答数据(一般服务端持有请求引用的超时(hold)时间要小于http请求的超时时间,否则每次无变化就timeout不是很合理)长轮询同样有数据更新延迟的问题只是比前端轮询延迟相对来说较小,同样占用服务器资源(虽然服务器只是保存的请求的引用,然后线程就可以处理其他请求了,但是也需要分配一个或多个线程来处理这些hold住的请求),当长轮询的请求应答或超时之后本次请求建立的http链接结束断开;其次http是半双工协议(即同一时刻只能由建立链接的一方传输数据,只有一方传输完毕,则另一方才能传输数据)这就意味着数据传输不会很及时,虽然这个延迟是没什么影响的,但如果对数据更新要求非常高,半双工协议就会出现延迟的影响;而基于TCP协议实现的全双工(即同一时刻建立链接的双方都可以发送数据和接收数据)websocket只要客户端和服务端建立连接之后,不主动断开的情况下,链接一直存在双方可以随时发送数据,解决了http协议中服务端无法主动推送数据的问题,或推送不及时的问题。
websocket主要有以下特点:
1、建立在 TCP 协议之上,服务器端实现比较容易
2、与 HTTP 协议有着良好的兼容性。默认端口也是80(ws)和443(wss),并且握手阶段用 HTTP 协议(第一次握手依赖于http,后续数据传输则是通过tcp实现),因此握手时不容易屏蔽,能通过各种HTTP 代理服务器
3、数据格式比较轻量(相对于http协议来说请求头更小),性能开销小,通信高效
4、可以发送文本,也可以发送二进制数据
5、没有同源限制,客户端可以与任意服务器通信
6、客户端服务器建立连接之后会一直保持连接,而http请求一次响应一次就结束连接

websocket协议建立连接请求及应答
在这里插入图片描述

springboot整合websocket服务端

1、引入依赖jar包

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
            <!-- 版本可不写,因为springboot 项目 pom一般指定了 父pom为spring-boot-starter-parent其中有指定spring-boot-starter-websocket的版本-->
            <version>2.3.5.RELEASE</version>
        </dependency>
        <!--下面的两个jar包不是websocket需要的仅仅只是为了使用其中的api方便处理业务导入的-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.12</version>
        </dependency>        

2、定义 WebSocketConfig

package com.test.websocket.websockettest.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * 如果是springboot项目且直接使用springboot的内置容器,才需要ServerEndpointExporter
 * 注入ServerEndpointExporter,作用是ServerEndpointExporter的bean会自动注册使用@ServerEndpoint注解声明的Websocket Endpoint
 * 否则项目可以正常启动但是无法连接上@ServerEndpoint注解定义的websocket地址的服务端
 */


@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }

}

3、定义接收客户端消息参数类

package com.test.websocket.websockettest.dto;

import lombok.Data;

/**
 * 接收客户端消息参数类
 */
@Data
public class WebSocketMessageDTO {

    private Integer userId;

    private String message;

}

4、定义服务端类 WebSocketServer

package com.test.websocket.websockettest.service;


import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONUtil;
import com.test.websocket.websockettest.dto.WebSocketMessageDTO;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @ServerEndpoint 指定当前类为websocket服务器端并指定 spring boot 暴露的 ws 接口路径,和@RequestMapping作用类似,但不是http的接口而是websocket的接口
 * 比如当前类指定的websocket服务端地址是 ws://主机地址:服务端口/websocket/test/{userId} 其中{userId}要替换成当前发起连接的客户端的标记
 * 如 ws://127.0.0.1:8080/websocket/test/1  主机地址:服务端口 相关信息有域名映射也可以使用域名 客户端即使用该地址连接websocket服务端
 * @Component 默认是单例模式,但springboot会为每个websocket连接初始化一个当前类的 bean,所以如果要在当前类中定义变量保存所有的客户端连接就需要静态的变量,
 * 比如这里的concurrentHashMap变量,否则每次建立新的连接实例化新的WebSocketServer 的bean,里面的concurrentHashMap都是全新的,没办法访问到其他的客户端
 */
@Log4j2
@Component
@ServerEndpoint("/websocket/test/{userId}")
public class WebSocketServer {

    /**
     * 当前客户端与服务端的websocket连接
     */
    private Session session;

    /**
     * 保存客户端与服务端的每个连接,当需要给某个或某些客户端推送消息时取出对应的session进行发送信息给客户端,不保存所有客户端的连接则
     * 只能实现当前客户端与服务端的通信,不能将某条消息推送到其他客户端,注意通过获取ConcurrentHashMap的size获得客户端连接数量数据可能是不准确的
     * 因为ConcurrentHashMap只保证了写的线程安全,在写入元素后增加size的时候并没有在sync里面,而且size的统计是基于baseCount和数组每个元素值的累加
     * 设计的目的是为了提升修改size的并发性,因为修改size时是通过cas操作修改,那么当并发量很高时可能出现在增加size时比较耗时,所以设计为baseCount和数组
     * 的形式,并发量不高时就累加baseCount,并发高时再使用一个数组累加每个数组元素值,比如数组长度为2,那么此时进行cas增加size时就有三个锁可以抢占累加值,
     * 那么最终的size也就是由这三个值的和决定的,所以在获取size的时候可能出现put了元素,但是size还未增加上,所以获取的size可能是不准确的
     */
    public static ConcurrentHashMap<Integer,WebSocketServer> concurrentHashMap = new ConcurrentHashMap();

    /**
     * 统计当前连接的客户端数量
     */
    private static AtomicInteger onlineCount=new AtomicInteger(0);


    /**
     * 接收客户端websocket连接 建立连接成功后会触发@OnOpen注解修饰的方法
     * @param session 当前客户端和服务端的连接
     * @param userId 当前客户端标记,获取websocket接口路径中的参数只能用 @PathParam 注解获取,而不是 @PathVariable 注解或者不添加注解都会报错
     *               @PathParam 注解的value就是路径中的形参名称
     */
    @OnOpen
    public void onOpen(Session session,@PathParam("userId") Integer userId){
        this.session = session;
        concurrentHashMap.put(userId,this);
        log.info("有新的客户端连接了:userId:"+userId+" 当前客户端连接总数:"+onlineCount.incrementAndGet());
    }

    /**
     * 关闭客户端websocket连接 建立的连接断开后会触发@OnClose注解修饰的方法
     * @param userId
     * @param session
     */
    @OnClose
    public void  onClose(Session session, @PathParam("userId") Integer userId){
        try {
            concurrentHashMap.remove(userId);
            session.close();
            log.info("有客户端关闭了连接:userId:"+userId+" 当前客户端连接总数:"+onlineCount.decrementAndGet());
        } catch (IOException e) {
            log.error("关闭客户端连接失败:"+e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     *  客户端和服务端建立连接或通信时出现异常会触发@OnError注解修饰的方法
     * @param session
     * @param error 具体错误信息的封装
     * @param userId
     */
    @OnError
    public void onError(Session session,Throwable error,@PathParam("userId") Integer userId){
        log.error("websocket发生错误,userId:"+userId+" 错误信息:"+error.getMessage());
        error.printStackTrace();
    }

    /**
     * 接收客户端通过websocket发送的消息 客户端发送消息到服务端时,会触发@OnMessage注解修饰的方法
     * @param session
     * @param message 客户端发送给服务端的消息
     * @param userId
     */
    @OnMessage
    public void onMessage(Session session,String message,@PathParam("userId")Integer userId){
        log.info("收到客户端发送的消息!"+message+" 客户端userId:"+userId);
        //服务端收到客户端的消息之后,后续逻辑按照业务进行处理,我这里是回复客户端消息并将消息转发给目标客户端
        //这种方式可以作为websocket实现多个客户端之间通信的方式,如果不需要通过服务端转发消息给目标客户端下面的方法调用可以删除
        try {
            session.getBasicRemote().sendText("我是服务端已收到你发送的消息,现在将你的消息转发给目标客户端!");
        } catch (IOException e) {
            e.printStackTrace();
        }
        WebSocketMessageDTO webSocketMessageDTO = JSONUtil.toBean(JSONUtil.toJsonStr(message), WebSocketMessageDTO.class);
        log.info("转换后的客户端消息:"+webSocketMessageDTO.toString());
        sendMessage(webSocketMessageDTO.getMessage(),webSocketMessageDTO.getUserId());
    }


    /**
     * 通过服务端保存的websocket给客户端发送消息
     * @param message 服务端要发送给客户端的消息
     * @param userId 接收服务端推送消息的客户端标记即给哪个客户端推送消息
     */
    public void sendMessage(String message,Integer userId){
        if (userId == null || StringUtils.isEmpty(message)){
            log.info("userId或消息为空!");
            return;
        }
        if (CollUtil.isEmpty(concurrentHashMap)){
            log.info("没有任何可发送消息的客户端!");
            return;
        }
        if (userId.equals(0)){
            //如果userId是0的话我们可以当作是给所有客户端发送该消息,可以基于自己的逻辑定义
            log.info("全部客户端推送当前消息:"+message);
            concurrentHashMap.forEach((userIdTemp,webSocketServerTemp)->{
                sendMessageSession(webSocketServerTemp,message,userIdTemp);
            });
        }else {
            //userId不是0则给指定某个客户端单独发送消息
            log.info("指定客户端推送当前消息:"+message);
            WebSocketServer webSocketServer = concurrentHashMap.get(userId);
            sendMessageSession(webSocketServer,message,userId);
        }
    }

    /**
     * 给某个websocket客户端发送消息
     * @param webSocketServer 某个客户端和服务端连接的WebSocketServer信息
     * @param message 服务端要发送客户端的消息
     */
    private void sendMessageSession(WebSocketServer webSocketServer,String message,Integer userId){
        if (webSocketServer != null && webSocketServer.session.isOpen()){
            try {
                webSocketServer.session.getBasicRemote().sendText(message);
                log.info("消息发送完毕,客户端userId:"+userId+" 消息内容:"+message);
            } catch (IOException e) {
                log.error("发送客戶端websocket消息失败,userId:"+userId+" 错误信息:"+e.getMessage());
                e.printStackTrace();
            }
        }else {
            log.error("客户端已断开不可发送消息,客户端userId:" + userId);
        }
    }



}

到此websocket服务端的定义就已经完成了,下面写的是模拟数据变化时给客户端发送消息的接口,和websocket本身没关系

package com.test.websocket.websockettest.controller;

import cn.hutool.core.collection.CollUtil;
import com.test.websocket.websockettest.service.WebSocketServer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/websocket")
public class TestWebSocketController {


    /**
     * 通过调用该接口模仿服务端数据产生变化主动将消息推送给目标客户端
     * 实际上数据的变化可能来源于redis或mq里面的数据变化消费等,导致业务上做一定处理需要给客户端通知消息
     * @param userId 目标客户端标识
     * @param message 消息内容
     */
    @GetMapping("/send/{userId}/{message}")
    public String sendMessage(@PathVariable("userId") Integer userId,@PathVariable("message") String message){
        if (CollUtil.isNotEmpty(WebSocketServer.concurrentHashMap)){
            WebSocketServer.concurrentHashMap.get(WebSocketServer.concurrentHashMap.keys().nextElement()).sendMessage(message,userId);
            return "消息发送完毕!";
        }else {
            return "没有客户端可发送!";
        }
    }

}

测试功能:
可以使用该链接进行测试,这样就不用单独再写客户端 websocket客户端调试地址 只要服务端可以访问该链接就可以使用,比如本地或部署在云服务器上的服务端。

两个客户端发送消息时,是json格式且带了userId和message而不是直接的消息是因为需要指定将当前客户端发送的消息转发给目标客户端,所以userId是目标客户端的标记,message则是要发送给目标客户端的消息,就像微信私聊一样,发送的消息得有一个接受消息的好友,只是这块添加好友标记的逻辑即这里的userId可以交由代码自动实现,我这里因为测试网站没有该功能,只能输入userId来模拟相关数据,userId为0则是将消息发送给所有客户端,相当于将消息发送给所有微信好友,如果只是当前客户端和服务端的通信,那么只需要发送消息即可不需要指定userId,当然服务端的 @OnMessage 方法只需要回复客户端即可,不需要做转发操作需要做相关逻辑调整。

客户端1
在这里插入图片描述

客户端2
在这里插入图片描述

模拟服务端数据变化主动推送消息给所有客户端
在这里插入图片描述
服务端日志
在这里插入图片描述

错误解决

1、项目启动时报错如下:
在这里插入图片描述
原因是websocket相关类导入的包错了,正确应该是 javax.websocket 包下面的类,我是错误的导入了 jakarta.websocket 包下面的类,具体原因是因为项目指定的 spring-boot-starter-parent 的版本有问题,可以在 https://mvnrepository.com 网页中查看spring-boot-starter-parent有哪些版本,一般建议采用使用量最多的RELEASE版本的包,因为release版的包是稳定发行版本的包,其他版本的包可能是不稳定存在一些问题的包,有问题时导入的是 3.0.3 版本,当编写websocket服务端时并没有 javax.websocket 这个包只有 jakarta.websocket 包所以报错,将 spring-boot-starter-parent 的版本换成 2.3.5.RELEASE 之后重启项目即解决。

2、项目启动报错 @PathParam
在这里插入图片描述
意思是在被websocket连接和消息相关注解标记的方法中,形参除了固定的参数比如 Session、Throwable、message等之外的形参,都需要使用 @PathParam 注解标记,使用其他注解标记或不写注解标记都会报错如上,@PathParam 注解的 value 值就是 @ServerEndpoint 注解定义的地址中的参数名称。

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

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

相关文章

Rust学习总结之if,while,loop,for使用

目录 一&#xff1a;if的使用 二&#xff1a;while的使用 三&#xff1a;loop的使用 四&#xff1a;for的使用 本文总结的四种语句&#xff08;if&#xff0c;while&#xff0c;loop&#xff0c;for&#xff09;除了loop&#xff0c;其他的三个在C语言或者Python中都是常见…

DDD系列 - 第1讲 DDD相关概念入门

目录一、引言二、 统一语言Ubiquitous Language三、 三个阶段&#xff08;战略、战术、实现&#xff09;阶段1&#xff1a;战略设计阶段阶段2&#xff1a;战术设计阶段阶段3&#xff1a;技术实现阶段四、限界上下文Bounded Context五、上下文映射Context Map防腐层Anti-Corrupt…

深度学习代码怎么读-小白阶段性思路

深度学习代码怎么读-小白阶段性思路目前思路学习资料读代码工具-chatgpt目前思路 努力上路的小白一枚&#xff0c;麻烦路过的大佬指导一二&#xff0c;同时希望能和大家交流学习~ 和学长、实习老师们交流后的目前思路&#xff1a; 先找到自己研究领域的顶级期刊&#xff0c;…

21 Nacos客户端本地缓存及故障转移

Nacos客户端本地缓存及故障转移 在Nacos本地缓存的时候有的时候必然会出现一些故障&#xff0c;这些故障就需要进行处理&#xff0c;涉及到的核心类为ServiceInfoHolder和FailoverReactor。 本地缓存有两方面&#xff0c;第一方面是从注册中心获得实例信息会缓存在内存当中&a…

AGV机器人出圈:助力产线物流自动化

随着开年档电影《流浪地球2》的热映&#xff0c;里面的四足仿生机器人机械狗“笨笨”、可穿戴的外骨骼机器人等“黑科技”&#xff0c;都让人对机器人的魅力刮目相看&#xff0c;机器人成功“出圈”了&#xff0c;随着智能技术的发展与进步&#xff0c;我们常见的机器人种类越来…

Linux命令之sed

sed&#xff0c;Stream Editor&#xff08;字符流编辑器&#xff09;的缩写&#xff0c;简称流编辑器&#xff0c;是操作、过滤、转换文本内容的工具。 常用功能包括结合正则表达式对文件实现快速的增删改查。 工作原理 sed有2个空间来缓存数据&#xff0c;paattern space&am…

Qt交叉编译环境搭建

环境及版本&#xff1a;Deepin 20.3 Qt 5.12.9 arm编译工具 gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz 1.下载Qt源码&#xff1a;qt-everywhere-src-5.12.9.tar.xz&#xff0c;并解压 2.下载arm编译工具&#xff1a; gcc-linaro-7.5.0-2019.12-x86_64_arm…

央企集团是怎么设置信息化、数字化部门的?

在数字经济大潮中&#xff0c;数字化转型已不是企业的“选修课”&#xff0c;而是关乎企业生存和长远发展的“必修课”。在企业数字化转型中&#xff0c;国有企业特别是中央企业普遍将数字化转型战略作为“十四五”时期业务规划的重要内容之一&#xff0c;数字化能力也成为衡量…

代码随想录【Day31】| 455. 分发饼干、376. 摆动序列、53. 最大子数组和

455. 分发饼干 题目链接 题目描述&#xff1a; 假设你是一位很棒的家长&#xff0c;想要给你的孩子们一些小饼干。但是&#xff0c;每个孩子最多只能给一块饼干。 对每个孩子 i&#xff0c;都有一个胃口值 g[i]&#xff0c;这是能让孩子们满足胃口的饼干的最小尺寸&#xff…

用Docker搭建yolov5开发环境

拉取镜像 sudo docker pull pytorch/pytorch:latest 创建容器 sudo docker run -it -d --gpus "device0" pytorch/pytorch bash 查看所有容器 sudo docker ps -a 查看运行中的容器 sudo docker ps 进入容器 docker start -i 容器ID 将依赖包全都导入到requiremen…

如何将图数据库应用于电影智能推荐

导读 电影&#xff0c;是一种结合视觉与听觉的现代艺术。如今&#xff0c;电影已不单是人们娱乐消遣的生活方式&#xff0c;也逐渐成为国家文化软实力的重要标志之一。据有关数据统计&#xff0c;2021年中国影视行业市场规模达2349亿元&#xff0c;同比增长23.2%&#xff0c;预…

java--IO

IO1.文件流2.常用的文件操作&#xff08;1&#xff09;根据路径构建一个File对象&#xff08;2&#xff09;根据父目录文件子路径构建&#xff08;3&#xff09;根据父目录子路径构建&#xff08;4&#xff09;获取文件相关信息&#xff08;5&#xff09;目录的操作和文件的删除…

计算机图形学07:有效边表法的多边形扫描转换

作者&#xff1a;非妃是公主 专栏&#xff1a;《计算机图形学》 博客地址&#xff1a;https://blog.csdn.net/myf_666 个性签&#xff1a;顺境不惰&#xff0c;逆境不馁&#xff0c;以心制境&#xff0c;万事可成。——曾国藩 文章目录专栏推荐专栏系列文章序一、算法原理二、…

Git 企业级分支提交流程

Git 企业级分支提交流程 首先在本地分支hfdev上进行开发&#xff0c;开发后要经过测试。 如果测试通过了&#xff0c;那么久可以合并到本地分支develop&#xff0c;合并之后hfdev和development应该完全一样。 git add 文件 git commit -m ‘注释’ git checkout develop //切换…

svn使用

一、SVN概述 1.1为什么需要SVN版本控制软件 1.2解决之道 SCM&#xff1a;软件配置管理 所谓的软件配置管理实际就是对软件源代码进行控制与管理 CVS&#xff1a;元老级产品 VSS&#xff1a;入门级产品 ClearCase&#xff1a;IBM公司提供技术支持&#xff0c;中坚级产品 1.…

【无标题】开发板设置系统时间

开发板设置系统时间环境查看系统时间查看硬件时间设置系统时间设置RTC时间时钟包括硬件时钟和系统时钟&#xff0c;系统时钟就是linux系统显示的时间&#xff0c;用命令 date可以显示当前系统时间&#xff1b;硬件时钟就是硬件自身的时间了。它们两者没有关系的&#xff0c;但是…

如何利用Power Virtual Agents机器人远程打开电脑中的应用

今天我们来介绍如何利用Power Virtual Agents来远程控制电脑。我们的设计思路是在聊天机器人里输入触发短语后打开自己电脑中的题库软件。 首先&#xff0c;进入已经创建好的聊天机器人编辑界面。 新建一个主题后&#xff0c;在“新建主题”中添加“触发短语”。 添加节点后&a…

C++代码优化(3):条款13~17

"野性袒露着灵魂纯粹"条款13:以对象管理资源(1)什么是资源&#xff1f;C中最常使用的资源就是动态内存分配&#xff0c;在系统编程层面上&#xff0c;文件描述符(fd)、互斥锁(mutex)、套接字网络socket……不管是哪一种资源&#xff0c;重要的是&#xff0c;你不使用…

CEC2014:鱼鹰优化算法(Osprey optimization algorithm,OOA)求解CEC2014(提供MATLAB代码

一、鱼鹰优化算法简介 鱼鹰优化算法&#xff08;Osprey optimization algorithm&#xff0c;OOA&#xff09;由Mohammad Dehghani 和 Pavel Trojovsk于2023年提出&#xff0c;其模拟鱼鹰的捕食行为。 鱼鹰是鹰形目、鹗科、鹗属的仅有的一种中型猛禽。雌雄相似。体长51-64厘米…

Spark 任务调度机制

1.Spark任务提交流程 Spark YARN-Cluster模式下的任务提交流程&#xff0c;如下图所示&#xff1a; 图YARN-Cluster任务提交流程 下面的时序图清晰地说明了一个Spark应用程序从提交到运行的完整流程&#xff1a; 图Spark任务提交时序图 提交一个Spark应用程序&#xff0c;首…