利用websocket +定时器简易的实现一个网络聊天室

其实原理非常简单,就是客户端用户通过websoket来连接websocket服务端。然后服务端,收集每个用户发出的消息, 进而将每条用户的消息通过广播的形式推送到每个连接到服务端的客户端。从而实现用户的实时聊天。

// TODO : 我主要是讲一下实现思路。并未完善其功能。

1.后端

依赖

<!--websocket-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-websocket</artifactId>
		</dependency>
		<!--huttol-->
		<dependency>
			<groupId>cn.hutool</groupId>
			<artifactId>hutool-all</artifactId>
			<version>5.8.11</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>

 webSocket配置类

@Configuration
public class WebSocketConfig
{

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

 WebSocket类

类似于controller接口,只不过这个接口,用来专门处理websoket相关的。

package com.example.websocketdemo.websocket;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.example.websocketdemo.domain.User;
import com.example.websocketdemo.domain.UserMes;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;

@Component
//定义websocket服务器端,它的功能主要是将目前的类定义成一个websocket服务器端。注解的值将被用于监听用户连接的终端访问URL地址
@ServerEndpoint("/websocket")
@Slf4j
public class WebSocket {
 
    //实例一个session,这个session是websocket的session
    private Session session;

    private User user; // 每个websocket连接对应的用户信息
 
    //存放websocket的集合(本次demo不会用到,聊天室的demo会用到)
    private static CopyOnWriteArraySet<WebSocket> webSocketSet = new CopyOnWriteArraySet<>();
    // 用户服务器数据存储结构体
    private static List<UserMes> userMess = new ArrayList<>();

    public List<UserMes> getUserMess(){
        return userMess;
    }
 
    //前端请求时一个websocket时
    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        webSocketSet.add(this);
        log.info("【websocket消息】有新的连接, 总数:{}", webSocketSet.size());
    }


    //前端关闭时一个websocket时
    @OnClose
    public void onClose() {
        webSocketSet.remove(this);
        log.info("【websocket消息】连接断开, 总数:{}", webSocketSet.size());
    }


    //前端向后端发送消息
    @OnMessage
    public void onMessage(String message) {
        if(isUserStr(message)){
            this.user = userStrConvertUser(message);
            log.info("【websocket消息】客户端发来的连接请求:{}", message);
            return;
        }
        userMess.add(new UserMes(user.getName(),message));
        log.info("【websocket消息】收到客户端发来的消息:{}", message);
    }

    // 判断消息中是否包含用户信息的json字符串
    private boolean isUserStr(String mes){
        JSONObject response;
       try
       {
          response   = JSONUtil.parseObj(mes);
          if(response.containsKey("name") && response.containsKey("age"))
          {
              return true;
          }
          return false;
       }catch (Exception e){
           return false;
       }
    }

    // 将包装用户信息的json字符串转化为用户对象
    private User userStrConvertUser(String mes){
        JSONObject res = JSONUtil.parseObj(mes);
        String name = res.getStr("name");
        Integer age = res.getInt("age");
        return new User(name,age);
    }
 
    //新增一个方法用于主动向客户端发送消息
    public void sendMessage(String message) {
        for (WebSocket webSocket: webSocketSet) {
            log.info("【websocket消息】广播消息, message={}", message);
            try {
                webSocket.session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public List<User> getUserList()
    {
        ArrayList<User> users = new ArrayList<>();
        for (WebSocket webSocket : webSocketSet)
        {
          users.add(webSocket.user);
        }
        return users;
    }

    //新增一个方法用于主动向客户端发送消息
    // 卧槽消息推送方法

}

WebSocketTasks

利用定时器,实现服务端向客户端消息的推送。

websocket定时器,负责处理将客户端传入服务的消息,整合推送到相应的客户端。

package com.example.websocketdemo.tasks;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.example.websocketdemo.domain.User;
import com.example.websocketdemo.domain.UserMes;
import com.example.websocketdemo.websocket.WebSocket;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;


import javax.annotation.Resource;
import java.util.List;

/**
 * websocket定时器
 * 
 * @author: jzm
 * @date: 2024-03-05 20:02
 **/

@Component
public class WebSocketTasks
{
    @Resource
    private WebSocket webSocket;

    // 每隔10s定时推送当前用户在线人数
    @Scheduled(cron = "0/10 * * * * ?")
    public void sendOlineUserInfo() throws InterruptedException
    {
        WebSocket ws = webSocket;
        List<User> userList = ws.getUserList();
        JSONObject res = new JSONObject();
        res.set("size",userList.size());
        res.set("users",userList);
        webSocket.sendMessage(JSONUtil.toJsonStr(res));
    }

    @Scheduled(cron = "0/10 * * * * ?")
    public void sendUserList() throws InterruptedException
    {
        WebSocket ws = webSocket;
        List<UserMes> userMess = ws.getUserMess();
        ws.sendMessage(JSONUtil.toJsonStr(userMess));
    }

}

 设计到的用户实体类 和其他配置类

/**
 * 用户
 * 
 * @author: jzm
 * @date: 2024-03-06 08:11
 **/

@Data
@AllArgsConstructor
public class User
{
    private String name;
    private Integer age;
}
/**
 * 用户消息
 * 
 * @author: jzm
 * @date: 2024-03-06 08:34
 **/

@Data
@AllArgsConstructor
public class UserMes
{
    private String username;
    private String message;
}

我是利用vue.js搭建的前端工程,是2个服务端口。会有跨域的影响。 

还有就是我服务端口是: 8089

@Configuration
public class WebMvcConfig implements WebMvcConfigurer
{



    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 设置允许跨域的路径
        registry.addMapping("/**")
                // 设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                // 是否允许cookie
                .allowCredentials(true)
                // 设置允许的请求方式
                .allowedMethods("*")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);
    }


}

2.前端

Index.vue

主要是这个Index.vue。用element-ui做ui。参考下面衔接,按照官方文档自扃安装以下。另外的我的vue版本是vue2.x的。

 参考: 组件 | Element

<template>
	<div class="index">
		<div class="box" style="border: 1px solid black">
			<el-card class="box-card">
				<div slot="header" class="clearfix">
					<h2 style="text-align: center">聊天室首页</h2>
				</div>
				<div class="box-main">
					<div
						class="box-main-line clearfix"
						v-for="(item, index) in userMess"
						:key="index"
					>
						<span class="avatar" :style="messageStyle(item)">
							<i class="el-icon-user-solid" style="font-size: 20px"></i>
							<h5>{{ item.username }}</h5>
						</span>
						<span class="message" :style="messageStyle(item)">{{ item.message }} </span>
					</div>
				</div>
				<br />
				<div class="box-input">
					<el-input placeholder="请输入内容" v-model="mes" @keyup.enter.native="sendMe">
						<template slot="prepend">
							<el-button type="info" round @click="sendMe">发送</el-button>
						</template>
					</el-input>
				</div>
			</el-card>
		</div>
		<!-- 一开始弹出表单 -->
		<el-dialog
			title="请输入您的信息"
			:visible.sync="isShowUserPage"
			:before-close="checkUser"
			width="30%"
			style="padding: 0 10px"
		>
			<el-form :model="user" status-icon label-width="100px">
				<el-form-item label="用户名" prop="pass">
					<el-input type="name" v-model="user.name" autocomplete="off"></el-input>
				</el-form-item>
				<el-form-item label="年龄" prop="age">
					<el-input v-model.number="user.age"></el-input>
				</el-form-item>
			</el-form>
			<span slot="footer" class="dialog-footer">
				<el-button type="primary" @click="configUserPage">确 定</el-button>
			</span>
		</el-dialog>
		<!-- 表格展示页 -->
		<template>
			<el-table :data="users" style="width: 100%">
				<el-table-column prop="name" label="用户名" width="180"> </el-table-column>
				<el-table-column prop="age" label="年龄" width="180"> </el-table-column>
			</el-table>
		</template>
	</div>
</template>

<script>
export default {
	name: "FrontIndex",
	data() {
		return {
			mes: "",
			websocket: null,
			isShowUserPage: false,
			user: {
				name: "",
				age: null,
			},
			users: [],
			// 用户信息列表
			userMess: [],
		}
	},
	mounted() {
		// TODO
		this.configUserPage()
	},

	methods: {
		messageStyle(item) {
			return {
				float: item.username == this.user.name ? "right" : "left",
				textAlign: item.username == this.user.name ? "right" : "left",
			}
		},
		sendMe() {
			let websocket = this.websocket
			let mes = this.mes
			if (mes == "") {
				this.$message.warning("不能发送空消息")
				return
			}
			websocket.send(this.mes)
			this.mes = ""
		},
		// 连接服务器
		connectServer() {
			this.websocket = new WebSocket("ws://localhost:8089/websocket")
			this.handWebSocketCallback()
		},
		// 处理websocket 连接回调函数
		handWebSocketCallback() {
			let websocket = this.websocket
			websocket.addEventListener("open", (e) => {
				this.$message.success("用户连接成功!")
				websocket.send(JSON.stringify(this.user))
				this.isShowUserPage = false
			})

			// 监听服务器消息
			websocket.addEventListener("message", (e) => {
				let mes = e.data
				let obj = JSON.parse(mes)
				if (this.checkUsersMessage(mes)) {
					this.users = obj.users
				} else {
					this.userMess = obj
				}
			})
		},
		// 校验这个服务器消息是不是用户列表消息
		checkUsersMessage(mes) {
			let obj = JSON.parse(mes)
			if (obj.users != undefined) {
				return true
			}
			return false
		},
		// 确定、错误输入都是校验这个
		configUserPage() {
			let end = this.checkUser()
			if (end) {
				this.connectServer()
			}
		},
		checkUser() {
			let user = this.user
			if (user.name == "") {
				this.$message.error("用户名不能为空")
				this.isShowUserPage = true
				return false
			}
			if (user.age == null) {
				this.$message.error("年龄不能为空")
				this.isShowUserPage = true
				return false
			}
			return true
		},
	},
}
</script>
<style>
.clearfix:before,
.clearfix:after {
	display: table;
	content: "";
}
.clearfix:after {
	clear: both;
}

.index {
	width: 600px;
	margin: 10px auto;
}
.box-main {
	height: 200px;
	border: 1px solid black;
	overflow-y: scroll;
}

.box-input {
	width: 500px;
	height: 100px;
	margin: 10px auto;
}

/* 隐藏滚动条,保留滚动功能 */

/* 隐藏滚动条本身 */
.box-main::-webkit-scrollbar {
	width: 0;
	height: 0;
}

/* 为了保留滚动功能,使用伪元素来模拟滚动条 */
.box-main::-webkit-scrollbar-track {
	box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
}

.box-main::-webkit-scrollbar-thumb {
	background-color: #888;
}
.box-main-line {
	margin: 10xp 0 0 0;
}
.box-main-line .avatar {
	display: inline-block;
	width: 50px;
	height: 50px;
	border: 1px solid black;
	border-radius: 50%;
	text-align: center;
}
.box-main-line .message {
	display: inline-block;
	width: 88%;
	padding: 15px 0;
	margin: 0 0 0 10px;
	box-shadow: rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, rgba(0, 0, 0, 0.3) 0px 30px 60px -30px,
		rgba(10, 37, 64, 0.35) 0px -2px 6px 0px inset;
}
</style>

3.测试

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

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

相关文章

2024年【电工(初级)】考试内容及电工(初级)考试报名

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 电工&#xff08;初级&#xff09;考试内容根据新电工&#xff08;初级&#xff09;考试大纲要求&#xff0c;安全生产模拟考试一点通将电工&#xff08;初级&#xff09;模拟考试试题进行汇编&#xff0c;组成一套电…

线程的魔法:揭开现代操作系统并发执行的面纱

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

elasticsearch篇:RestClient操作

1. RestClient ES官方提供了各种不同语言的客户端&#xff0c;用来操作ES。这些客户端的本质就是组装DSL语句&#xff0c;通过http请求发送给ES。官方文档地址&#xff1a;Elasticsearch Clients | Elastic 其中的Java Rest Client又包括两种&#xff1a; Java Low Level Res…

解读BOT攻击,探索灵活高效的防护之道

回顾早期的互联网应用&#xff0c;由于业务流量比较小&#xff0c;往往单台服务器就能满足负载需求。随着互联网的流量越来越大&#xff0c;单服务器已经不能满足业务需求&#xff0c;无论它优化得再好&#xff0c;都较难承受大量的访问压力。支持负载均衡的技术很多&#xff0…

openssl3.2 - exp - 选择最好的内建椭圆曲线

文章目录 openssl3.2 - exp - 选择最好的内建椭圆曲线概述笔记将 openssl ecparam -list_curves 实现迁移到自己的demo工程备注END openssl3.2 - exp - 选择最好的内建椭圆曲线 概述 在openssl中使用椭圆曲线, 只允许选择椭圆曲线的名字, 无法给定椭圆曲线的位数. 估计每种椭…

扩展学习|系统理解数字经济

文献来源&#xff1a;[1]肖静华,胡杨颂,吴瑶.成长品&#xff1a;数据驱动的企业与用户互动创新案例研究[J].管理世界,2020,36(03):183-205.DOI:10.19744/j.cnki.11-1235/f.2020.0041. [2]陈晓红,李杨扬,宋丽洁等.数字经济理论体系与研究展望[J].管理世界,2022,38(02):208-22413…

浅谈JUC的理解(含JUC知识体系图)

浅谈JUC的理解 一、前言感悟二、并发知识三、一年前回答四、补充体系回答五、补充层次回答六、碎碎念 本文除了说技术&#xff0c;更多的是在一个两年多开发经验的程序员视角下&#xff0c;记录下自己探索到的世界。 如有不妥之处&#xff0c;还请指正。共勉。 一、前言感悟 当…

力扣hot100:239.滑动窗口最大值(优先队列/单调队列)

本题是一个经典的单调队列题。不过用优先队列也能解决。 一、优先队列 在使用优先队列时&#xff0c;我们会遇到这样的问题&#xff1a;如何将一个目标数从优先队列中弹出&#xff1f;如果使用stl这是办不到的&#xff0c;虽然可以自行实现这样的功能。但是我们可以这样思考&am…

什么是GoogLeNet,亮点是什么,为什么是这个结构?

GooLeNet 亮点 最明显的亮点就是引入了Inception&#xff0c;初衷是多卷积核增加特征的多样性&#xff0c;提高泛化能力 &#xff0c;比如&#xff0c;最下边是一个输入层&#xff0c;然后这个输入分别传递给1*1&#xff0c;3 * 3 &#xff0c;5 * 5和一个最大池化层&#xff…

IP数据报格式

每一行都由32位比特&#xff0c;即4个字节组成&#xff0c;每个格子称为字段或者域。IP数据报由20字节的固定部分和最大40字节的可变部分组成。 总长度 总长度为16个比特&#xff0c;该字段的取值以字节为单位&#xff0c;用来表示IPv4数据报的长度(首部长度数据载荷长度)最大…

Long-term Correlation Tracking LCT 目标跟踪算法源码运行

资源 LCT-tracker项目地址VLFeat官网OpenCV下载地址OTB50数据集百度网盘资源 参考博客 一步一步教你跑lct-tracker&#xff08;Win10Matlab 2016bVisual Studio 2015&#xff09;LCT代码跑起来先文章思路总结 正文 1. 环境配置 我的环境&#xff1a;Win11、Visual Studio…

python+realsense

单目相机(RGB影像):分辨率&#xff1a;320180,320240,424240,640360,640480,848480,960540,1280720,19201080&#xff1b;帧率&#xff1a;6,15,30,60 按照博文Python实战之Realsense_realsense python-CSDN博客的代码显示如下&#xff08;我更改了分辨率和帧率&#xff0c;大…

设计模式:观察者模式 ⑧

一、思想 观察者模式是一种常见的设计模式&#xff0c;也称作发布-订阅模式。它主要解决了对象之间的通知依赖关系问题。在这种模式中&#xff0c;一个对象&#xff08;称作Subject&#xff09;维护着一个对象列表&#xff0c;这些对象&#xff08;称作Observers&#xff09;都…

css3中nth-child属性作用及用法剖析

hello宝子们...我们是艾斯视觉擅长ui设计和前端开发10年经验!希望我的分享能帮助到您!如需帮助可以评论关注私信我们一起探讨!致敬感谢感恩! 标题&#xff1a;CSS3中nth-child属性作用及用法剖析 摘要&#xff1a;CSS3中的nth-child选择器允许我们根据元素位置来定位特定的元素…

Vue3中Vue Router的使用区别

在 Vue 3 中&#xff0c;useRouter 和 useRoute 是两个用于 Vue Router 的 Composition API 函数&#xff0c;它们的用途和返回的对象不同&#xff0c;接下来详细了解一下它们的区别以及如何正确使用它们。 useRouter useRouter 用于获取 router 实例&#xff0c;这个实例提供…

python(5)之处理数组

上次代码结果如下&#xff1a; 1、处理数组的缺失值 1、isnan&#xff08;&#xff09;函数 isnan&#xff08;&#xff09;函数是Numpy模块里的一个可以标记数组中缺失值的位置 代码示例如下&#xff1a; import numpy as np ac np.array([1,2,3,2,3,4,5,9,np.nan,1]) p…

OSPF收发报文实验简述

1、OSPF采用组播形式收发报文&#xff0c;这样可以减少对其它不运行OSPF路由器的影响。 通过wireshark软件对r2 e0/0/0 端口进行数据抓包&#xff0c;发现224.0.0.5为组播地址&#xff0c;如下图

深入了解二叉搜索树:原理、实现与应用

目录 一、介绍二叉搜索树 二、二叉搜索树的基本性质 三、二叉搜索树的实现 四、总结 在计算机科学中&#xff0c;数据结构是构建算法和程序的基础。其中&#xff0c;二叉搜索树&#xff08;Binary Search Tree&#xff0c;简称 BST&#xff09;作为一种常见的数据结构&#…

力扣图论篇

以下思路来自代码随想录以及官方题解。 文章目录 797.所有可能的路径200.岛屿数量130.被围绕的区域1020.飞地的数量 797.所有可能的路径 给你一个有 n 个节点的 有向无环图&#xff08;DAG&#xff09;&#xff0c;请你找出所有从节点 0 到节点 n-1 的路径并输出&#xff08;不…

基于PySide2实现调用本地摄像头抓拍并保存照片(Python版本)

因为横向课题需要&#xff0c;这是其中的一个小小的功能&#xff0c;单独拎出来作为一个小demo&#xff0c;方便后续学习使用 项目实现功能&#xff1a; 点击open按钮&#xff0c;摄像头开启&#xff0c;实时捕获周围图像并显示 点击capture按钮&#xff0c;保存摄像头照片&am…