WebSocket实现在线聊天室

news2024/11/16 12:30:30

项目实现源码:

前端源码

后端源码

1.常见的消息推送方式

1.1 轮询

1.1.1 轮询的概念

客户端以固定的事件间隔(例如每秒或几分钟)向服务器发送HTTP请求,服务器收到请求后,处理请求并返回数据给客户端

轮询具体实现https://blog.csdn.net/m0_48333563/article/details/125968144

1.1.2 轮询的优点

  • 实现简单:轮询是一种相对简单的获取都武器更新的方法,易于理解和实现
  • 兼容性:由于轮询基于HYTTP请求和响应,因此他兼容几乎所有的网络服务和客户端

1.1.3 轮询的缺点

  • 数据更新不及时:客户端必须等待下一次轮询间隔才能接收到新数据,这可能导致数据更新不及时
  • 资源浪费:频繁的轮询可能会浪费服务器和客户端的资源,油漆是在没有新数据的情况下,大部分请求都是无效的。

1.2 长轮询

1.2.1长轮询的概念

长轮询是一种改进的轮询技术,客户端向服务器发送HTTP请求。服务器接收到请求后,会阻塞请求,直到有新数据达到指定的超时时间才会返回结果

  • 如果有新数据,服务器会立即返回结果并关闭连接
  • 如果没有新数据,服务器会在超时后关闭连接
  • 客户端收到响应或连接超时后,会再发送新的请求

1.2.2 长轮询的优点

  • 实时性提升:长轮询可以更快的接受到服务器的更新,因为他减少了客户端在两次请求之间的等待时长
  • 减少了无效请求:与定时轮询相比,长轮询减少了在没有数据更新时无效请求的次数,因为服务器仅在数据准备好时才发送响应。

1.2.3 长轮询的缺点

  • 资源占用:虽然轮询减少了请求次数,但他可能会长时间占用服务器资源,因为服务器需要保持连接打开直到新数据出现或超时。
  • 兼容性和复杂度:长轮询的实现比简单的轮询复杂,需要服务器编写额外的逻辑
@RestController
@RequestMapping("/longpolling")
public class LongPollingController {

    private static final ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>();

    // 假设这是模拟的数据源
    private static final List<String> messages = new CopyOnWriteArrayList<>();

    @PostMapping("/subscribe")
    public Callable<String> subscribe(@RequestParam String clientId) {
        return () -> {
            // 模拟等待新数据
            synchronized (messages) {
                while (messages.isEmpty()) {
                    try {
                        messages.wait();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        return null;
                    }
                }
                String message = messages.remove(0);
                return message;
            }
        };
    }

    // 模拟数据推送
    public static void pushMessage(String message) {
        synchronized (messages) {
            messages.add(message);
            messages.notifyAll();
        }
    }

    // 注意:这里的示例为了简化并未真正使用WebSocketSession,实际中可能需要WebSocket来管理连接
}

1.3 WebSocket

1.4 SSE

SSE:服务器发送事件,主要用于服务器向客户端推送实时更新(不需要客户端主动请求)

  • SSE会在客户端和服务器之间打开一个单项通道
  • 服务器返回的不再是一次性的数据包,而是text/event-stream类型的数据流信息
  • 服务器有数据发生改变时会将数据以流的形式传输给客户端

SSE仅支持从服务器到客户端单项通信,客户端无法通过SSE发送到服务器

2.什么是WebSocket

2.1 全双工通信和半双工的概念

全双工:允许数据在两个方向上同时传输

半双工:允许数据在两个方向上传输,但是同一时间段只允许一个方向传输

2.2 WebScoket的概念

WebSocket 是一种基于 TCP 的网络通信协议,允许在客户端和服务器建立全双工的通信通道。这意味着客户端和服务器可以在任何时候互相发送消息,不需要像传统的 HTTP 请求那样等待响应。WebSocket 非常适合于需要实时更新数据的应用场景,如在线游戏、实时聊天、实时数据推送等

2.3WebSocket的原理

WebSocket 协议会在客户端和服务器之间建立一条持久的连接通道,连接建立后,双方可以在任意时间通过这个通道发送数据,每次请求无需重新建立连接

WebSocket 的数据传输是双向的,这意味着服务器可以主动向客户端推送数据,而不仅仅是响应客户端的请求

WebScoket连接建立的步骤:

1.客户端发起握手请求:客户端通过HTTP请求发起WebScoket握手请求

2.服务器响应握手请求:服务器收到握手请求后,如果同意升级协议,就会返回一个HTTP 101状态码,表示协议切换成功

3.连接建立:握手成功,客户端和服务器之间的连接切换成WebSockt协议,之后双方通过此连接进行双向通信。

3.浏览器中与WebSocket相关的API

3.1websocket对象创建

let ws = new WebSocket(URL);

3.2 websocket对象相关事件

3.3 websocket对象提供的方法

3.4具体代码实现:

3.4.1 前端

<script>
  let ws = new WebScoket("ws://localhost/chat");

  ws.onopen = function() {
    
  };
  ws.onmessage = function(evt) {
    // 通过evt.data 可以获取服务器发送的数据
  }
</script>

4.服务器API

Tomcat的7.0.5版本开始支持WebSocket,并实现了Java WebSocket规范。

Java WebSocket应用由一系列的Endpoint组成。Endpoint是一个java对象,代表WebSocket链接的一端,对于服务器,我们可以视为处理具体的WebSocket消息的接口。

我们可以通过两种方式定义Endpoint:

第一种是编程式,继承javax.websocket.Endpoint并实现其方法

第二种是注解式,定义一个POJO,并添加@ServerEndpoint

Endpoint实例在WebSocket握手时创建,并在客户端与服务器连接过程中有效,在最后关闭连接时结束。在Endpoint接口中明确定义与其生命周期县官的方法,规范实现者确保生命周期的各个阶段调用实例相关的方法。生命周期如下:

@ServerEndpoint("/chat")
@Component
public class ChatEndpoint{

    @OnOpen
    // 连接建立时被调用
    public void onOpen(Session session, EndpointConfig config){
        
    }

    @OnMessage
    // 接收到客户端发送的数据时被调用
    public void onMessage(String message) {
        
    }

    @OnClose
    // 连接关闭时被调用
    public void onClose(Session session) {
        
    }
}

5.需求分析

5.1通过websocket实现在线聊天室

登陆页面:

聊天页面:

5.2 流程分析

5.3消息格式:

客户端->服务器

{
  "toName":"张三",
  "message":"你好"
}

服务器->客户端

①系统消息格式:

{
  "system":true,
  "fromName":null,
  "message"["李四","王五"]
}

②推送给某一个用户的消息格式:

{
  "system":false,
  "fromName":"张三",
  "message""你好"
}

6.搭建websocket服务端

6.1 后端环境

  • SpringBoot: 2.6.7
  • JDK: 1.8

6.2 准备工作(导入Maven依赖)

WebSocket:

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

Web:

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

fastjson2:

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.50</version>
</dependency>

6.3 编写配置类,扫描添加有@ServerEndpoint注解的 Bean

@Configuration
public class WebSocketConfig {

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

6.4 编写配置类,用于获取HttpSession对象

public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {

    @Override
    public void modifyHandshake(ServerEndpointConfig serverEndpointConfig, HandshakeRequest request, HandshakeResponse response) {
        HttpSession httpSession = (HttpSession) request.getHttpSession();

        // 将 httpSession 对象存到 ServerEndpointConfig 对象中
        serverEndpointConfig.getUserProperties().put(HttpSession.class.getName(), httpSession);
    }

}

6.4在@ServerEndPoint注解中指定配置类

import com.alibaba.fastjson2.JSON;
import jakarta.servlet.http.HttpSession;
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;

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

@ServerEndpoint(value = "/chat", configurator = GetHttpSessionConfig.class)
@Component
public class ChatEndpoint {

    // 保存在线的用户,key为用户名,value为 Session 对象
    private static final Map<String, Session> onlineUsers = new ConcurrentHashMap<>();

    private HttpSession httpSession;

    /**
     * 建立websocket连接后,被调用
     *
     * @param session Session
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());

        String user = (String) this.httpSession.getAttribute("currentUser");
        if (user != null) {
            onlineUsers.put(user, session);
        }

        // 通知所有用户,当前用户上线了
        String message = MessageUtils.getMessage(true, null, getFriends());
        broadcastAllUsers(message);
    }


    private Set<String> getFriends() {
        return onlineUsers.keySet();
    }

    private void broadcastAllUsers(String message) {
        try {
            Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet();

            for (Map.Entry<String, Session> entry : entries) {
                // 获取到所有用户对应的 session 对象
                Session session = entry.getValue();

                // 使用 getBasicRemote() 方法发送同步消息
                session.getBasicRemote().sendText(message);
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

    /**
     * 浏览器发送消息到服务端时该方法会被调用,也就是私聊
     * 张三  -->  李四
     *
     * @param message String
     */
    @OnMessage
    public void onMessage(String message) {
        try {
            // 将消息推送给指定的用户
            Message msg = JSON.parseObject(message, Message.class);

            // 获取消息接收方的用户名
            String toName = msg.getToName();
            String tempMessage = msg.getMessage();

            // 获取消息接收方用户对象的 session 对象
            Session session = onlineUsers.get(toName);
            String currentUser = (String) this.httpSession.getAttribute("currentUser");
            String messageToSend = MessageUtils.getMessage(false, currentUser, tempMessage);

            session.getBasicRemote().sendText(messageToSend);
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

    /**
     * 断开 websocket 连接时被调用
     *
     * @param session Session
     */
    @OnClose
    public void onClose(Session session) throws IOException {
        // 1.从 onlineUsers 中删除当前用户的 session 对象,表示当前用户已下线
        String user = (String) this.httpSession.getAttribute("currentUser");
        if (user != null) {
            Session remove = onlineUsers.remove(user);
            if (remove != null) {
                remove.close();
            }

            session.close();
        }

        // 2.通知其他用户,当前用户已下线
        // 注意:不是发送类似于 xxx 已下线的消息,而是向在线用户重新发送一次当前在线的所有用户
        String message = MessageUtils.getMessage(true, null, getFriends());
        broadcastAllUsers(message);
    }

}

7.搭建WebSocket客户端

前端使用的技术:Vue3+Axois+Elementplus

7.1创建一个axios实例

import axios from 'axios'

const request = axios.create({
  baseURL: '/api',
  timeout: 60000,
  headers: {
    'Content-Type': 'application/json;charset=UTF-8'
  }
})

request.interceptors.request.use(

)

request.interceptors.response.use(response => {
  if (response.data) {
    return response.data
  }
  return response
}, (error) => {
  return Promise.reject(error)
})

export default request

7.2 编写代理规则

vite.config.js

import {fileURLToPath, URL} from 'node:url'

import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue()
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:7024',
        changeOrigin: true,
        rewrite: (path) => {
          return path.replace('/api', '')
        }
      }
    }
  }
})

7.2创建WebSocket对象

webSocket.value = new WebSocket('ws://localhost:7024/chat')

7.3为WebSocket对象绑定事件

webSocket.value.onopen = onOpen

// 接收到服务端推送的消息后触发
webSocket.value.onmessage = onMessage

webSocket.value.onclose = onClose

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

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

相关文章

Redis的一些数据类型(一)

&#xff08;一&#xff09;数据类型 我们说redis是key value键值对的方式存储数据&#xff0c;key是字符串&#xff0c;而value是一些数据结构,那今天就来说一下value存储的数据。 我们数据结构包含&#xff0c;String&#xff0c;hash&#xff0c;list&#xff0c;set和zest但…

SegFormer网络结构的学习和重构

因为太多的博客并没有深入理解,本文是自己学习后加入自己深入理解的总结记录&#xff0c;方便自己以后查看。 segformer中encoder、decoder的详解。 学习前言 一起来学习Segformer的原理,如果有用的话&#xff0c;请记得点赞关注哦。 一、Segformer的网络结构图 网络结构&…

反转字符串 II--力扣541

反转字符串 II 题目思路代码 题目 思路 本题的关键在于理解每隔 2k 个字符的前 k 个字符进行反转&#xff0c;剩余字符小于 2k 但大于或等于 k 个&#xff0c;则反转前 k 个字符。并且剩余字符少于 k 个&#xff0c;则将剩余字符全部反转。 让i每次跳2k&#xff0c;成为每一次…

Xshell 连接 VMware虚拟机操作 截图和使用

Xshell 连接 VMware虚拟机操作 文章目录 Xshell 连接 VMware虚拟机操作一、本机环境截图1.1配置 Xshell环境截图VWware 配置环境截图最后下载地址 一、本机环境截图 1.1配置 Xshell环境截图 VWware 配置环境截图 最后 下载地址 vmware https://www.vmware.com/ VMware总部位于…

电场(electric-field)

图中&#xff1a; Q 产生电场的正电荷&#xff08;可正可负&#xff0c;这里用正举例&#xff09;q 试验电荷&#xff0c;正电荷&#xff08;习惯上用正电荷&#xff09;p 试验电荷所在的位置&#xff08;即要测的电场强度的位置&#xff09;r 为电荷间的距离 r ^ \widehat{r}…

广州电影产业博览交易会将于本周五开始

“影动广州绽放世界”广州电影产业博览交易会由广州市人民政府主办&#xff0c;广州市委宣传部承办&#xff0c;将在广交会展馆A区4.2及5.2馆启幕。本届广州影博会聚焦电影产业交易、科技创新和消费市场&#xff0c;链接国内外电影资源&#xff0c;活动内容丰富。设置电影主题展…

MySQL Performance Schema 详解及运行时配置优化

引言 MySQL 的 Performance Schema 是一套性能监控与诊断工具&#xff0c;帮助开发者和数据库管理员收集、分析 MySQL 实例的运行状态&#xff0c;找出性能瓶颈并进行优化。通过 Performance Schema&#xff0c;我们能够监控不同的内部事件、线程、会话、语句执行等关键性能指…

Python批量合并365个工作表的2种方法

一、引言 小明刚进入到新公司&#xff0c;就被委以重任&#xff1a;将365个Excel文件中的英文表头修改为中文。传统方法是逐一打开每个文件&#xff0c;手动修改标题&#xff0c;然后保存&#xff0c;最后再合并。这种方法不仅耗时耗力&#xff0c;还容易出错。如果用Python就…

[大语言模型-论文精读] MoRAG - 基于多部分融合的检索增强型人体动作生成

MoRAG--Multi-Fusion Retrieval Augmented Generation for Human Motion KS Shashank, S Maheshwari, RK Sarvadevabhatla - arXiv preprint arXiv:2409.12140, 2024 MoRAG - 基于多部分融合的检索增强型人体动作生成 1. 目录 MoRAG--Multi-Fusion Retrieval Augmented Generat…

Redis:缓存

为什么要理解Redis缓存问题 在高并发的业务场景下&#xff0c;数据库大多数情况都是用户并发访问最薄弱的环节。所以&#xff0c;就需要使用redis做一个缓冲操作&#xff0c;让请求先访问到redis&#xff0c;而不是直接访问Mysql等数据库。这样可以大大缓解数据库的压力。 当…

【MySql】在ubuntu下安装MySql数据库

目录 查看操作系统版本 添加 MySql APT源 访问下载页面并下载发布包 安装发布包 执行安装命令 从MySql APT源更新包信息 安装MySql 执行安装命令 查看MySql状态 开启自启动 登录MySql 查看操作系统版本 rootVM-24-2-ubuntu:~# lsb_release -a No LSB modules are ava…

软考高级:需求工程- 4+1 视图 AI 解读

这几个术语常见于软件架构的描述中&#xff0c;它们分别代表软件系统的不同角度或视角。为了更好理解它们&#xff0c;我们可以把软件系统想象成一个公司&#xff0c;每个视图就像从不同角度观察这个公司的运作方式。 生活化例子 想象你在经营一家餐馆&#xff1a; 逻辑视图…

SSM+Vue共享单车管理系统

目录 1 项目介绍2 项目截图3 核心代码3.1 Controller3.2 Service3.3 Dao3.4 spring-mybatis.xml3.5 spring-mvc.xml3.5 Vue 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍&#xff1a;CSDN认证博客专家&#xff0c;CSDN平台Java领域优质创作…

C++网络编程之网络模型

概述 所谓网络模型&#xff0c;是指一组定义了网络通信功能和行为的规则和标准。这些模型通过将网络通信功能分解成不同的层次&#xff0c;使得网络通信更加模块化&#xff0c;也更易于理解和实施。每一层都有其特定的功能&#xff0c;通过层与层之间的交互&#xff0c;确保数据…

await命令的用法

正常情况下&#xff0c;await 命令后面是一个 Promise 对象&#xff0c;返回值是该 Promise 对象的结果。如果不是 Promise 对象&#xff0c;就会进行自动转换&#xff0c;使用 Promise.resolve()&#xff0c;就直接返回对应的值 await 命令后面的 Promise 对象如果变为 reject…

方法部分 学习

方法是程序中最小的执行单元 方法的定义调用 public static void 方法名&#xff08;&#xff09;{ 方法体 } 写在main方法外面&#xff0c;在main函数里面直接调用带参数&#xff1a;public static void 方法名&#xff08;int num1 &#xff0c; int num2&am…

计算机的错误计算(一百零三)

摘要 探讨 的计算精度问题。 从计算机的错误计算&#xff08;九十九&#xff09;知&#xff0c; 在IEEE 754-2019的列表中&#xff0c;并且定义域是实数域。但是&#xff0c;截止撰写本节内容时&#xff0c;本文作者仍未找到内置了该函数的语言或编译器。 例1. 已知 计算…

Vue3:shallowRef与shallowReactive

目录 一.shallowRef 和 shallowReactive 1.shallowRef 2.shallowReactive 二.ref 和 reactive 1. ref 2. reactive 三.各自使用场景 1.shallowRef 2.shallowReactive 3.ref 4.reactive 四.shallowRef 使用 五.shallowReactive使用 六.效果 一.shallowRef 和 shal…

Redis 分布式缓存服务(集群)

作者&#xff1a;程序那点事儿 日期&#xff1a;2023/11/17 13:05 准备6台虚拟机&#xff0c;ip分别是 192.168.10.101 192.168.10.102 192.168.10.103 192.168.10.104 192.168.10.105 192.168.10.106 创建6个节点 mkdir -p /usr/local/cluster/redis-node1 #对应192.168.10.…

【html网页制作】旅游风景主题网页制作含css动画及js特效(8页面附效果源码)

HTMLCSS旅游风景主题旅游网页制作 &#x1f354;涉及知识&#x1f964;写在前面&#x1f367;一、网页主题&#x1f333;二、网页效果菜单切换效果PageA、整体页Page1、首页Page2、旅行趣事页Page3、旅行美景页Page4、旅行指南页Page5、旅行视频页Page6、留言页Page7、西湖简介…