WebSocket 快速入门 - springboo聊天功能

目录

一、概述 

1、HTTP(超文本传输协议)

2、轮询和长轮询 

3、WebSocket

二、WebSocket快速使用

1、基于Java注解实现WebSocket服务器端

2、JS前端测试

三、WebSocket进阶使用

1、如何获取当前用户信息

2、 后端聊天功能实现


一、概述 

HTTP和WebSocket都是啥?

1、HTTP(超文本传输协议)

        比如我们去逛某宝的商品列表,从HTTP协议的角度来看,前端发送了一次HTTP请求,服务器返回一次HTTP响应。不过从始至终服务器都不会主动给客户端发送消息请求,这就是HTTP协议的特点。

        HTTP有轮询和长轮询是两种客户端与服务器之间进行数据交换的技术,常用于实现网页上的实时数据更新。这些技术在 WebSocket 出现之前特别流行,尤其是在需要从服务器实时获取更新的场景中。

2、轮询和长轮询 

轮询是一种客户端定时发送请求到服务器的技术,以检查是否有新数据可用。这是一种非常直接的方法,客户端每隔一定时间(如每5秒)向服务器发起HTTP请求,无论服务器是否真的有新数据提供。

  1. 客户端发起HTTP请求到服务器。
  2. 服务器立即响应,返回数据(如果有新数据)或者返回一个空响应。
  3. 客户端处理响应,然后等待一定的时间。
  4. 时间到了之后,客户端再次发送请求。
  5. 这个过程持续重复。

 缺点也很明显,这样频繁的向后端发起请求必定会给服务器带来大量不必要的数据流量和服务器负载,而且实时性不足,为此有了长轮询

长轮询是轮询的一个改进版本,可以提供更快的响应时间和更少的服务器负载。与传统轮询不同,长轮询在服务器没有数据可发送时不会立即响应客户端的请求。而是保持连接开放,直到有数据发送或达到预设的超时时间。

  1. 客户端发送HTTP请求到服务器。
  2. 服务器不立即响应。它将请求挂起,直到有新数据可用或超时。
  3. 一旦有新数据可用,服务器立即响应请求,发送数据给客户端。
  4. 客户端收到数据并处理后,立即发起新的请求,重复此过程。

这样减少了不必要的HTTP请求,因为每次请求都可能得到有效数据,而且响应速度更快,因为数据一旦可用,客户端会立即收到通知。

3、WebSocket

        又比如我们玩传奇一刀999的网页游戏,我们甚至全程都没有点一次鼠标,但是服务器就源源不断地将怪物的移动数据和攻击数据发给我们。这种服务器可以主动给客户端发送消息的可双向传输数据场景就是使用了WebSocket协议。

底层原理:

  • WebSocket协议建立在TCP协议基础上,所以服务器也容易实现,不同的语言都有支持。
  • tcp协议是全双工协议,http协议基于它,但设计成了单向
  • WebSocket没有同源限制。(什么是同源看这里: localhost跨域问题解决-CSDN博客)
  • WebSocket连接开始于一个HTTP请求,这个过程叫做握手这个请求包含特定的头部信息,表明客户端希望将连接“升级”为WebSocket。如果服务器接受请求,它会返回一个响应,同意升级到WebSocket。此后,这个TCP连接将用于WebSocket通信,而不是传统的HTTP请求和响应。

至于Socket协议,套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。它偏向于底层,与WebSocket的关系就如Java和JavaScript,可以说是除了名字相似之外没什么关系。 


二、WebSocket快速使用

1、基于Java注解实现WebSocket服务器端

需要使用到的类

首先这里需要着重说明一下,WebSocket中的Session对象与传统的前端发起HTTP请求时携带的那个session( token )概念是有所不同的。

它们有以下关键区别:

1、WebScokeT

  • Session在这里指的是由 Jakarta WebSocket API 提供的一个对象,它代表了一个 WebSocket 连接。
  • 每个 WebSocket的Session是独立的,代表了服务器与一个特定客户端之间的持续连接。
  • WebSocket连接是全双工的,允许数据在任何时候从任一端向另一端发送,适用于需要实时数据交换的场景。

2、HTTP

  • 在HTTP中,session 通常是用来在多个请求之间保存用户数据的一种方式,常通过 cookie 或 session 存储在服务器端。
  • Token,如 JWT(Json Web Tokens),通常用于身份验证和保持状态,是一个可以在客户端和服务器之间安全传输的加密信息。
  • HTTP请求是无状态的,通常每次请求之间不直接保持连接,每个请求都需要重新进行连接和身份验证。
/**
 * 监听WebSocket地址/ws
 */
@ServerEndpoint("/ws")
@Component
@Slf4j
public class WebSocketServerEndpoint {
    //线程安全的HashMap
    static Map<String,Session> sessionMap = new ConcurrentHashMap<>();

    //websocket连接成功就立即触发该注解修饰的方法
    @OnOpen
    public void onOpen(Session session){
        sessionMap.put(session.getId(),session);
        log.info("WebSocket is open");
    }

    //websocket收到客户端消息便会立即触发该方法
    @OnMessage
    public String onMessage(String message){
        log.info("收到了一条消息: " + message);
        return "已经收到了你的消息";
    }

    //websocket关闭连接就立即触发该方法
    @OnClose
    public void onClose(Session session){
        sessionMap.remove(session);
        log.info("Websocket is close");
    }


}

因此我们在这段基础代码中看到Session对象,实际上它就是每一个与该WebSocket服务器连接的独立的客户端对象。 

此时还不可以直接使用,虽然已经使用@Component修饰,但是还需要使用ServerEndpointExporter,它会扫描所有声明了@ServerEndpoint注解的Beans,并将它们注册到WebSocket服务器,使它们能够接收WebSocket连接请求。

@Configuration
public class WebSocketConfig {

    /**
     * 这个bean对象可以自动注册使用了@ServerEndpoint的Bean
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }

}

此时一个最基础的WebSocket服务器就搭建好了。

2、JS前端测试

写一个最简单的按钮,点击触发向ws发送一个Hello消息

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>ws-client</title>
</head>
<body>
<button id="sendMessage">
    打招呼!
</button>
</body>
<script>
    const button = document.getElementById('sendMessage');

    button.addEventListener("click", () => {
        const ws = new WebSocket("ws://localhost:8888/ws");

        ws.onopen = function () {
            console.log("Connection opened!");
            ws.send("hello!");
        };

        ws.onmessage = function (message) {
            console.log("Received:", message.data);
        };

    });
</script>
</html>


可以看到我们点击了按钮向后端发送了hello,之后后端就自动调用了@onMessage方法,向我们这个Session对象返回了消息message,最终调用了前端的onmessage将信息打印在了控制台上。

不过这样看起来似乎和一个HTTP协议没什么区别,同样都是前端发送一个请求然后后端返回一个数据。因此为了体现websocket能够主动的向客户端发送信息我们写一个定时器每两秒向服务器发送数据。

首先要在启动类上加上@EnableScheduling注解,这是用于在 Spring 应用中启用基于注解的定时任务。当你在配置类上加上@EnableScheduling注解后,Spring 的任务调度器将会被激活,使得你可以使用@Scheduled注解来定义具体的定时任务。
'

 /**
     * 模拟服务器每两秒主动向客户端发起请求
     * @throws IOException
     */
    @Scheduled(fixedDelay = 2000)
    public void sendMessage() throws IOException {

        for (String key: sessionMap.keySet()){
            sessionMap.get(key).getBasicRemote().sendText("心跳");
        }
    }

成功实现服务器每2秒向客户端发起一次数据 


三、WebSocket进阶使用

1、如何获取当前用户信息

        我们平常做一个软件,肯定都会使用到身份验证用于区分不同的用户。但是我们前面刚刚提到过WebSocket的Session与Http的Session不是同一个东西,那么我们想要实现两个用户聊天该如何实现呢?

public class GetTokenConfig extends ServerEndpointConfig.Configurator {

    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        // 获取请求头中的token(假设token存放在名为"Authorization"的请求头中)
        List<String> tokenHeader = request.getHeaders().get("Authorization");
        if (tokenHeader != null && !tokenHeader.isEmpty()) {
            String token = tokenHeader.get(0);
            // 将token保存起来
            sec.getUserProperties().put("token", token);
        }
    }
}

在这段代码中,我们创建一个类继承ServerEndpointConfig.Configurator,重写其中的modifyHandshake方法

        其实顾名思义,就是对WebSocket的握手进行了操作,我们一开始就提到过,WebSocket开始于一个HTTP请求,这个过程被称为握手,之后才开始WebSocket连接。虽然WebSocket本身并没有在建立连接后发送请求头的能力,不过在一开始握手的时候中是可以携带请求头的。

其中HandshakeRequest虽然与我们常用的HttpRequest没有什么关系,不过在使用的时候你可以简单粗暴的将它看作HttpRequest。

其次的ServerEndpointConfig其实就是EndpointConfig,将token存储在这个里面就可以方便别的类使用这个token了

我们稍微修改一下我我们之前的代码

/**
 * 监听WebSocket地址/ws
 */
@ServerEndpoint(value = "/ws", configurator = GetTokenConfig.class)
@Component
@Slf4j
public class WebSocketServerEndpoint {

    @Autowired
    private JwtProperties jwtProperties;


    static Map<String, WebSocketServerEndpoint> onlineUsers = new ConcurrentHashMap<>();

    private Session session;

    private String token;

    //websocket连接成功就立即触发该注解修饰的方法
    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        log.info("WebSocket is open,连接成功");

        this.session = session;

        // 从EndpointConfig获取token
        String token = (String) config.getUserProperties().get("token");

        this.token = token;
        log.info("token: " + token);

        this.jwtProperties = BeanUtil.getBean(JwtProperties.class);
        //从token中获取一下用户信息
        Integer userId = JwtUtil.getUserIdFromToken(jwtProperties.getUserSecretKey(), token);
        String username = JwtUtil.getUsernameFromToken(jwtProperties.getUserSecretKey(), token);


        log.info("当前用户:ID: " + userId + " Username " + username);
        

    }


}

使用Postman测试一下,因为JavaScript原生的Websocket的API没有直接携带请求头的能力

2、 后端聊天功能实现

/**
 * 监听WebSocket地址/ws
 */
@ServerEndpoint(value = "/ws", configurator = GetTokenConfig.class)
@Component
@Slf4j
public class WebSocketServerEndpoint {

    @Autowired
    private JwtProperties jwtProperties;


    static Map<Integer, Session> onlineUsers = new ConcurrentHashMap<>();

    private Integer userId;

    //websocket连接成功就立即触发该注解修饰的方法
    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        log.info("WebSocket is open,连接成功");

        // 从EndpointConfig获取token
        String token = (String) config.getUserProperties().get("token");

        log.info("token: " + token);

        this.jwtProperties = BeanUtil.getBean(JwtProperties.class);
        //从token中获取一下用户信息
        Integer userId = JwtUtil.getUserIdFromToken(jwtProperties.getUserSecretKey(), token);
        String username = JwtUtil.getUsernameFromToken(jwtProperties.getUserSecretKey(), token);
        this.userId = userId;

        // 将userId存储在Session的用户属性中
        session.getUserProperties().put("userId", userId);

        log.info("当前用户:ID: " + userId + " Username " + username);


        onlineUsers.put(userId,session);

    }

    //websocket收到客户端消息便会立即触发该方法
    @OnMessage
    public void onMessage(String message,Session session) {
        //将message转换成Message对象
        try {
            ObjectMapper mapper = new ObjectMapper();
            Message msg = mapper.readValue(message,Message.class);

            //获取要将数据发送给的用户
            Integer receiver = msg.getReceiver();
            
            //获取消息数据
            String data = msg.getMessage();

            //获取当前登录的这个用户,也就是发送者
            Integer senderId = (Integer) session.getUserProperties().get("userId");

            ResultMessage resultMessage = ResultMessage.builder().message(data).sender(senderId).build();

            //发送
            onlineUsers.get(receiver).getBasicRemote().sendText(mapper.writeValueAsString(resultMessage));


        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    //websocket关闭连接就立即触发该方法
    @OnClose
    public void onClose(Session session) {
        Integer userId = (Integer) session.getUserProperties().get("userId");
        if (userId != null) {
            onlineUsers.remove(userId);
            log.info("WebSocket is closed and user removed from onlineUsers: UserID = " + userId);
        } else {
            log.warn("User ID not found in session properties on close.");
        }
    }

}

测试

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Navicat Premium 16最新版激活 mac/win

Navicat Premium 16 for Mac是一款专业的多连接数据库管理工具。它支持连接多种类型的数据库&#xff0c;包括MySQL、MongoDB、Oracle、SQLite、SQL Server、PostgreSQL等&#xff0c;可以同时连接多种数据库&#xff0c;帮助用户轻松地管理和迁移数据。 Navicat Premium 16 fo…

Wpf 使用 Prism 实战开发Day21

配置默认首页 当应用程序启动时&#xff0c;默认显示首页 一.实现思路&#xff0c;通过自定义接口来配置应用程序加载完成时&#xff0c;设置默认显示页 步骤1.创建自定义 IConfigureService 接口 namespace MyToDo.Common {/// <summary>/// 配置默认显示页接口/// <…

Golang那些违背直觉的编程陷阱

目录 知识点1&#xff1a;切片拷贝之后都是同一个元素 知识点2&#xff1a;方法集合决定接口实现&#xff0c;类型方法集合是接口方法集合的超集则认定为实现接口&#xff0c;否则未实现接口 切片拷贝之后都是同一个元素 package mainimport ("encoding/json"&quo…

springboot是什么?

可以应用于Web相关的应用开发。 选择合适的框架&#xff0c;去开发相关的功能&#xff0c;会有更高的效率。 为什么Spring Boot才是你该学的!学java找工作必会技能!在职程序员带你梳理JavaEE框架_哔哩哔哩_bilibili java工程师的必备技能 Spring是Java EE领域的企业级开发宽…

Kafka源码分析(四) - Server端-请求处理框架

系列文章目录 Kafka源码分析-目录 一. 总体结构 先给一张概览图&#xff1a; 服务端请求处理过程涉及到两个模块&#xff1a;kafka.network和kafka.server。 1.1 kafka.network 该包是kafka底层模块&#xff0c;提供了服务端NIO通信能力基础。 有4个核心类&#xff1a;…

华为海思校园招聘-芯片-数字 IC 方向 题目分享——第六套

华为海思校园招聘-芯片-数字 IC 方向 题目分享——第六套 (共9套&#xff0c;有答案和解析&#xff0c;答案非官方&#xff0c;未仔细校正&#xff0c;仅供参考&#xff09; 部分题目分享&#xff0c;完整版获取&#xff08;WX:didadidadidida313&#xff0c;加我备注&#x…

使用python socket搭建Client测试平台

目录 概述 1 背景 2 Client功能实现 2.1 何谓Client 2.2 代码功能介绍 2.3 代码实现 2.3.1 代码介绍 2.3.2 代码内容 3 测试 3.1 PC上创建Server 3.2 同一台PC上运行Client 3.2.1 建立连接 3.2.2 测试数据交互 3.3 Linux 环境下运行Client 3.3.1 建立连接 3.3.…

无限滚动分页加载与下拉刷新技术探析:原理深度解读与实战应用详述

滚动分页加载&#xff08;也称为无限滚动加载、滚动分页等&#xff09;是一种常见的Web和移动端应用界面设计模式&#xff0c;用于在用户滚动到底部时自动加载下一页内容&#xff0c;而无需点击传统的分页按钮。这种设计旨在提供更加流畅、连续的浏览体验&#xff0c;减少用户交…

Redis 如何实现分布式锁

课程地址 单机 Redis naive 版 加锁&#xff1a; SETNX ${lockName} ${value} # set if not exist如果不存在则插入成功&#xff0c;返回 1&#xff0c;加锁成功&#xff1b;否则返回 0&#xff0c;加锁失败 解锁&#xff1a; DEL ${lockName}问题1 2 个线程 A、B&#…

深入理解与实践“git add”命令的作用

文章目录 **git add命令的作用****git add命令的基本作用****高级用法与注意事项** git add命令的作用 引言&#xff1a; 在Git分布式版本控制系统中&#xff0c;git add命令扮演着至关重要的角色&#xff0c;它是将本地工作区的文件变动整合进版本控制流程的关键步骤。本文旨…

使用docker搭建GitLab个人开发项目私服

一、安装docker 1.更新系统 dnf update # 最后出现这个标识就说明更新系统成功 Complete!2.添加docker源 dnf config-manager --add-repohttps://download.docker.com/linux/centos/docker-ce.repo # 最后出现这个标识就说明添加成功 Adding repo from: https://download.…

ConcurrentHashMap 源码分析(一)

一、简述 本文对 ConcurrentHashMap#put() 源码进行分析。 二、源码概览 public V put(K key, V value) {return putVal(key, value, false); }上面是 ConcurrentHashMap#put() 的源码&#xff0c;我们可以看出其核心逻辑在 putVal() 方法中。 final V putVal(K key, V val…

在centos系统中使用boost库

打开MobaXterm软件 下载 boost_1_85_0.tar.gz tar -zxvf boost_1_85_0.tar.gz解压缩成boost_1_85_0文件夹 双击arrayDemo.cpp 在里面可以编写代码 arrayDemo.cpp #include <boost/timer/timer.hpp> #include <boost/array.hpp> #include <cmath> #inc…

Redis中的Lua脚本(六)

Lua脚本 清空repl_scriptcache_dict字典 每当主服务器添加一个新的从服务器时&#xff0c;主服务器都会清空自己的repl_scriptcache_dict字典&#xff0c;这是因为随着新从服务器的出现&#xff0c;repl_scriptcache_字典里面记录的脚本已经不再被所有从服务器载入过&#xf…

天梯赛 L2-052 吉利矩阵

//r[n]:当前第几列的值。 //l[n]:当前第几行的值。 暴力减止 #include<bits/stdc.h> using namespace std; #define int long long const int n1e3; int a,b,c,l[n],r[n],an; void dfs(int x,int y) {if(xb1){an;return ;}for(int i0;i<a;i){l[x]i;r[y]i;if(l[x]&l…

【001_音频开发-基础篇-专业术语】

001_音频开发-基础篇-专业术语 文章目录 001_音频开发-基础篇-专业术语创作背景术语表常见音源HDMI相关声音系统立体声2.1 声音系统5.1 环绕声系统5.1.2 环绕声系统7.1 环绕声系统7.1.4 环绕声系统9.1.4 环绕声系统 音质等级定义QQ音乐网易云音乐 创作背景 学历代表过去、能力…

ubuntu安装QEMU

qemu虚拟机的使用&#xff08;一&#xff09;——ubuntu20.4安装QEMU_ubuntu安装qemu-CSDN博客 遇到的问题&#xff1a; (1)本来使用git clone https://github.com/qemu/qemu.git fatal: 无法访问 https://github.com/qemu/qemu.git/&#xff1a;GnuTLS recv error (-110): …

IoT、IIoT、AIoT的区别是什么?

一、IoT、IIoT、AIoT的区别是什么&#xff1f; IoT、IIoT和AIoT都是物联网&#xff08;Internet of Things&#xff09;的不同应用和发展方向&#xff0c;但它们之间存在一些区别。 IoT&#xff08;物联网&#xff09;&#xff1a;物联网是指通过互联网连接各种物理设备&#x…

密码学 | 数字证书:应用

&#x1f951;原文&#xff1a;数字签名和数字证书的原理解读 - 知乎 &#x1f951;前文&#xff1a;密码学 | 数字签名 数字证书 - CSDN &#x1f951;提示&#xff1a;把客户端想成 Alice&#xff0c;服务器端想成 Bob 即可。客户端实际上指的是客户端浏览器。 下面&#…

openGauss学习笔记-267 openGauss性能调优-TPCC性能调优测试指导-网络配置-网卡多中断队列设置

文章目录 openGauss学习笔记-267 openGauss性能调优-TPCC性能调优测试指导-网络配置-网卡多中断队列设置267.1 操作步骤 openGauss学习笔记-267 openGauss性能调优-TPCC性能调优测试指导-网络配置-网卡多中断队列设置 本章节主要介绍openGauss数据库内核基于鲲鹏服务器和openE…