1.背景

基于项目需求,最近需要实现一个简单的聊天功能。日常生活中,大家对于聊天也习以为常,微信、QQ等软件也经常用到,其实我们也可以引入一些第三方的sdk包等去实现,也可以利用WebSocket通信协议去手动实现简单的聊天。本文主要讲述下WebSocket实现的具基于项目需求,最近需要实现一个简单的聊天功能。日常生活中,大家对于聊天也习以为常,微信、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,只给该组的用户推送群聊消息即可

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

  1. html5 websocket java 聊天室_如何利用WebSocket实现网页版聊天室

    花了将近一周的时间终于完成了利用WebSocket完成网页版聊天室这个小demo,期间还走过了一段"看似弯曲"的道路,但是我想其实也不算是弯路吧,因为你走过的路必将留下你的足迹.这 ...

  2. html5利用websocket完成的推送功能(tomcat)

    html5利用websocket完成的推送功能(tomcat) 利用websocket和java完成的消息推送功能,服务器用的是tomcat7.0.42,一些东西是自己琢磨的,也不知道恰不恰当,不恰当 ...

  3. JAVA利用websocket实现多人聊天室、私信(附源码)

    声明:此文为原创,转载请声明出处! 小编曾在毕业设计中用到了聊天室这个功能,现在稍作整理分享一下,希望能对大家有所帮助,有不足之处请指出 在学习websocket前,首先得知道它的一些基本操作,可参考 ...

  4. 微信小程序+webSocket一对一客服聊天

    直接上代码 chat.wxml <!--pages/chat/chat.wxml--> <view class="chat-all"><block w ...

  5. html5利用websocket完成的推送功能

    程序简单来说,就是客户A可以发送消息给客户B,但有很多可以扩展的地方, 比如 1.如果加入数据库后,A发消息时客户B未上线,服务端将会把消息存在数据库中,等客户B上线后,在将消息取出发送给客户B 2. ...

  6. 利用Websocket实现网页调用打印功能

    https://blog.csdn.net/janwool/article/details/78540727 康虎云打印不错

  7. springmvc(18)使用WebSocket 和 STOMP 实现消息功能

    [0]README 1)本文旨在 介绍如何 利用 WebSocket 和 STOMP 实现消息功能: 2)要知道, WebSocket 是发送和接收消息的 底层API,而SockJS 是在 WebSo ...

  8. 微信小程序 WebSocket 通信 —— 在线聊天

    在Node栏目就讲到了Socket通信的内容,使用Node实现Socke通信,还使用两个流行的WebSocket 库,ws 和 socket.io,在小程序中的WebSocket接口和HTML5的We ...

  9. 微信小程序websocket聊天前端实现

    微信小程序websocket聊天前端实现,可以发语音.图片.文字. 代码下载:https://download.csdn.net/download/cc1314_/10983195

最新文章

  1. 0/0型极限等于多少_求极限时是否可以进行代入?
  2. 【金三银四】java是世界上最好的语言
  3. [改善Java代码]覆写equals方法必须覆写hashCode方法
  4. spring入门-----spring中遍历各种集合
  5. 微信小程序自定义弹窗实例
  6. 蓝桥杯2015初赛-加法变乘法-枚举
  7. 用户体验可视化指南pdf_R中增强可视化的初学者指南
  8. 《从零开始学Swift》学习笔记(Day 14)——字符串的插入、删除和替换
  9. 计算机视觉 | 面试题:26、LBP算法原理
  10. Linux学习笔记(更新中)
  11. 行业专家揭秘家装五大主材底价
  12. 寻找百度图片搜索接口历程--one
  13. 不用做实验也能发论文——带你看懂元分析
  14. quartz问题记录-missed their scheduled fire-time
  15. 【网络知识】Wireshark抓不到vlan tag问题解决
  16. 互联网B端产品设计经验总结
  17. PDF转图片哪个格式最清晰?PDF转高清图片的方法
  18. Photoshop学习(二十):画面的浮雕效果
  19. 使用mockplus设计大屏原型
  20. XXX XXX Company introduction个人特点英语描述

热门文章

  1. 【Angular 4.0】在线竞拍网站开发
  2. 22-基于单片机的LED点阵滚动显示系统
  3. GPIO 端口模式寄存器 (GPIOx_MODER)之寄存器使用解析
  4. 去除网页广告--浏览器插件
  5. python教程print_Python入门
  6. 安装win10时关闭计算机,Win10怎么关闭“安装更新并关机”选项
  7. 单场收入过万,语音直播会是内容创业富矿吗?
  8. python(server) javascript(client) 做websocket
  9. Jony Ive升任苹果首席设计官,该职位首次设立
  10. macos安全性偏好设置_如何更改macOS系统偏好设置的布局