习惯了微信聊天,利用WebSocket手动实现个聊天功能怎么样?

news/2024/5/19 18:13:54/文章来源:https://blog.csdn.net/yyyy_11119/article/details/121581957

1.背景

基于项目需求,最近需要实现一个简单的聊天功能。日常生活中,大家对于聊天也习以为常,微信、QQ等软件也经常用到,其实我们也可以引入一些第三方的sdk包等去实现,也可以利用WebSocket通信协议去手动实现简单的聊天。本文主要讲述下WebSocket实现的具体步骤及实现的效果图。

2.方案选型及优缺点介绍

  • 方案一 利用http接口手动实现三个接口:sengMsg(消息发送)、receiveMsg(消息接收)、getHistoryMsg(获取历史消息) ,然后前端发送消息时调用sendMsg接口,将数据写入数据库以便获取历史消息使用,接收消息时前端声明一个定时器,每一秒钟去刷新消息接收接口,来获取消息内容显示到聊天框中,最后,如果用户需要翻看历史消息,调用getHistoryMsg接口即可。优点 后端实现简单,且能将聊天消息持久化到数据库永久保存,可以根据聊天室id随时获取消息内容缺点 由于频繁调用接口,服务器和api接口压力比较大,高并发情况下服务器可能会宕机,而且不进行消息发送时,由于定时器的使用,前端频繁请求会造成空跑,显然不太合理
  • 方案二 利用已有的WebSocket服务实现聊天功能优点 不用额外自己实现接口,直接按照WebSocket定义的规则直接套用即可缺点 消息没有持久化,如果服务宕机,可能无法查看历史消息

3.服务搭建及实现

  • 3.1 引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
  • 3.2 声明socket配置类
@Configuration
public class WebSocketConfig {//注入一个ServerEndpointExporter@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}
  • 3.3 声明聊天Controller
/*** 聊天控制器* @ServerEndpoint("/chat/{userId}")中的userId是前端创建会话窗口时当前用户的id,即消息发送者的id*/
@ServerEndpoint("/chat/{userId}")
@Component
public class ChatWebSocketController {private final Logger logger = Logger.getLogger(ChatWebSocketController.class);//onlineCount:在线连接数private static AtomicInteger onlineCount = new AtomicInteger(0);//webSocketSet:用来存放每个客户端对应的MyWebSocket对象。public static List<ChatWebSocketController> webSocketSet = new ArrayList<>();//存放所有连接人信息public static List<String> userList  = new ArrayList<>();//与某个客户端的连接会话,需要通过它来给客户端发送数据private Session session;//用户IDpublic String userId = "";/*** 连接建立成功调用的方法*/@OnOpenpublic void onOpen(Session session, @PathParam("userId") String userId) {this.session = session;this.userId = userId;this.userList.add(userId) ;//加入set中webSocketSet.add(this);//在线数加1onlineCount.incrementAndGet();logger.info("有新连接加入!" + userId + "当前在线用户数为" + onlineCount.get());JSONObject msg = new JSONObject();try {msg.put("msg", "连接成功");msg.put("status", "SUCCESS");msg.put("userId", userId);sendMessage(JSON.toJSONString(msg));} catch (Exception e) {logger.debug("IO异常");}}/*** 连接关闭调用的方法*/@OnClosepublic void onClose(@PathParam("userId") String userId ) {//从set中删除webSocketSet.remove(this);onlineCount.decrementAndGet(); // 在线数减1logger.info("用户"+ userId +"退出聊天!当前在线用户数为" + onlineCount.get());}/*** 收到客户端消息后调用的方法** @param message 客户端发送过来的消息*/@OnMessagepublic  void onMessage(String message, @PathParam("userId") String userId ) {//客户端输入的消息message要经过处理后封装成新的message,后端拿到新的消息后进行数据解析,然后判断是群发还是单发,并调用对应的方法logger.info("来自客户端" + userId + "的消息:" + message);try {MyMessage myMessage = JSON.parseObject(message, MyMessage.class);String messageContent = myMessage.getMessage();//messageContent:真正的消息内容String messageType = myMessage.getMessageType();if("1".equals(messageType)){ //单聊String recUser = myMessage.getUserId();//recUser:消息接收者sendInfo(messageContent,recUser,userId);//messageContent:输入框实际内容 recUser:消息接收者  userId 消息发送者}else{ //群聊sendGroupInfo(messageContent,userId);//messageContent:输入框实际内容 userId 消息发送者}} catch (Exception e) {logger.error("解析失败:{}", e);}}/*** 发生错误时调用的方法** @OnError**/@OnErrorpublic void onError(Throwable error) {logger.debug("Websocket 发生错误");error.printStackTrace();}public synchronized void sendMessage(String message) {this.session.getAsyncRemote().sendText(message);}/*** 单聊* message : 消息内容,输入的实际内容,不是拼接后的内容* recUser : 消息接收者* sendUser : 消息发送者*/public void sendInfo( String message , String recUser,String sendUser) {JSONObject msgObject = new JSONObject();//msgObject 包含发送者信息的消息for (ChatWebSocketController item : webSocketSet) {if (StringUtil.equals(item.userId, recUser)) {logger.info("给用户" + recUser + "传递消息:" + message);//拼接返回的消息,除了输入的实际内容,还要包含发送者信息msgObject.put("message",message);msgObject.put("sendUser",sendUser);item.sendMessage(JSON.toJSONString(msgObject));}}}/*** 群聊* message : 消息内容,输入的实际内容,不是拼接后的内容* sendUser : 消息发送者*/public  void sendGroupInfo(String message,String sendUser) {JSONObject msgObject = new JSONObject();//msgObject 包含发送者信息的消息if (StringUtil.isNotEmpty(webSocketSet)) {for (ChatWebSocketController item : webSocketSet) {if(!StringUtil.equals(item.userId, sendUser)) { //排除给发送者自身回送消息,如果不是自己就回送logger.info("回送消息:" + message);//拼接返回的消息,除了输入的实际内容,还要包含发送者信息msgObject.put("message",message);msgObject.put("sendUser",sendUser);item.sendMessage(JSON.toJSONString(msgObject));}}}}/*** Map/Set的key为自定义对象时,必须重写hashCode和equals。* 关于hashCode和equals的处理,遵循如下规则:* 1)只要重写equals,就必须重写hashCode。* 2)因为Set存储的是不重复的对象,依据hashCode和equals进行判断,所以Set存储的对象必须重写这两个方法。* 3)如果自定义对象做为Map的键,那么必须重写hashCode和equals。** @param o* @return*/@Overridepublic boolean equals(Object o) {if (this == o) {return true;}if (o == null || getClass() != o.getClass()) {return false;}ChatWebSocketController that = (ChatWebSocketController) o;return Objects.equals(session, that.session);}@Overridepublic int hashCode() {return Objects.hash(session);}
}
  • 3.4 声明Controller中的MyMessage实体类
public class MyMessage implements Serializable {private static final long serialVersionUID = 1L;private String userId;private String message;//消息内容private String messageType;//消息类型  1 代表单聊 2 代表群聊public String getUserId() {return userId;}public void setUserId(String userId) {this.userId = userId;}public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}public String getMessageType() {return messageType;}public void setMessageType(String messageType) {this.messageType = messageType;}
}
  • 3.5 声明Controller中的StringUtil工具类
public final class StringUtil {/*** 对象为空** @param object* @return*/public static boolean isEmpty(Object object) {if (object == null) {return true;}if (object instanceof String && "".equals(((String) object).trim())) {return true;}if (object instanceof List && ((List) object).size() == 0) {return true;}if (object instanceof Map && ((Map) object).isEmpty()) {return true;}if (object instanceof CharSequence && ((CharSequence) object).length() == 0) {return true;}if (object instanceof Arrays && (Array.getLength(object) == 0)) {return true;}return false;}/*** 对象不为空** @param object* @return*/public static boolean isNotEmpty(Object object) {return !isEmpty(object);}/*** 查询字符串中某个字符首次出现的位置 从1计数** @param string 字符串* @param c* @return*/public static int strFirstIndex(String c, String string) {Matcher matcher = Pattern.compile(c).matcher(string);if (matcher.find()) {return matcher.start() + 1;} else {return -1;}}/*** 两个对象是否相等** @param obj1* @param obj2* @return*/public static boolean equals(Object obj1, Object obj2) {if (obj1 instanceof String && obj2 instanceof String) {obj1 = ((String) obj1).replace("\\*", "");obj2 = ((String) obj2).replaceAll("\\*", "");if (obj1.equals(obj2) || obj1 == obj2) {return true;}}if (obj1.equals(obj2) || obj1 == obj2) {return true;}return false;}/*** 根据字节截取内容** @param bytes   自定义字节数组* @param content 需要截取的内容* @return*/public static String[] separatorByBytes(double[] bytes, String content) {String[] contentArray = new String[bytes.length];double[] array = new double[bytes.length + 1];array[0] = 0;//复制数组System.arraycopy(bytes, 0, array, 1, bytes.length);for (int i = 0; i < bytes.length; i++) {content = content.substring((int) (array[i] * 2));contentArray[i] = content;}String[] strings = new String[bytes.length];for (int i = 0; i < contentArray.length; i++) {strings[i] = contentArray[i].substring(0, (int) (bytes[i] * 2));}return strings;}/*** 获取指定字符串出现的次数** @param srcText  源字符串* @param findText 要查找的字符串* @return*/public static int appearNumber(String srcText, String findText) {int count = 0;Pattern p = Pattern.compile(findText);Matcher m = p.matcher(srcText);while (m.find()) {count++;}return count;}/*** 将字符串str每隔2个分割存入数组** @param str* @return*/public static String[] setStr(String str) {int m = str.length() / 2;if (m * 2 < str.length()) {m++;}String[] strings = new String[m];int j = 0;for (int i = 0; i < str.length(); i++) {if (i % 2 == 0) {//每隔两个strings[j] = "" + str.charAt(i);} else {strings[j] = strings[j] + str.charAt(i);j++;}}return strings;}/*** 定义一个StringBuffer,利用StringBuffer类中的reverse()方法直接倒序输出* 倒叙字符串** @param s*/public static String reverseString2(String s) {if (s.length() > 0) {StringBuffer buffer = new StringBuffer(s);return buffer.reverse().toString();} else {return "";}}/*** 截取字符串中的所有日期时间** @param str* @return*/public static List<String> dateTimeSubAll(String str) {try {List<String> dateTimeStrList = new ArrayList<>();String regex = "[0-9]{4}[-][0-9]{1,2}[-][0-9]{1,2}[ ][0-9]{1,2}[:][0-9]{1,2}[:][0-9]{1,2}";Pattern pattern = compile(regex);Matcher matcher = pattern.matcher(str);while (matcher.find()) {String group = matcher.group();dateTimeStrList.add(group);}return dateTimeStrList;} catch (Exception e) {e.getMessage();return null;}}/*** 截取字符串中的所有日期** @param str* @return*/public static List<String> dateSubAll(String str) {try {List<String> dateStrList = new ArrayList<>();Pattern pattern = compile("[0-9]{4}[-][0-9]{1,2}[-][0-9]{1,2}");Matcher matcher = pattern.matcher(str);while (matcher.find()) {String group = matcher.group();dateStrList.add(group);}return dateStrList;} catch (Exception e) {e.getMessage();return null;}}/*** 获取随机字符串** @param length* @return*/public static String getRandomString(int length) {String base = "abcdefghijklmnopqrstuvwxyz0123456789";Random random = new Random();StringBuffer sb = new StringBuffer();for (int i = 0; i < length; i++) {int number = random.nextInt(base.length());sb.append(base.charAt(number));}return sb.toString();}
}
  • 3.6 后台声明测试的html页面
<!DOCTYPE HTML>
<html>
<head><title>WebSocket Chat Demo</title>
</head><body><input id="inputContent" type="text" style="width:600px;"/><button onclick="send()">Send</button><button onclick="closeConnection()">Close</button><div id="msg"></div>
</body><script type="text/javascript">var websocket = null;//声明自己搭建的websocket服务if ('WebSocket' in window) {var random = parseInt(Math.random() * 1000000) + "";websocket = new WebSocket("ws://localhost:8005/chat/"+ random);} else {alert('Not support websocket')}//连接发生错误的回调方法websocket.onerror = function() {setMessageInnerHTML("error");};//连接成功建立的回调方法websocket.onopen = function(event) {//setMessageInnerHTML("open");}//接收到消息的回调方法websocket.onmessage = function(event) {setMessageInnerHTML(event.data);}//连接关闭的回调方法websocket.onclose = function() {setMessageInnerHTML("close");}//监听窗口关闭事件,当窗口关闭时关闭对应websocket连接window.onbeforeunload = function() {websocket.close();}//将消息回显在页面上function setMessageInnerHTML(innerHTML) {document.getElementById('msg').innerHTML += innerHTML + '<br/>';}//关闭连接function closeConnection() {websocket.close();}//发送消息function send() {var msg = document.getElementById('inputContent').value;websocket.send(msg);}
</script>
</html>

该类对应的路径如下:

4.启动服务并测试

页面输入ip+端口建立websocket连接并发送一条消息,测试结果如图:

注意

注意

  • 1.正常情况下,输入框中只输入要发送的实际聊天内容即可,比如“在吗老公,急事”,但是为了更容易测试,页面中输入的是拼接后的json消息体,接收者用户id,以及消息类型,实际开发中数据格式让前端处理即可,前端根据输入的内容拼接成如输入框图所示的数据格式即可
  • 2.messageType来区分单聊还是群聊,但是此处的群聊是建立连接的所有websocket服务,没有区分组概念,如果区分的话,后台接口请求路径中要添加上roomId参数,然后建立连接时将进入该聊天室的用户放入一个map集合中,群聊发送消息时,根据不同的roomId,只给该组的用户推送群聊消息即可

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

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

相关文章

【蓝桥杯】第十三届蓝桥杯省赛 AK 攻略 —— C++ B组全真题超详细剖析

&#x1f384;目录&#x1f33c;写在前面&#x1f33b; A题 --- 九进制转十进制&#x1f337; 题目描述&#x1f337; 解题思路&#x1f337; 代码编写&#x1f33b; B题 --- 顺子日期&#x1f337; 题目描述&#x1f337; 解题思路&#x1f337; 代码编写&#x1f33b; C题 --…

92年程序员发帖晒薪资称自己很迷茫,网友:老弟你可以了

当下&#xff0c;是一个“向钱看&#xff0c;向厚赚”的社会。快节奏的生活下&#xff0c;家庭、工作各方面压力很容易使年轻人陷入迷茫和焦虑。 与其他行业相比&#xff0c;程序员的高薪让人羡慕。那么&#xff0c;对于那些真正达到这么多收入的人来说&#xff0c;他们是怎么…

Mysql安装包安装教程(亲测简单高效版)

Mysql安装包安装教程&#xff08;亲测简单高效版&#xff09;安装流程mysql安装SQLyog安装安装流程 mysql安装 1.下载mysql&#xff0c;官方地址&#xff1a;mysql官网 2.解压mysql安装包到任意目录下 3.新建my.ini文件 4.配置my.ini [mysqld] basedirD:\Program Files\J…

sql语法:详解DDL

Mysql版本&#xff1a;8.0.26 可视化客户端&#xff1a;sql yog 目录一、DDL是什么&#xff1f;二、和数据库相关的DDL2.1 创建数据库2.2 删除数据库2.3 查看所有的数据库&#xff0c;当前用户登录后&#xff0c;可以看到哪些数据库2.4 查看某个数据库的详细定义2.5 修改数据库…

Windows系统GIT安装与GitHub远程仓库

文章目录Windows系统GIT安装Git是什么windows环境安装环境变量验证安装GitHub与远程仓库GitHub是什么GitHub账号注册创建本地SSH KeyGitHub接入本地电脑公匙创建个人远程库传送门Windows系统GIT安装 Git是什么 Git&#xff08;读音为/gɪt/&#xff09;是一个开源的分布式版本…

接口测试之Postman使用全指南(原来使用 Postman测试API接口如此简单)

目录 一、Postman背景介绍 二、Postman的操作环境 三、Postman重要提示&#xff1a; 四、什么是接口测试 五、接口测试工具 六、接口测试流程 七、接口测试执行 八、全局变量和环境变量 九、postman接口关联 十、postman动态参数 十一、postman断言 十二、postman用…

Unity --- Transform类

1.一个很有意思的事实是Transform类不仅用来管理游戏物体的位置缩放旋转&#xff0c;还用来管理游戏物体的父物体与子物体之间的关系 当游戏物体A的trasnform类a是游戏物体B的transform类b的父类的话&#xff0c;游戏物体A就是游戏物体B的父物体 2.如何访问脚本当前挂载的游戏…

手把手教你安装VSCode(附带图解步骤)

一、前端工具vscode 1.1、概述 前端开发是创建Web页面或app等前端界面呈现给用户的过程&#xff0c;通过HTML&#xff0c;CSS及JavaScript以及衍生出来的各种技术、框架、解决方案&#xff0c;来实现互联网产品的用户界面交互 [1] 。它从网页制作演变而来&#xff0c;名称上有…

如何用Python对股票数据进行LSTM神经网络和XGboost机器学习预测分析(附源码和详细步骤),学会的小伙伴们说不定就成为炒股专家一夜暴富了

前言 最近调研了一下我做的项目受欢迎程度&#xff0c;大数据分析方向竟然排第一&#xff0c;尤其是这两年受疫情影响&#xff0c;大家都非常担心自家公司裁员或倒闭&#xff0c;都想着有没有其他副业搞搞或者炒炒股、投资点理财产品&#xff0c;未雨绸缪&#xff0c;所以不少…

你单位数字化转型了吗?

写在前面&#xff1a;本文由Bing AI和我一起完成&#xff0c;它完成90%内容&#xff0c;致谢&#xff01; 1.数字化转型 近两年数字化转型在社会面搞得轰轰烈烈&#xff0c;数字化转型是指&#xff0c;利用新一代信息技术&#xff0c;构建数据的采集、传输、存储、处理和反馈的…

抓取某话题下指定时间内的微博数据,包括博文数据、评论信息等(可通过高级搜索筛选时间)

代码有点长&#xff0c;完整代码放在文章最后了。 最后的数据存储为了3个表&#xff0c;表的各字段如下&#xff1a; # csv头部 writer.writerow((话题链接, 话题内容, 楼主ID, 楼主昵称, 楼主性别, 发布日期,发布时间, 转发量, 评论量, 点赞量, 评论者ID, 评论者昵称,评论者…

低代码开发公司:用科技强力开启产业分工新时代!

实现办公自动化&#xff0c;是不少企业的共同追求。低代码开发公司会遵循时代发展规律&#xff0c;注入强劲的科技新生力量&#xff0c;在低代码开发市场厚积爆发、努力奋斗&#xff0c;推动企业数字化转型升级&#xff0c;为每一个企业的办公自动化升级创新贡献应有的力量。 一…

Matlab仿真,数字基带传输系统的设计实验报告

实验目的 1、提高独立学习的能力&#xff1b; 2、培养发现问题、解决问题和分析问题的能力&#xff1b; 3、学习Matlab 的使用&#xff1b; 4、掌握基带数字传输系统的仿真方法&#xff1b; 5、熟悉基带传输系统的基本结构&#xff1b; 6、理解奈奎斯特第一准则&#xff1b; 7…

echarts入门基础教程

目录 效果图 1.下载资源 新建项目 2.引入echarts 3.准备一个呈现图表的盒子 4.初始化echarts实例对象 5.准备配置项 6.将配置项设置给echarts实例对象 7.完整代码 效果图 1.下载资源 新建项目 去官网下载echarts压缩包&#xff0c;在包里的dist文件里找到echarts.min.j…

sql语法:事务的”那些事“

Mysql版本&#xff1a;8.0.26 可视化客户端&#xff1a;sql yog 目录前言一、事务是什么&#xff1f;二、事务的特点三、如何提交事务和回滚事务?3.1 手动提交3.2 自动提交模式下开启事务3.3 注意事项四、事务的隔离级别4.1 模拟事务安全问题4.1.1 脏读问题模拟如下&#xff1…

【模块介绍】6×6矩阵键盘(硬件部分和扫描方式)

目录 概述 原理图 扫描方式 扫描法 单个按键按下 多个按键按下 行反转法 图解 成品 概述 矩阵键盘非常常见 就是利用键盘组成矩阵来减少IO口的使用 做成66的矩阵键盘可以使用12个IO口读取36个按键 矩阵键盘的优势在于成本低&#xff0c;无需其他芯片即可实现功能 …

Android WMS工作原理浅析(一)

WMS(WindowManagerService)相关概念 window:它是一个抽象类&#xff0c;具体实现类为 PhoneWindow &#xff0c;它对 View 进行管理。Window是View的容器&#xff0c;View是Window的具体表现内容&#xff1b; windowManager:是一个接口类&#xff0c;继承自接口 ViewManager &…

Mac上初次使用vite新建Vue3项目需要注意,自己的错误记录

执行npm init vitejs/app时 报错&#xff1a; internal/modules/cjs/loader.js:1089 throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath); 一开始网上找原因&#xff0c;以为是node的版本过低&#xff0c;但是看了是自己的弄的版本号是12.1.x.x刚刚好跨过门槛…

ElasticSearch学习(十一)—— es7.2升级log4j版本

下载log4j2.17 下载地址&#xff1a;Apache Logging Serviceshttps://logging.apache.org/ 查找es安装目录下需要替换的log4j文件 /opt/elk# find . -name log4j* ./elasticsearch-7.2.0/lib/log4j-api-2.11.1.jar ./elasticsearch-7.2.0/lib/log4j-core-2.11.1.jar ./elastics…

【VulnHub靶场】——BEELZEBUB: 1

作者名&#xff1a;Demo不是emo 主页面链接&#xff1a;主页传送门创作初心&#xff1a;对于计算机的学习者来说&#xff0c;初期的学习无疑是最迷茫和难以坚持的&#xff0c;中后期主要是经验和能力的提高&#xff0c;我也刚接触计算机1年&#xff0c;也在不断的探索&#xf…