WebSocket的基本使用

news2024/11/22 14:22:22

目录

为何使用websocket

1.后端搭建

2.搭建webSocket前后分离

1.配置跨域过滤器与初始化websocket

2.定义websocket服务

3.定义控制器进行测试webSocket向前端发送消息

2.前端准备

3.进行测试

向后端发送消息测试

后端向前端发送消息测试


为何使用websocket

在浏览器与服务器通信间,传统的 HTTP 请求在某些场景下并不理想,比如实时聊天、实时性的小游戏等等,

其面临主要两个缺点:

  • 无法做到消息的「实时性」;
  • 服务端无法主动推送信息;

其基于 HTTP 的主要解决方案有:

  • 基于 ajax 的轮询:客户端定时或者动态相隔短时间内不断向服务端请求接口,询问服务端是否有新信息;其缺点也很明显:多余的空请求(浪费资源)、数据获取有延时;
  • Long Poll:其采用的是阻塞性的方案,客户端向服务端发起 ajax 请求,服务端挂起该请求不返回数据直到有新的数据,客户端接收到数据之后再次执行 Long Poll;该方案中每个请求都挂起了服务器资源,在大量连接的场景下是不可接受的;

可以看到,基于 HTTP 协议的方案都包含一个本质缺陷 —— 「被动性」,服务端无法下推消息,仅能由客户端发起请求不断询问是否有新的消息,同时对于客户端与服务端都存在性能消耗。

WebSocket 是 HTML5 开始提供的一种浏览器与服务器间进行全双工通讯的网络技术。 WebSocket 通信协议于2011年被IETF定为标准RFC 6455,WebSocketAPI 被 W3C 定为标准。 在 WebSocket API 中,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

WebSocket 是 HTML5 中提出的新的网络协议标准,其包含几个特点:

  • 建立于 TCP 协议之上的应用层;
  • 一旦建立连接(直到断开或者出错),服务端与客户端握手后则一直保持连接状态,是持久化连接;
  • 服务端可通过实时通道主动下发消息;
  • 数据接收的「实时性(相对)」与「时序性」;
  • 较少的控制开销。连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。而HTTP协议每次通信都需要携带完整的头部。
  • 支持扩展。ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。(比如支持自定义压缩算法等)

实践

1.后端搭建

准配工作

所需要架包 注意:springboot环境 版本2.7.7

  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-websocket</artifactId>
  </dependency>

  <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
  </dependency>
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
  </dependency>
<!--  <dependency>
      <groupId>cn.hutool</groupId>
      <artifactId>hutool-json</artifactId>
      <version>5.8.9</version>
  </dependency>-->
  <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>2.0.21</version>
  </dependency>
  <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.7</version>
  </dependency>

application配置

server.port=8080
server.servlet.context-path=/

2.搭建webSocket前后分离

1.配置跨域过滤器与初始化websocket

package com.zking.web.websocketdemo.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;


/**
 * Spring MVC 配置
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final Logger logger = LoggerFactory.getLogger(WebMvcConfig.class);

    //服务器支持跨域
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "POST","OPTIONS")
                .allowedHeaders("*")
                .exposedHeaders("Access-Control-Allow-Headers",
                        "Access-Control-Allow-Methods",
                        "Access-Control-Allow-Origin",
                        "Access-Control-Max-Age",
                        "X-Frame-Options")
                .allowCredentials(false)
                .maxAge(3600);
    }

    
    /**
     * The bean shown in the preceding example registers any @ServerEndpoint
     * annotated beans with the underlying WebSocket container. When deployed to a
     * standalone servlet container, this role is performed by a servlet container
     * initializer, and the ServerEndpointExporter bean is not required.
     * 
     * @return
     * 在Spring中可以直接使用Java WebSocket API来提供服务,如果使用内置的web容器,需要做的仅仅是需要在下面添加
     */
    /** 注入ServerEndpointExporter,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint 。
     * 要注意,如果使用独立的servlet容器,而不是直接使用springboot的内置容器,就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理。*/
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
    
}

2.定义websocket服务

websocket 是 javax.websocket下面的,不需要任何依赖,直接就可以使用

  @ServerEndpoint 标记声明一个websocket 服务 ,configurator 属性指定 鉴权 配置类,@ServerEndpoint 标记的类 为每个链接会创建一个该对象实例,也就是成员变量这个链接内私有。

  @OnOpen , @OnClose , @OnMessage , @OnError 4个事件方法,对应事件触发的时候调用 (除了@PathParam("path") 标记的参数以外,最多只能有 String message, Session session 两个参数)

package com.zking.web.websocketdemo.component;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;


import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

@ServerEndpoint("/ws/{sid}")
@Component
public class WebSocketServer {

    private final static Logger log = LoggerFactory.getLogger(WebSocketServer.class);
    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static int onlineCount = 0;
    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
    //旧:concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。由于遍历set费时,改用map优化
    //private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();
    //新:使用map对象优化,便于根据sid来获取对应的WebSocket
    private static ConcurrentHashMap<String,WebSocketServer> websocketMap = new ConcurrentHashMap<>();
    //接收用户的sid,指定需要推送的用户
    private String sid;

    /**
     * 连接成功后调用的方法
     */
    @OnOpen
    public void onOpen(Session session,@PathParam("sid") String sid) {
        this.session = session;
        //webSocketSet.add(this);     //加入set中
        websocketMap.put(sid,this); //加入map中
        addOnlineCount();           //在线数加1
        log.info("有新窗口开始监听:"+sid+",当前在线人数为" + getOnlineCount());
        this.sid=sid;
        try {
            sendMessage("连接成功");
        } catch (IOException e) {
            log.error("websocket IO异常");
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        if(websocketMap.get(this.sid)!=null){
            //webSocketSet.remove(this);  //从set中删除
            websocketMap.remove(this.sid);  //从map中删除
            subOnlineCount();           //在线数减1
            log.info("有一连接关闭!当前在线人数为" + getOnlineCount());
        }
    }

    /**
     * 收到客户端消息后调用的方法,根据业务要求进行处理,这里就简单地将收到的消息直接群发推送出去
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("收到来自窗口"+sid+"的信息:"+message);
        if(StringUtils.isNotBlank(message)){
            for(WebSocketServer server:websocketMap.values()) {
                try {
                    server.sendMessage(message);
                } catch (IOException e) {
                    e.printStackTrace();
                    continue;
                }
            }
        }
    }

    /**
     * 发生错误时的回调函数
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("发生错误");
        error.printStackTrace();
    }

    /**
     * 实现服务器主动推送消息
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }


    /**
     * 群发自定义消息(用set会方便些)
     * */
    public static void sendInfo(@PathParam("message")  String message,@PathParam("sid") String sid) throws IOException {
        log.info("推送消息到窗口"+sid+",推送内容:"+message);
        /*for (WebSocketServer item : webSocketSet) {
            try {
                //这里可以设定只推送给这个sid的,为null则全部推送
                if(sid==null) {
                    item.sendMessage(message);
                }else if(item.sid.equals(sid)){
                    item.sendMessage(message);
                }
            } catch (IOException e) {
                continue;
            }
        }*/
        if(StringUtils.isNotBlank(message)){
            for(WebSocketServer server:websocketMap.values()) {
                try {
                    // sid为null时群发,不为null则只发一个
                    if (sid == null) {
                        server.sendMessage(message);
                    } else if (server.sid.equals(sid)) {
                        server.sendMessage(message);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    continue;
                }
            }
        }
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }
    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }
    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }
}

3.定义控制器进行测试webSocket向前端发送消息

package com.zking.web.websocketdemo.controller;

import com.zking.web.websocketdemo.component.WebSocketServer;
import com.zking.web.websocketdemo.config.WebSocketConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
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.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Controller
@RequestMapping("/websocket")
public class WebSocketController {

    //页面请求
    @GetMapping("/socket/{cid}")
    public ModelAndView socket(@PathVariable String cid) {
        ModelAndView mav=new ModelAndView("/socket");
        mav.addObject("cid", cid);
        return mav;
    }
    //推送数据接口
    @ResponseBody
    @RequestMapping("/socket/push/{cid}")
    public Map<String,Object> pushToWeb(@PathVariable String cid, String message) {
        Map<String,Object> result = new HashMap<>();
        try {
            WebSocketServer.sendInfo(message,cid);
            result.put("status","success");
        } catch (IOException e) {
            e.printStackTrace();
            result.put("status","fail");
            result.put("errMsg",e.getMessage());
        }
        return result;
    }

}

后端准配完毕

2.前端准备

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>websocket通讯</title>
</head>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
    var socket;
    function openSocket() {
        if(typeof(WebSocket) == "undefined") {
            console.log("您的浏览器不支持WebSocket");
        }else{
            console.log("您的浏览器支持WebSocket");
            //实现化WebSocket对象,指定要连接的服务器地址与端口  建立连接
            //等同于socket = new WebSocket("ws://localhost:8888/xxxx/im/25");
            //var socketUrl="${request.contextPath}/im/"+$("#userId").val();
            var socketUrl="http://localhost:8080/ws/"+$("#userId").val();
            socketUrl=socketUrl.replace("https","ws").replace("http","ws");
            console.log(socketUrl);
            if(socket!=null){
                socket.close();
                socket=null;
            }
            socket = new WebSocket(socketUrl);
            //打开事件
            socket.onopen = function() {
                console.log("websocket已打开");
                //socket.send("这是来自客户端的消息" + location.href + new Date());
            };
            //获得消息事件
            socket.onmessage = function(msg) {
                console.log(msg.data);
                //发现消息进入    开始处理前端触发逻辑
            };
            //关闭事件
            socket.onclose = function() {
                console.log("websocket已关闭");
            };
            //发生了错误事件
            socket.onerror = function() {
                console.log("websocket发生了错误");
            }
        }
    }
    function sendMessage() {
        if(typeof(WebSocket) == "undefined") {
            console.log("您的浏览器不支持WebSocket");
        }else {
            console.log("您的浏览器支持WebSocket");
            console.log('{"toUserId":"'+$("#toUserId").val()+'","contentText":"'+$("#contentText").val()+'"}');
            socket.send('{"toUserId":"'+$("#toUserId").val()+'","contentText":"'+$("#contentText").val()+'"}');
        }
    }
</script>
<body>
<p>【userId】:<div><input id="userId" name="userId" type="text" value="10"></div>
<p>【toUserId】:<div><input id="toUserId" name="toUserId" type="text" value="20"></div>
<p>【toUserId】:<div><input id="contentText" name="contentText" type="text" value="hello websocket"></div>
<p>【操作】:<div><a onclick="openSocket()">开启socket</a></div>
<p>【操作】:<div><a onclick="sendMessage()">发送消息</a></div>
</body>
</html>




运行前端与后端

3.进行测试

向后端发送消息测试

后端向前端发送消息测试

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

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

相关文章

小型云台机械手红外搬运功能的实现

1. 功能说明 在小型云台机械手前方安装近红外传感器&#xff0c;如果近红外触发&#xff08;检测到有货物&#xff09;&#xff0c;机械手开始抓取货物&#xff0c;并将货物从一个区域搬运到另一个指定区域&#xff1b;否则&#xff0c;机械臂不动作。 2. 使用样机 本实验使用…

【LeetCode】从前序与中序遍历序列构造二叉树 [M](二叉树重构)

105. 从前序与中序遍历序列构造二叉树 - 力扣&#xff08;LeetCode&#xff09; 一、题目 给定两个整数数组 preorder 和 inorder &#xff0c;其中 preorder 是二叉树的先序遍历&#xff0c; inorder 是同一棵树的中序遍历&#xff0c;请构造二叉树并返回其根节点。 示例 1&…

ASEMI整流桥MB10S,DB207S和ABS210有什么区别

编辑-Z ASEMI整流桥MB10S&#xff0c;DB207S和ABS210有什么区别&#xff1f;这几个型号从外观看都是很相似的&#xff0c;那么他们参数有什么不一样呢&#xff1f; MB10S参数&#xff1a; 型号&#xff1a;MB10S 封装&#xff1a;MBS-4 最大重复峰值反向电压&#xff08;VR…

缓冲区Buffer类的设计(参考Muduo实现)

Buffer的功能需求&#xff1a; Buffer类的设计目的是再创造一层应用层缓冲区。 其对外表现为一块连续的内存(char* p, int len)&#xff0c;以方便客户代码的编写。 size() 可以自动增长&#xf…

Java如何自定义一个变长数组?

文章目录思路分析实现代码测试结果首先需要声明的是&#xff0c; Java本身是提供了变长数组的&#xff0c;即 ArrayList。那么自定义一个变长数组有啥用&#xff1f;其实没啥用或者说用处不大&#xff0c;主要就是为了了解下变长数组的设计理念而已。实际工作中直接使用 ArrayL…

文华财经期货背离信号准确率高指标公式,单边趋势行情增仓上行多空共振策略

由“短线震荡波段王”和“三柱共振-高把握”指标合成 功能 : 1.红绿小波段黄蓝中波段粉青大波段 2.红绿中小波段 3.顶背底背提示 4.金叉死叉提示 5.多和空提示-金叉死叉改写 优点:宽幅震荡和窄幅震荡 弊端:单边行情(可结合多空趋势主图规避) 功能: 1.红绿波段 2.大中量仓-单…

MySQL索引详解

目录 1、为什么要有索引&#xff1f; 2、预备知识 3、为何IO交互要是 Page&#xff1f; 4、如何理解Page以及索引理解 5、索引操作 <1> 创建主键索引 <2> 创建唯一索引 <3> 普通索引的创建 <4> 全文索引的创建 <5> 查询索引 <5>…

Python图像识别实战(五):卷积神经网络CNN模型图像二分类预测结果评价(附源码和实现效果)

前面我介绍了可视化的一些方法以及机器学习在预测方面的应用&#xff0c;分为分类问题&#xff08;预测值是离散型&#xff09;和回归问题&#xff08;预测值是连续型&#xff09;&#xff08;具体见之前的文章&#xff09;。 从本期开始&#xff0c;我将做一个关于图像识别的…

BOOT进程控制模式与故障排错

1. BOOT reboot and shutdown—使用systemctl 命令。 systemctl poweroff–关机 systemctl reboot --重启 systemctl halt 禁用CPU 在7版本中使用systemctl 工具。 选择systemd target graphical.target 桌面图形模式 multi-user.target 多用户模式–命令行 rescue.target 救援…

Linux驱动开发基础__总线设备驱动模型

目录 1 驱动编写的三种方法 1.1 传统写法 1.2 总线设备驱动模型 1.3 设备树 2 在 Linux 中实现“分离”&#xff1a;Bus/Dev/Drv 模型 3 匹配原则 4 函数调用关系 1 驱动编写的三种方法 1.1 传统写法 1.2 总线设备驱动模型 引入platform_device、platform_driver&…

二叉数题型2

目录 二叉搜索树的众数 二叉树的最近公共祖先 修剪二叉树 二叉搜索树的众数 问题描述&#xff1a; 给你一个含重复值的二叉搜索树&#xff08;BST&#xff09;的根节点 root &#xff0c;找出并返回 BST 中的所有 众数&#xff08;即&#xff0c;出现频率最高的元素&#…

PROJ 9.1.1源码下载编译(Win10+VS2022)

目录PROJ什么是PROJPROJ下载方式资源结构编译PROJ打包编译成功的库PROJ 什么是PROJ Proj是一个免费的GIS工具。 它专注于地图投影的表达&#xff0c;以及转换。采用一种非常简单明了的投影表达PROJ&#xff0c;比其它的投影定义简单&#xff0c;但很明显。很容易就能看到各种…

无人机倾斜摄影测量技术的优势有哪些?

传统的地理信息获取工作一般是通过人工测量的方式进行&#xff0c;但这样的测量方式具有工作强度大、成本高等问题。随着现代科技的不断发展&#xff0c;测绘行业对地理信息数据的准确性、时效性要求也越来越高&#xff0c;人工成本和时间成本也为行业带来了巨大的压力。因此&a…

GIT回退到指定版本的两种方法(reset/revert)

实现多人合作程序开发的过程中&#xff0c;我们有时会出现错误提交的情况&#xff0c;此时我们希望能撤销提交操作&#xff0c;让程序回到提交前的样子&#xff0c;本文总结了两种解决方法&#xff1a;reset、revert。 命令特点reset该命令会强行覆盖当前版本和要回退的版本之…

ArcGIS基础实验操作100例--实验15设置字段属性域

本实验专栏来自于汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 基础编辑篇--实验15 设置字段属性域 目录 一、实验背景 二、实验数据 三、实验步骤 &#xff08;1&a…

如何用Sonic云真机打王者

使用Sonic进行跨网段部署&#xff0c;助力海外业务的公司进行专项检测。提供定时任务充分利用无人值守时间回归UI测试&#xff0c;省时省力。自研随机事件测试与UI遍历测试&#xff0c;支持打通Jenkins的DevOps流程&#xff0c;Sonic提供图像识别&#xff0c;后续还会添加poco控…

ECS-弹性容器服务 - Part 2

68-ECS-弹性容器服务 - Part 2 Hello大家好&#xff0c;我们今天继续ECS的内容。 Service load balancing 之前的课时讨论过&#xff0c;在ECS集群上创建的ECS服务支持AWS负载均衡器&#xff0c;而应用程序负载均衡器和ECS服务通常是一个很好的搭配&#xff0c;因为应用程序负…

Docker 基础概念介绍

一 什么是 docker &#xff1f; Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中&#xff0c;然后发布到任何流行的 Linux或Windows操作系统的机器上&#xff0c;也可以实现虚拟化。容器是完全使用沙箱机制&#xff0c;…

【nowcoder】笔试强训Day13

目录 一、选择题 二、编程题 2.1参数解析 2.2跳石板 一、选择题 1.一个关系数据库文件中的各条记录 &#xff08;&#xff09; 。 A. 前后顺序不能任意颠倒&#xff0c;一定要按照输入的顺序排列 B. 前后顺序可以任意颠倒&#xff0c;不影响库中的数据关系 C. 前后顺序…

前端面试题之计算机网络篇--HTTP协议

HTTP协议 1. GET和POST的请求的区别 GET和POST方法 GET和POST方法都是HTTP中的方法 什么是 HTTP&#xff1f; 超文本传输协议&#xff08;Hypertext Transfer Protocol&#xff0c;缩写 HTTP&#xff09;旨在启用客户端和服务器之间的通信。 HTTP 充当客户端和服务器之间的…