学习风`宇blog的websocket模块

news2024/12/28 20:20:09

文章目录

    • 后端
      • 代码
        • 引入依赖
        • WebSocketConfig
        • WebSocketServiceImpl
      • 分析
        • tb_chat_record表
        • WebSocketServiceImpl
          • ChatConfigurator
        • 聊天消息
          • ChatTypeEnums
          • WebsocketMessageDTO

后端

代码

引入依赖

仅需引入以下依赖

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

 <!--fastJson-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.7</version>
</dependency>

WebSocketConfig

/**
 * websocket配置类
 *
 * @author yezhiqiu
 * @date 2021/07/29
 */
@Configuration
public class WebSocketConfig {

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

}

WebSocketServiceImpl

/**
 * websocket服务
 *
 * @author yezhiqiu
 * @date 2021/07/28
 */
@Data
@Service
@ServerEndpoint(value = "/websocket", configurator = WebSocketServiceImpl.ChatConfigurator.class)
public class WebSocketServiceImpl {

    /**
     * 用户session
     */
    private Session session;

    /**
     * 用户session集合
     */
    private static CopyOnWriteArraySet<WebSocketServiceImpl> webSocketSet = new CopyOnWriteArraySet<>();

    @Autowired
    public void setChatRecordDao(ChatRecordDao chatRecordDao) {
        WebSocketServiceImpl.chatRecordDao = chatRecordDao;
    }

    @Autowired
    public void setUploadStrategyContext(UploadStrategyContext uploadStrategyContext) {
        WebSocketServiceImpl.uploadStrategyContext = uploadStrategyContext;
    }

    private static ChatRecordDao chatRecordDao;

    private static UploadStrategyContext uploadStrategyContext;

    /**
     * 获取客户端真实ip
     */
    public static class ChatConfigurator extends ServerEndpointConfig.Configurator {

        public static String HEADER_NAME = "X-Real-IP";

        @Override
        public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
            try {
                String firstFoundHeader = request.getHeaders().get(HEADER_NAME.toLowerCase()).get(0);
                sec.getUserProperties().put(HEADER_NAME, firstFoundHeader);
            } catch (Exception e) {
                sec.getUserProperties().put(HEADER_NAME, "未知ip");
            }
        }
    }

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig endpointConfig) throws IOException {
        // 加入连接
        this.session = session;
        webSocketSet.add(this);
        // 更新在线人数
        updateOnlineCount();
        // 加载历史聊天记录
        ChatRecordDTO chatRecordDTO = listChartRecords(endpointConfig);
        // 发送消息
        WebsocketMessageDTO messageDTO = WebsocketMessageDTO.builder()
                .type(HISTORY_RECORD.getType())
                .data(chatRecordDTO)
                .build();
        synchronized (session) {
            session.getBasicRemote().sendText(JSON.toJSONString(messageDTO));
        }
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) throws IOException {
        WebsocketMessageDTO messageDTO = JSON.parseObject(message, WebsocketMessageDTO.class);
        switch (Objects.requireNonNull(getChatType(messageDTO.getType()))) {
            case SEND_MESSAGE:
                // 发送消息
                ChatRecord chatRecord = JSON.parseObject(JSON.toJSONString(messageDTO.getData()), ChatRecord.class);
                // 过滤html标签
                chatRecord.setContent(HTMLUtils.filter(chatRecord.getContent()));
                chatRecordDao.insert(chatRecord);
                messageDTO.setData(chatRecord);
                // 广播消息
                broadcastMessage(messageDTO);
                break;
            case RECALL_MESSAGE:
                // 撤回消息
                RecallMessageDTO recallMessage = JSON.parseObject(JSON.toJSONString(messageDTO.getData()), RecallMessageDTO.class);
                // 删除记录
                chatRecordDao.deleteById(recallMessage.getId());
                // 广播消息
                broadcastMessage(messageDTO);
                break;
            case HEART_BEAT:
                // 心跳消息
                messageDTO.setData("pong");
                session.getBasicRemote().sendText(JSON.toJSONString(JSON.toJSONString(messageDTO)));
            default:
                break;
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() throws IOException {
        // 更新在线人数
        webSocketSet.remove(this);
        updateOnlineCount();
    }

    /**
     * 加载历史聊天记录
     *
     * @param endpointConfig 配置
     * @return 加载历史聊天记录
     */
    private ChatRecordDTO listChartRecords(EndpointConfig endpointConfig) {
        // 获取聊天历史记录
        List<ChatRecord> chatRecordList = chatRecordDao.selectList(new LambdaQueryWrapper<ChatRecord>()
                .ge(ChatRecord::getCreateTime, DateUtil.offsetHour(new Date(), -12)));
        // 获取当前用户ip
        String ipAddress = endpointConfig.getUserProperties().get(ChatConfigurator.HEADER_NAME).toString();
        return ChatRecordDTO.builder()
                .chatRecordList(chatRecordList)
                .ipAddress(ipAddress)
                .ipSource(IpUtils.getIpSource(ipAddress))
                .build();
    }

    /**
     * 更新在线人数
     *
     * @throws IOException io异常
     */
    @Async
    public void updateOnlineCount() throws IOException {
        // 获取当前在线人数
        WebsocketMessageDTO messageDTO = WebsocketMessageDTO.builder()
                .type(ONLINE_COUNT.getType())
                .data(webSocketSet.size())
                .build();
        // 广播消息
        broadcastMessage(messageDTO);
    }

    /**
     * 发送语音
     *
     * @param voiceVO 语音路径
     */
    public void sendVoice(VoiceVO voiceVO) {
        // 上传语音文件
        String content = uploadStrategyContext.executeUploadStrategy(voiceVO.getFile(), FilePathEnum.VOICE.getPath());
        voiceVO.setContent(content);
        // 保存记录
        ChatRecord chatRecord = BeanCopyUtils.copyObject(voiceVO, ChatRecord.class);
        chatRecordDao.insert(chatRecord);
        // 发送消息
        WebsocketMessageDTO messageDTO = WebsocketMessageDTO.builder()
                .type(VOICE_MESSAGE.getType())
                .data(chatRecord)
                .build();
        // 广播消息
        try {
            broadcastMessage(messageDTO);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 广播消息
     *
     * @param messageDTO 消息dto
     * @throws IOException io异常
     */
    private void broadcastMessage(WebsocketMessageDTO messageDTO) throws IOException {
        for (WebSocketServiceImpl webSocketService : webSocketSet) {
            synchronized (webSocketService.session) {
                webSocketService.session.getBasicRemote().sendText(JSON.toJSONString(messageDTO));
            }
        }
    }

}

分析

tb_chat_record表

CREATE TABLE `tb_chat_record` (

	  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
	  `user_id` int(11) DEFAULT NULL COMMENT '用户id',
	  `nickname` varchar(50) NOT NULL COMMENT '昵称',
	  `avatar` varchar(255) NOT NULL COMMENT '头像',
	  
	  `content` varchar(1000) NOT NULL COMMENT '聊天内容',
	  
	  `ip_address` varchar(50) NOT NULL COMMENT 'ip地址',
	  `ip_source` varchar(255) NOT NULL COMMENT 'ip来源',
	  
	  `type` tinyint(4) NOT NULL COMMENT '类型',
	  
	  `create_time` datetime NOT NULL COMMENT '创建时间',
	  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
	  
	  PRIMARY KEY (`id`) USING BTREE
	  
) ENGINE=InnoDB AUTO_INCREMENT=2991 DEFAULT CHARSET=utf8mb4;

WebSocketServiceImpl

  • 使用@ServerEndpoint标记一个websocket服务器端点类,提供该websocket服务端点的连接路径,并可以使用configurator属性指定一个配置器,该配置器可以介入握手过程。

  • 这个类基本上处理了websocket的几乎所有逻辑,每当有一个新的连接进来时,都会创建一个新的WebSocketServiceImpl对象,并且回调@OnOpen标识的方法,@OnOpen方法可以声明Session 和 EndpointConfig 类型参数,Session将会被存储起来,用于后面与客户端进行双向通信。

在这里插入图片描述

ChatConfigurator
  • 在握手(即在modifyHandShake方法中)时,获取客户端的ip,存入ServerEndpointConfig的userProperties属性中。等到在@OnOpen表示的方法中可以声明EndpointConfig参数类型,拿到userProperties,从而拿到存到里面的客户端的ip。

  • 属于ServerEndpointConfig.Configurator类型,可追溯到UpgradeUtil#doUpgrade升级协议时的处理,在Configurator类的modifyHandShake方法中,可以拿到握手请求对象,握手成功之后,@OnOpen方法才会调用执行。

    • 还有一点就是,不能每个客户端想连接websocket服务端的时候,就来连接吧?!参考:【JavaScript】在websocket里面添加Token

      • 至少需要携带一个凭证,放在请求头里面,就可以在这个modifyHandShake方法里做手脚,前端通过let websocket = new WebSocket('ws://localhost:8084/websocket/user001/username001,“eyxxxx-yyyy”),第二个参数就是"Sec-WebSocket-Protocol"请求头,服务端需要返回一摸一样的响应头,并且值也要跟客户端发过来的值一样,websocket才会连接成功,否则,不会建立websocket连接。但是这样把协议头变成了token,不知道合不合适。
      • 还有一些变通的方法,比如:
        1. 可以在ws://…后面拼接查询参数,然后再在modifyHandShake里面校验。
        2. 也可以websocket连接完成后,再让客户端把token发过来,如果token不对,立即断掉websocket连接
public static class ChatConfigurator extends ServerEndpointConfig.Configurator {

    public static String HEADER_NAME = "X-Real-IP";

    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        try {
            String firstFoundHeader = request.getHeaders().get(HEADER_NAME.toLowerCase()).get(0);
            sec.getUserProperties().put(HEADER_NAME, firstFoundHeader);
        } catch (Exception e) {
            sec.getUserProperties().put(HEADER_NAME, "未知ip");
        }
    }
}

聊天消息

ChatTypeEnums

websocket服务端和客户端之间发送的消息内容,使用json格式,它必须先指明消息类型,然后对方得到消息类型后,就能根据该消息类型做相应的处理。
在这里插入图片描述

WebsocketMessageDTO

不管什么消息,都能转为WebsocketMessageDTO类型。浏览器客户端发过来的消息必须是json格式,并且有type标识消息类型,然后data标识消息内容。根据不同的消息类型type,消息内容中的数据会有不同。
在这里插入图片描述

// 其中data是个字符串
WebsocketMessageDTO wsMsgDto = JSON.parseObject("{\"type\":100,\"data\":\"very good~\"}", WebsocketMessageDTO.class);
System.out.println(wsMsgDto.getData()); // very good~ // String类型

// 其中data是个json格式字符串
WebsocketMessageDTO wsMsgDto2 = JSON.parseObject("{\"type\":100,\"data\":{\"name\":\"zzhua\",\"sex\":1}}",WebsocketMessageDTO.class);
System.out.println(wsMsgDto2.getData()); // {"sex":1,"name":"zzhua"} // JSONObject类型, 里面使用map存储了name->zzhua,sex->1

// 其中data是个多层级的json格式字符串
WebsocketMessageDTO wsMsgDto3 = JSON.parseObject("{\"type\":100,\"data\":{\"name\":\"zzhua\",\"sex\":1, \"info\":{\"idcard\":\"430xxx\",\"hobbies\":[\"java\",\"spring\",\"vue\"]}}}",WebsocketMessageDTO.class);
System.out.println(wsMsgDto3.getData());

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

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

相关文章

ACM8629 立体声50W/100W单声道I2S数字输入D类音频功放IC

概述 ACM8629 一款高度集成、高效率的双通道数字输入功放。供电电压范围在4.5V-26.4V,数字接口电源支持3.3V 。在4 欧负载&#xff0c;BTL模式下输出功率可以到250W1%THDN&#xff0c;在2欧负载&#xff0c;PBTL模式下单通道可以输出1100W 1%THDN. ACM8629采用新型PWM脉宽调制架…

全国青少年软件编程(Scratch)等级考试二级考试真题2023年3月——持续更新.....

一、单选题(共25题,共50分) 1. 小猫的程序如图所示,积木块的颜色与球的颜色一致。点击绿旗执行程序后,下列说法正确的是?( ) A.小猫一直在左右移动,嘴里一直说着“抓到了”。 B.小猫会碰到球,然后停止。 C.小猫一直在左右移动,嘴里一直说着“别跑” D.小猫会碰到球,…

2023MatherCup杯三人小队手搓!(C 题 电商物流网络包裹应急调运与结构优化问题)

一个不知名大学生&#xff0c;江湖人称菜狗original author: Jacky LiEmail : 3435673055qq.com Time of completion&#xff1a;2023.4.16 Last edited: 2023.4.16 实际完成时间&#xff1a;2023/4/17 0:52 Mathematical modeling Author: HandSome Wang、BigTall Hu、Jacky L…

一个Email简单高效处理.Net开源库

推荐一个可处理电子邮件消息开源库&#xff0c;可用于消息解析、消息创建、消息修改和消息发送等功能。 项目简介 这是一个基于C#开发的&#xff0c;针对MIME&#xff08;多用途邮件扩展&#xff09;消息创建与解析&#xff0c;该项目简单易用、可用于消息解析、消息创建、消…

【Pytorch】神经网络搭建

在之前我们学习了如何用Pytorch去导入我们的数据和数据集&#xff0c;并且对数据进行预处理。接下来我们就需要学习如何利用Pytorch去构建我们的神经网络了。 目录 基本网络框架Module搭建 卷积层 从conv2d方法了解原理 从Conv2d方法了解使用 池化层 填充层 非线性层 …

Node实现 Socket 通信

socket 通信流程 Socket通信&#xff0c;首先要知道 Socket 是什么&#xff0c;就是网络上的两个程序通过一个双向的通信连接实现数据的交换&#xff0c;这个连接的一端被称为 socket &#xff0c;举一个简单的例子就是两个人在线上进行聊天&#xff0c;即线上通信&#xff0c…

充电桩检测设备TK4860E交流充电桩检定装置

产品特点 充电桩检测设备内置5.28 kW单相交流负载&#xff0c;无需携带额外负载进行测试。 宽动态范围测量技术&#xff0c;避免充电桩输出波动引起的测量风险。 ms级电能刷新速度&#xff0c;减少充电桩与标准仪器在非同步累积电能过程中引入的误差&#xff0c;提高累积电能…

【C++11那些事儿(二)】

文章目录一、新的类功能1.1 默认成员函数1.2 强制生成默认函数的关键字default1.3 禁止生成默认函数的关键字delete二、lambda表达式2.1 语法2.2 捕捉列表说明2.3 函数对象与lambda表达式一、新的类功能 1.1 默认成员函数 原来C类中&#xff0c;有6个默认成员函数&#xff1a…

性能测试总结-根据工作经验总结还比较全面

性能测试总结性能测试理论性能测试的策略基准测试负载测试稳定性测试压力测试并发测试性能测试的指标响应时间并发数吞吐量资源指标性能测试流程性能测试工具JMeter基本使用元件构成线程组jmeter的分布式使用jmeter测试报告常用插件性能测试的计算1.根据请求数明细数据计算满足…

【MySQL】多表查询

文章目录&#x1f389;多表查询&#x1f388;3.1 内连接查询&#x1f388;3.2 外连接查询&#x1f388;3.3 子查询最后说一句&#x1f389;多表查询 &#x1f388;3.1 内连接查询 语法 -- 隐式内连接 SELECT 字段列表 FROM 表1,表2… WHERE 条件;-- 显示内连接 SELECT 字段列…

LeetCode 特训 ---- Week1

目录 LeetCode 特训 --- Week1 两数之和 最长回文子串 删除有序数组中的重复项 删除有序数组中的重复项Ⅱ 删除链表中的重复元素 移动0 旋转链表 分隔链表 快慢指针&#xff08;前后指针&#xff09;用的好&#xff0c;链表&#xff0c;数组起码轻松打十个。 LeetCode…

videoPictureInPicture,视频画中画播放初探

从Chrome 70版本开始video元素开始支持画中画播放&#xff0c;简单写个demo体验一下 简介 在触发画中画之后视频会始终在右下角悬浮&#xff0c;不论是否在当前标签页或者是浏览器是否最小化。 可以在chrome内看任意视频时点击控制条的画中画按钮即可。 API 文档&#xff1…

人工智能机器人技术概述

移动机器人是一种能够在其环境中移动的自主或半自主机器人系统&#xff0c;通常是通过轮子或履带进行移动。这些机器人旨在在各种环境中执行各种任务&#xff0c;包括探索、监视、检查、运输和操作&#xff0c;包括室内和室外空间、危险区域甚至其他星球。 移动机器人配备传感…

日常记录:天梯赛练习集L1-048 矩阵A乘以B

题目&#xff1a; 给定两个矩阵A和B&#xff0c;要求你计算它们的乘积矩阵AB。需要注意的是&#xff0c;只有规模匹配的矩阵才可以相乘。即若A有Ra​行、Ca​列&#xff0c;B有Rb​行、Cb​列&#xff0c;则只有Ca​与Rb​相等时&#xff0c;两个矩阵才能相乘。 输入格式&…

【JAVA-模块四 流程控制语句】

JAVA-模块四 流程控制语句一 选择分支语句&#xff1a;if语句if第一种格式&#xff1a;if第二种格式 双分支&#xff1a;if的第三种格式&#xff1a;多条件分支switch多分支&#xff1a;注意&#xff1a;if语句和swich语句如何选择&#xff1a;二 循环语句&#xff1a;2.1 for循…

聚焦弹性问题,杭州铭师堂的 Serverless 之路

作者&#xff1a;王彬、朱磊、史明伟 得益于互联网的发展&#xff0c;知识的传播有了新的载体&#xff0c;使用在线学习平台的学生规模逐年增长&#xff0c;越来越多学生在线上获取和使用学习资源&#xff0c;其中教育科技企业是比较独特的存在&#xff0c;他们担当的不仅仅是…

Mars3D集成到ruoyi管理系统

尽管Mars3d的官网上提供了详尽的文档和API参考手册&#xff0c;但是在集成至ruoyi后天管理系统中时&#xff0c;还是碰到了不少问题&#xff1a; npm安装方式&#xff0c;若只安装mars3d&#xff0c;会提示找不到mars3d-cesium引用cesium相关库的时候&#xff0c;报404错误 这…

MongoDB基础学习总结及SpringBoot项目中的整合

前言 MongoDB 如今是最流行的 NoSQL 数据库之一&#xff0c;被广泛应用于各行各业中&#xff0c;很多创业公司数据库选型就直接使用了 MongoDB。MongoDB一经推出就受到了广大社区的热爱&#xff0c;可以说是对程序员最友好的一种数据库之一&#xff0c;下面主要是笔者在平常的…

大学刚毕业,用10000小时,走进字节跳动拿了offer

前言&#xff1a; 没有绝对的天才&#xff0c;只有持续不断的付出。对于我们每一个平凡人来说&#xff0c;改变命运只能依靠努力幸运&#xff0c;但如果你不够幸运&#xff0c;那就只能拉高努力的占比。 2020年7月&#xff0c;我有幸成为了字节跳动的一名测试开发&#xff0c…

IO流、多线程

FileInputStream FileOutputStream 原理&#xff1a; //1、创建一个FileOutputStream对象&#xff0c;构造方法中写入数据的目的地 FileOutStream fos new FileOutputStream("C:\\a.txt"); //2、调用FileOutputStream对象中的方法write&#xff0c;把数据写入文件中…