博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
WebSocket+Java 私聊、群聊实例
阅读量:5245 次
发布时间:2019-06-14

本文共 22998 字,大约阅读时间需要 76 分钟。

   前言

  之前写毕业设计的时候就想加上聊天系统,当时已经用ajax长轮询实现了一个(还不懂什么是轮询机制的,猛戳这里:),但由于种种原因没有加到毕设里面。后来回校答辩后研究了一下websocket,并参照网上资料写了一个简单的聊天,现在又重新整理并记录下来。

  以下介绍来自维基百科:
  WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
  这里可以看一下官网介绍:
  官网里面介绍非常详细,我就不做搬运工了,要是有像我一样英语不好的同学,右键->翻译成简体中文
  spring对websocket的支持:
  这里有一份spring对websocket的详细介绍:

  四个大章节,内容很多,就不一一展开介绍了

  

 

  效果

  2019/04/30补充:我们这个登录、登出非常简单,就一个请求地址,连页面都没有,所以刚开始我就没有贴出来,导致博客文章阅读起来比较吃力,现在在这里补充一下(由于项目后面有所改动,请求地址少了 springboot/,不过大家看得懂就行),这里只是一个小demo,所有就怎么简单怎么来

  登录 http://localhost:10086/websocket/login/huanzi,

  登出 http://localhost:10086/websocket/logout/huanzi

    

  上下线有提示

 

 

  如果这时候发送消息给离线的人,则会收到系统提示消息

 

  群聊

  本例中,点击自己是群聊窗口

  huanzi一发送群聊,laowang跟xiaofang都不是在当前群聊窗口,出现小圆点+1

 

  

  huanzi一发送群聊,xiaofang在当前群聊窗口,直接追加消息,老王不在对应的聊天窗口,出现小圆点+1

 

  xiaofang回复,huanzi直接追加消息,laowang依旧小圆点+1

 

  laowang点击群聊窗口,小圆点消失,追加群聊消息

 

  laowang参与群聊

 

  xiaofang切出群聊窗口,laowang在群聊发送消息,xiaofang出现小圆点+1

 

  切回来,小圆点消失,聊天数据正常接收追加

 

  三方正常参与聊天

 

  私聊

  huanzis私聊xiaofang,xiaofang聊天窗口在群聊,小圆点+1,而laowang不受影响

 

   xiaofang切到私聊窗口,小圆点消失,数据正常追加;huanzi刚好处于私聊窗口,数据直接追加

 

   效果演示到此结束,下面贴出代码

 

  代码编写

  首先先介绍一下项目结构

 

  maven

org.springframework.boot
spring-boot-starter-websocket
org.springframework.boot
spring-boot-starter-thymeleaf

  配置文件

#修改thymeleaf访问根路径spring.thymeleaf.prefix=classpath:/view/

 

socketChart.css样式
body{
background-color: #efebdc;}#hz-main{
width: 700px; height: 500px; background-color: red; margin: 0 auto;}#hz-message{
width: 500px; height: 500px; float: left; background-color: #B5B5B5;}#hz-message-body{
width: 460px; height: 340px; background-color: #E0C4DA; padding: 10px 20px; overflow:auto;}#hz-message-input{
width: 500px; height: 99px; background-color: white; overflow:auto;}#hz-group{
width: 200px; height: 500px; background-color: rosybrown; float: right;}.hz-message-list{
min-height: 30px; margin: 10px 0;}.hz-message-list-text{
padding: 7px 13px; border-radius: 15px; width: auto; max-width: 85%; display: inline-block;}.hz-message-list-username{
margin: 0;}.hz-group-body{
overflow:auto;}.hz-group-list{
padding: 10px;}.left{
float: left; color: #595a5a; background-color: #ebebeb;}.right{
float: right; color: #f7f8f8; background-color: #919292;}.hz-badge{
width: 20px; height: 20px; background-color: #FF5722; border-radius: 50%; float: right; color: white; text-align: center; line-height: 20px; font-weight: bold; opacity: 0;}

 

  

socketChart.html页面
聊天页面
正在与
聊天

登录用户:
请登录
在线人数:
0

 

  

socketChart.js 逻辑代码
//消息对象数组    var msgObjArr = new Array();    var websocket = null;    //判断当前浏览器是否支持WebSocket, springboot是项目名    if ('WebSocket' in window) {        websocket = new WebSocket("ws://localhost:10086/springboot/websocket/"+username);    } else {        console.error("不支持WebSocket");    }    //连接发生错误的回调方法    websocket.onerror = function (e) {        console.error("WebSocket连接发生错误");    };    //连接成功建立的回调方法    websocket.onopen = function () {        //获取所有在线用户        $.ajax({            type: 'post',            url: ctx + "/websocket/getOnlineList",            contentType: 'application/json;charset=utf-8',            dataType: 'json',            data: {username:username},            success: function (data) {                if (data.length) {                    //列表                    for (var i = 0; i < data.length; i++) {                        var userName = data[i];                        $("#hz-group-body").append("
" + userName + "
[在线]
0
"); } //在线人数 $("#onlineCount").text(data.length); } }, error: function (xhr, status, error) { console.log("ajax错误!"); } }); } //接收到消息的回调方法 websocket.onmessage = function (event) { var messageJson = eval("(" + event.data + ")"); //普通消息(私聊) if (messageJson.type == "1") { //来源用户 var srcUser = messageJson.srcUser; //目标用户 var tarUser = messageJson.tarUser; //消息 var message = messageJson.message; //最加聊天数据 setMessageInnerHTML(srcUser.username,srcUser.username, message); } //普通消息(群聊) if (messageJson.type == "2"){ //来源用户 var srcUser = messageJson.srcUser; //目标用户 var tarUser = messageJson.tarUser; //消息 var message = messageJson.message; //最加聊天数据 setMessageInnerHTML(username,tarUser.username, message); } //对方不在线 if (messageJson.type == "0"){ //消息 var message = messageJson.message; $("#hz-message-body").append( "
" + "
" + "
" + message + "" + "
" + "
"); } //在线人数 if (messageJson.type == "onlineCount") { //取出username var onlineCount = messageJson.onlineCount; var userName = messageJson.username; var oldOnlineCount = $("#onlineCount").text(); //新旧在线人数对比 if (oldOnlineCount < onlineCount) { if($("#" + userName + "-status").length > 0){ $("#" + userName + "-status").text("[在线]"); }else{ $("#hz-group-body").append("
" + userName + "
[在线]
0
"); } } else { //有人下线 $("#" + userName + "-status").text("[离线]"); } $("#onlineCount").text(onlineCount); } } //连接关闭的回调方法 websocket.onclose = function () { //alert("WebSocket连接关闭"); } //将消息显示在对应聊天窗口 对于接收消息来说这里的toUserName就是来源用户,对于发送来说则相反 function setMessageInnerHTML(srcUserName,msgUserName, message) { //判断 var childrens = $("#hz-group-body").children(".hz-group-list"); var isExist = false; for (var i = 0; i < childrens.length; i++) { var text = $(childrens[i]).find(".hz-group-list-username").text(); if (text == srcUserName) { isExist = true; break; } } if (!isExist) { //追加聊天对象 msgObjArr.push({ toUserName: srcUserName, message: [{username: msgUserName, message: message, date: NowTime()}]//封装数据 }); $("#hz-group-body").append("
" + srcUserName + "
[在线]
0
"); } else { //取出对象 var isExist = false; for (var i = 0; i < msgObjArr.length; i++) { var obj = msgObjArr[i]; if (obj.toUserName == srcUserName) { //保存最新数据 obj.message.push({username: msgUserName, message: message, date: NowTime()}); isExist = true; break; } } if (!isExist) { //追加聊天对象 msgObjArr.push({ toUserName: srcUserName, message: [{username: msgUserName, message: message, date: NowTime()}]//封装数据 }); } } // 对于接收消息来说这里的toUserName就是来源用户,对于发送来说则相反 var username = $("#toUserName").text(); //刚好打开的是对应的聊天页面 if (srcUserName == username) { $("#hz-message-body").append( "
" + "

"+msgUserName+":

" + "
" + "
" + message + "" + "
" + "
" + "
"); } else { //小圆点++ var conut = $("#hz-badge-" + srcUserName).text(); $("#hz-badge-" + srcUserName).text(parseInt(conut) + 1); $("#hz-badge-" + srcUserName).css("opacity", "1"); } } //发送消息 function send() { //消息 var message = $("#hz-message-input").html(); //目标用户名 var tarUserName = $("#toUserName").text(); //登录用户名 var srcUserName = $("#talks").text(); websocket.send(JSON.stringify({ "type": "1", "tarUser": {"username": tarUserName}, "srcUser": {"username": srcUserName}, "message": message })); $("#hz-message-body").append( "
" + "
" + "
" + message + "" + "
" + "
"); $("#hz-message-input").html(""); //取出对象 if (msgObjArr.length > 0) { var isExist = false; for (var i = 0; i < msgObjArr.length; i++) { var obj = msgObjArr[i]; if (obj.toUserName == tarUserName) { //保存最新数据 obj.message.push({username: srcUserName, message: message, date: NowTime()}); isExist = true; break; } } if (!isExist) { //追加聊天对象 msgObjArr.push({ toUserName: tarUserName, message: [{username: srcUserName, message: message, date: NowTime()}]//封装数据[{username:huanzi,message:"你好,我是欢子!",date:2018-04-29 22:48:00}] }); } } else { //追加聊天对象 msgObjArr.push({ toUserName: tarUserName, message: [{username: srcUserName, message: message, date: NowTime()}]//封装数据[{username:huanzi,message:"你好,我是欢子!",date:2018-04-29 22:48:00}] }); } } //监听点击用户 $("body").on("click", ".hz-group-list", function () { $(".hz-group-list").css("background-color", ""); $(this).css("background-color", "whitesmoke"); $("#toUserName").text($(this).find(".hz-group-list-username").text()); //清空旧数据,从对象中取出并追加 $("#hz-message-body").empty(); $("#hz-badge-" + $("#toUserName").text()).text("0"); $("#hz-badge-" + $("#toUserName").text()).css("opacity", "0"); if (msgObjArr.length > 0) { for (var i = 0; i < msgObjArr.length; i++) { var obj = msgObjArr[i]; if (obj.toUserName == $("#toUserName").text()) { //追加数据 var messageArr = obj.message; if (messageArr.length > 0) { for (var j = 0; j < messageArr.length; j++) { var msgObj = messageArr[j]; var leftOrRight = "right"; var message = msgObj.message; var msgUserName = msgObj.username; var toUserName = $("#toUserName").text(); //当聊天窗口与msgUserName的人相同,文字在左边(对方/其他人),否则在右边(自己) if (msgUserName == toUserName) { leftOrRight = "left"; } //但是如果点击的是自己,群聊的逻辑就不太一样了 if (username == toUserName && msgUserName != toUserName) { leftOrRight = "left"; } if (username == toUserName && msgUserName == toUserName) { leftOrRight = "right"; } var magUserName = leftOrRight == "left" ? "

"+msgUserName+":

" : ""; $("#hz-message-body").append( "
" + magUserName+ "
" + "
" + message + "" + "
" + "
" + "
"); } } break; } } } }); //获取当前时间 function NowTime() { var time = new Date(); var year = time.getFullYear();//获取年 var month = time.getMonth() + 1;//或者月 var day = time.getDate();//或者天 var hour = time.getHours();//获取小时 var minu = time.getMinutes();//获取分钟 var second = time.getSeconds();//或者秒 var data = year + "-"; if (month < 10) { data += "0"; } data += month + "-"; if (day < 10) { data += "0" } data += day + " "; if (hour < 10) { data += "0" } data += hour + ":"; if (minu < 10) { data += "0" } data += minu + ":"; if (second < 10) { data += "0" } data += second; return data; }

 

  java代码有三个类,MyEndpointConfigure,WebSocketConfig,WebSocketServer;

  MyEndpointConfigure

/** * 解决注入其他类的问题,详情参考这篇帖子:webSocket无法注入其他类:https://blog.csdn.net/tornadojava/article/details/78781474 */public class MyEndpointConfigure extends ServerEndpointConfig.Configurator implements ApplicationContextAware {    private static volatile BeanFactory context;    @Override    public 
T getEndpointInstance(Class
clazz){ return context.getBean(clazz); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { MyEndpointConfigure.context = applicationContext; }}

 

  WebSocketConfig

/** * WebSocket配置 */@Configurationpublic class WebSocketConfig{    /**     * 用途:扫描并注册所有携带@ServerEndpoint注解的实例。 @ServerEndpoint("/websocket")     * PS:如果使用外部容器 则无需提供ServerEndpointExporter。     */    @Bean    public ServerEndpointExporter serverEndpointExporter() {        return new ServerEndpointExporter();    }    /**     * 支持注入其他类     */    @Bean    public MyEndpointConfigure  newMyEndpointConfigure (){        return new MyEndpointConfigure ();    }}

 

  WebSocketServer

/** * WebSocket服务 */@RestController@RequestMapping("/websocket")@ServerEndpoint(value = "/websocket/{username}", configurator = MyEndpointConfigure.class)public class WebSocketServer {    /**     * 在线人数     */    private static int onlineCount = 0;    /**     * 在线用户的Map集合,key:用户名,value:Session对象     */    private static Map
sessionMap = new HashMap
(); /** * 注入其他类(换成自己想注入的对象) */ @Autowired private UserService userService; /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam("username") String username) { //在webSocketMap新增上线用户 sessionMap.put(username, session); //在线人数加加 WebSocketServer.onlineCount++; //通知除了自己之外的所有人 sendOnlineCount(session, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + username + "'}"); } /** * 连接关闭调用的方法 */ @OnClose public void onClose(Session session) { //下线用户名 String logoutUserName = ""; //从webSocketMap删除下线用户 for (Entry
entry : sessionMap.entrySet()) { if (entry.getValue() == session) { sessionMap.remove(entry.getKey()); logoutUserName = entry.getKey(); break; } } //在线人数减减 WebSocketServer.onlineCount--; //通知除了自己之外的所有人 sendOnlineCount(session, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + logoutUserName + "'}"); } /** * 服务器接收到客户端消息时调用的方法 */ @OnMessage public void onMessage(String message, Session session) { try { //JSON字符串转 HashMap HashMap hashMap = new ObjectMapper().readValue(message, HashMap.class); //消息类型 String type = (String) hashMap.get("type"); //来源用户 Map srcUser = (Map) hashMap.get("srcUser"); //目标用户 Map tarUser = (Map) hashMap.get("tarUser"); //如果点击的是自己,那就是群聊 if (srcUser.get("username").equals(tarUser.get("username"))) { //群聊 groupChat(session,hashMap); } else { //私聊 privateChat(session, tarUser, hashMap); } //后期要做消息持久化 } catch (IOException e) { e.printStackTrace(); } } /** * 发生错误时调用 */ @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } /** * 通知除了自己之外的所有人 */ private void sendOnlineCount(Session session, String message) { for (Entry
entry : sessionMap.entrySet()) { try { if (entry.getValue() != session) { entry.getValue().getBasicRemote().sendText(message); } } catch (IOException e) { e.printStackTrace(); } } } /** * 私聊 */ private void privateChat(Session session, Map tarUser, HashMap hashMap) throws IOException { //获取目标用户的session Session tarUserSession = sessionMap.get(tarUser.get("username")); //如果不在线则发送“对方不在线”回来源用户 if (tarUserSession == null) { session.getBasicRemote().sendText("{\"type\":\"0\",\"message\":\"对方不在线\"}"); } else { hashMap.put("type", "1"); tarUserSession.getBasicRemote().sendText(new ObjectMapper().writeValueAsString(hashMap)); } } /** * 群聊 */ private void groupChat(Session session,HashMap hashMap) throws IOException { for (Entry
entry : sessionMap.entrySet()) { //自己就不用再发送消息了 if (entry.getValue() != session) { hashMap.put("type", "2"); entry.getValue().getBasicRemote().sendText(new ObjectMapper().writeValueAsString(hashMap)); } } } /** * 登录 */ @RequestMapping("/login/{username}") public ModelAndView login(HttpServletRequest request, @PathVariable String username) { return new ModelAndView("socketChart.html", "username", username); } /** * 登出 */ @RequestMapping("/logout/{username}") public String loginOut(HttpServletRequest request, @PathVariable String username) { return "退出成功!"; } /** * 获取在线用户 */ @RequestMapping("/getOnlineList") private List
getOnlineList(String username) { List
list = new ArrayList
(); //遍历webSocketMap for (Entry
entry : WebSocketServer.sessionMap.entrySet()) { if (!entry.getKey().equals(username)) { list.add(entry.getKey()); } } return list; }}

 

  后记

  后期把所有功能都补全就完美了,表情、图片都算比较简单,之前用轮询实现的时候写过了,但是没加到这里来;音视频聊天的话可以用WbeRTC来做,之前也研究了一下,不过还没搞完,这里贴一下维基百科对它的介绍,想了解更多的自行Google:

  WebRTC,名称源自网页即时通信(英语:Web Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音对话或视频对话的API。它于2011年6月1日开源并在Google、Mozilla、Opera支持下被纳入万维网联盟的W3C推荐标准。

  最后在加上持久化存储,注册后才能聊天,离线消息上线后接收,再加上用Redis或者其他的缓存技术支持,完美。不过聊天记录要做存储,表设计不知如何设计才合理,如果哪位大佬愿意分享可以留言给我,大家一起进步!

 

  补充

  2019-07-03补充:这里补充贴出pom代码,在子类引入父类,如果我们没有父类,只有一个子类,把两个整合一下就可以了

4.0.0
cn.huanzi.qch
parent
1.0.0
pom
org.springframework.boot
spring-boot-starter-parent
2.1.0.RELEASE
SpringBoot系列demo代码
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.projectlombok
lombok
org.springframework.boot
spring-boot-devtools
true
runtime
org.springframework.boot
spring-boot-maven-plugin
${project.artifactId}
../package
parent.xml
4.0.0
springboot-websocket
0.0.1
springboot-websocket
SpringBoot系列——WebSocket
cn.huanzi.qch
parent
1.0.0
org.springframework.boot
spring-boot-starter-websocket
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-maven-plugin
springboot-websocket.xml

 

  在后记的部分我们就提到要加上持久化存储,事实上我们已经开始慢慢在写一套简单的IM即时通讯,已经实现到第三版了,持续更新中...

   

  

  

 

 

  代码开源

  代码已经开源、托管到我的GitHub、码云:

  GitHub:

  码云:

 

转载于:https://www.cnblogs.com/huanzi-qch/p/9889521.html

你可能感兴趣的文章
poj 1331 Multiply
查看>>
tomcat7的数据库连接池tomcatjdbc的25个优势
查看>>
Html 小插件5 百度搜索代码2
查看>>
P1107 最大整数
查看>>
多进程与多线程的区别
查看>>
Ubuntu(虚拟机)下安装Qt5.5.1
查看>>
java.io.IOException: read failed, socket might closed or timeout, read ret: -1
查看>>
java 常用命令
查看>>
CodeForces Round #545 Div.2
查看>>
卷积中的参数
查看>>
51nod1076 (边双连通)
查看>>
Item 9: Avoid Conversion Operators in Your APIs(Effective C#)
查看>>
学习Spring Boot:(二十八)Spring Security 权限认证
查看>>
深入浅出JavaScript(2)—ECMAScript
查看>>
STEP2——《数据分析:企业的贤内助》重点摘要笔记(六)——数据描述
查看>>
ViewPager的onPageChangeListener里面的一些方法参数:
查看>>
Jenkins关闭、重启,Jenkins服务的启动、停止方法。
查看>>
CF E2 - Array and Segments (Hard version) (线段树)
查看>>
Linux SPI总线和设备驱动架构之四:SPI数据传输的队列化
查看>>
SIGPIPE并产生一个信号处理
查看>>