15158846557 在线咨询 在线咨询
15158846557 在线咨询
所在位置: 首页 > 营销资讯 > 网站运营 > SpringBoot入门建站全系列(二十七)WebSocket做简单的聊天室

SpringBoot入门建站全系列(二十七)WebSocket做简单的聊天室

时间:2023-07-06 13:00:01 | 来源:网站运营

时间:2023-07-06 13:00:01 来源:网站运营

SpringBoot入门建站全系列(二十七)WebSocket做简单的聊天室:

SpringBoot入门建站全系列(二十七)WebSocket做简单的聊天室

一、概述

WebSocket 是一种网络通信协议。RFC6455 定义了它的通信标准。 WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。 这种通信模型有一个弊端:HTTP 协议无法实现服务器主动向客户端发起消息。 这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数 Web 应用程序将通过频繁的异步JavaScript和XML(AJAX)请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。

WebSocket复用了HTTP的握手通道,使用ws或wss的统一资源标志符,类似于HTTPS。其中wss表示使用了TLS的Websocket。如:ws://example.com/wsapi或者wss://secure.example.com/wsapi

代码可以在SpringBoot组件化构建https://www.pomit.cn/java/spring/springboot.html中的WebSocket组件中查看,并下载。

首发地址: 品茗IT-首发

如果大家正在寻找一个java的学习环境,或者在开发中遇到困难,可以加入我们的java学习圈,点击即可加入,共同学习,节约学习时间,减少很多在学习中遇到的难题。

二、配置

本文假设你已经引入spring-boot-starter-web。已经是个SpringBoot项目了,如果不会搭建,可以打开这篇文章看一看《SpringBoot入门建站全系列(一)项目建立》。

2.1 Maven依赖

使用WebSocket需要引入spring-boot-starter-websocket。本文使用fastjson做json数据的传输。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId></dependency><dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId></dependency>

2.2 配置文件

无需额外配置。

三、WebSocket配置

3.1 WebSocket处理相关配置

Springboot整合WebSocket需要配置websocket的监听url、配置WebSocketInterceptor(连接握手配置)、webSocketHandler(连接成功配置)。

SpringWebSocketConfig:

package com.cff.springbootwork.websocket.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.socket.config.annotation.EnableWebSocket;import org.springframework.web.socket.config.annotation.WebSocketConfigurer;import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;import com.cff.springbootwork.websocket.handler.WebSocketHandler;import com.cff.springbootwork.websocket.handler.WebSocketInterceptor;@Configuration@EnableWebSocketpublic class SpringWebSocketConfig implements WebSocketConfigurer { public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(webSocketHandler(), "/websocket").addInterceptors(new WebSocketInterceptor()); registry.addHandler(webSocketHandler(), "/sockjs").addInterceptors(new WebSocketInterceptor()).withSockJS(); } @Bean public WebSocketHandler webSocketHandler() { return new WebSocketHandler(); }}

3.2 WebSocket的session拦截处理

WebSocketInterceptor对WebSocket的连接进行过滤,可以对连接前和连接后自定义处理。

WebSocketInterceptor:

package com.cff.springbootwork.websocket.handler;import java.util.Map;import javax.servlet.http.HttpSession;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.http.server.ServerHttpRequest;import org.springframework.http.server.ServerHttpResponse;import org.springframework.http.server.ServletServerHttpRequest;import org.springframework.stereotype.Component;import org.springframework.util.StringUtils;import org.springframework.web.socket.WebSocketHandler;import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;/** * websocket 握手处理器 * @author fufei * */@Componentpublic class WebSocketInterceptor extends HttpSessionHandshakeInterceptor { private Logger log = LoggerFactory.getLogger(this.getClass()); @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { log.info("收到握手请求。"); if (request instanceof ServletServerHttpRequest) { ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request; HttpSession session = servletRequest.getServletRequest().getSession(false); if (session == null) { return false; } // 使用userName区分WebSocketHandler,以便定向发送消息 String userName = (String) session.getAttribute("userName"); if (StringUtils.isEmpty(userName)) { return false; } log.info("获取到用户信息:{}", userName); attributes.put("userId", userName); } return super.beforeHandshake(request, response, wsHandler, attributes); } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) { log.info("成功握手。"); super.afterHandshake(request, response, wsHandler, ex); }}

3.3 WebSocket消息处理

WebsocketHandler负责处理消息发送接收的逻辑:

package com.cff.springbootwork.websocket.handler;import java.io.IOException;import java.util.List;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.util.StringUtils;import org.springframework.web.socket.AbstractWebSocketMessage;import org.springframework.web.socket.CloseStatus;import org.springframework.web.socket.TextMessage;import org.springframework.web.socket.WebSocketSession;import org.springframework.web.socket.handler.TextWebSocketHandler;import com.alibaba.fastjson.JSONObject;import com.cff.springbootwork.websocket.dto.MessageDTO;import com.cff.springbootwork.websocket.memory.WebSocketUser;/** * websocket消息处理器 * * @author fufei * */public class WebSocketHandler extends TextWebSocketHandler { protected Logger logger = LoggerFactory.getLogger(getClass()); public static WebSocketUser users = new WebSocketUser(); @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { logger.info("收到websocket消息:{}", message.toString()); super.handleTextMessage(session, message); String msg = message.getPayload(); logger.info("收到websocket消息的消息体:{}", msg); if (!StringUtils.isEmpty(msg)) { MessageDTO messageDTO = JSONObject.parseObject(msg, MessageDTO.class); sendMessageToUser(messageDTO.getTargetUserName(), message); } } @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { String userName = (String) session.getAttributes().get("userName"); if (StringUtils.isEmpty(userName)) { logger.error("用户不能为空!"); } WebSocketUser.add(userName, session); MessageDTO messageDTO = new MessageDTO(); messageDTO.setFromUserName(""); messageDTO.setTargetUserName(userName); messageDTO.setMessageType(MessageDTO.Type.TYPE_NEW.getMessageType()); messageDTO.setMessage("欢迎您!"); session.sendMessage(new TextMessage(JSONObject.toJSONString(messageDTO))); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { String userName = (String) session.getAttributes().get("userName"); if (StringUtils.isEmpty(userName)) { logger.error("用户不能为空!"); } WebSocketUser.removeWebSocketSession(userName, session); super.afterConnectionClosed(session, status); } /** * 给某个用户发送消息 * * @param userName * @param message */ public void sendMessageToUser(String userName, AbstractWebSocketMessage<?> message) { List<WebSocketSession> webUsers = WebSocketUser.getSessionByUserName(userName); if (webUsers == null || webUsers.size() == 0) { logger.error("发送给{},当前无session", userName); return; } logger.info("发送给{},当前session个数为:{}", userName, webUsers.size()); for (int i = 0; i < webUsers.size(); i++) { WebSocketSession session = webUsers.get(i); try { if (!session.isOpen()) { WebSocketUser.removeWebSocketSession(userName, session); } session.sendMessage(message); } catch (IOException e) { e.printStackTrace(); } } }}

3.4 用户与WebSocketSession对应

WebSocketUser是使用内存来存储用户:

package com.cff.springbootwork.websocket.memory;import java.util.ArrayList;import java.util.List;import java.util.Map;import java.util.Set;import java.util.concurrent.ConcurrentHashMap;import org.springframework.util.CollectionUtils;import org.springframework.web.socket.WebSocketSession;public class WebSocketUser { private static Map<String, List<WebSocketSession>> userNameWebsession = new ConcurrentHashMap<>(); public static void add(String userName, WebSocketSession webSocketSession) { userNameWebsession.computeIfAbsent(userName, v -> new ArrayList<WebSocketSession>()).add(webSocketSession); } /** * 根据昵称拿WebSocketSession * * @param nickName * @return */ public static List<WebSocketSession> getSessionByUserName(String userName) { return userNameWebsession.get(userName); } /** * 移除失效的WebSocketSession * * @param webSocketSession */ public static void removeWebSocketSession(String userName, WebSocketSession webSocketSession) { if (webSocketSession == null) return; List<WebSocketSession> webSessoin = userNameWebsession.get(userName); if (webSessoin == null || CollectionUtils.isEmpty(webSessoin)) return; webSessoin.remove(webSocketSession); } public static Set<String> getUserList(){ return userNameWebsession.keySet(); }}

四、用户生成、查询及文件发送

如果我们想发送文件,需要上传文件后转为二进制数据并使用websocket发送。另外,这里使用setUser来产生用户,并提供获取当前用户,用户列表的接口。

package com.cff.springbootwork.websocket.web;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.InputStream;import javax.servlet.http.HttpServletRequest;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.multipart.MultipartFile;import org.springframework.web.socket.BinaryMessage;import com.cff.springbootwork.websocket.dto.ResultModel;import com.cff.springbootwork.websocket.handler.WebSocketHandler;import com.cff.springbootwork.websocket.memory.WebSocketUser;@RestController@RequestMapping("/im")public class WebSocketController { protected Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private WebSocketHandler websocketHandler; @RequestMapping(value = "/fileUpload") public ResultModel fileUpload(@RequestParam("userName") String userName, @RequestParam MultipartFile[] myfiles, HttpServletRequest request) { logger.info("收到发往用户[{}]的文件上传请求;文件数量:{}", userName, myfiles.length); int count = 0; for (MultipartFile myfile : myfiles) { if (myfile.isEmpty()) { count++; } logger.info("文件原名:{};文件类型:", myfile.getOriginalFilename(), myfile.getContentType()); try (ByteArrayOutputStream swapStream = new ByteArrayOutputStream(); InputStream is = myfile.getInputStream();) { byte[] buff = new byte[100]; // buff用于存放循环读取的临时数据 int rc = 0; while ((rc = is.read(buff, 0, 100)) > 0) { swapStream.write(buff, 0, rc); } byte[] in_b = swapStream.toByteArray(); // in_b为转换之后的结果 logger.info("正在发送文件: "); websocketHandler.sendMessageToUser(userName, new BinaryMessage(in_b)); } catch (IOException e) { logger.error("文件原名:{}", myfile.getOriginalFilename(), e); e.printStackTrace(); count++; continue; } } return ResultModel.ok(count); } @RequestMapping(value = "/setUser") public ResultModel setUser(@RequestParam("userName") String userName, HttpServletRequest request) { logger.info("设置用户[{}]", userName); request.getSession().setAttribute("userName", userName); return ResultModel.ok(); } @RequestMapping(value = "/user") public ResultModel user(HttpServletRequest request) { Object userName = request.getSession().getAttribute("userName"); if(userName == null)return ResultModel.error("无用户"); return ResultModel.ok(userName); } @RequestMapping(value = "/userList") public ResultModel userList() { return ResultModel.ok(WebSocketUser.getUserList()); }}

五、聊天室页面

为了实现我们的简单聊天功能,我们需要前端进行配合。

chat.html实现了简单的聊天室,支持文字、表情、文件等:

该html需要很多js配合,下面贴出html和websocket.js,其他js都是很普遍的js,如果需要我发送,加入群聊向群主索要。

<!DOCTYPE html><html><head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <title>品茗IT-WebSocket测试</title> <!-- CSS --> <link href="https://lib.baomitu.com/material-design-icons/3.0.1/iconfont/material-icons.min.css" rel="stylesheet"> <link href="https://lib.baomitu.com/materialize/0.100.2/css/materialize.min.css" type="text/css" rel="stylesheet" media="screen,projection"/> <style>body { text-align:left; margin:0; font:normal 12px Verdana, Arial;background:#FFEEFF } form { margin:0; font:normal 12px Verdana,Arial; } table,input { font:normal 12px Verdana, Arial; }a:link,a:visited{ text-decoration:none; color:#333333; } a:hover{text-decoration:none; color:#FF6600 } #main { width:400px;position:absolute; left:600px; top:100px; background:#EFEFFF;text-align:left; filter:Alpha(opacity=90) } #ChatHead {text-align:right; padding:3px; border:1px solid #003399;background:#DCDCFF; font-size:20px; color:#3366FF; cursor:move; }#ChatHead a:link,#ChatHead a:visited, { font-size:14px;font-weight:bold; padding:0 3px } #ChatBody { border:1px solid#003399; border-top:none; padding:2px; } #ChatContent {height:200px; padding:6px; overflow-y:scroll; word-break: break-all}#ChatBtn { border-top:1px solid #003399; padding:2px } </style></head><script type="text/javascript">var ws = null;var curUser=null;var chatUser = null;var imgName = null;var fileImgSize = 0;window.onbeforeunload = function(){ disconnect(ws);}function gs(d) { var t = document.getElementById(d); if (t) { return t.style; } else { return null; }}function gs2(d, a) { if (d.currentStyle) { var curVal = d.currentStyle[a] } else { var curVal = document.defaultView .getComputedStyle(d, null)[a] } return curVal;}function ChatHidden() { gs("ChatBody").display = "none";}function ChatShow() { gs("ChatBody").display = "";}function ChatClose() { gs("main").display = "none";}function ChatNew(userId) { gs("main").display = ""; chatUser = userId; $("#ChatUsers").html(chatUser); $('.emotion').qqFace({ id : 'facebox', assign:'saytext', path: './img/arclist/' //表情存放的路径 });}function ChatClear(obj) { $("#ChatContent").html("");}function ChatRead() { if(document.getElementById(chatUser)){ document.getElementById(chatUser).setAttribute('src', './img/users.png'); }}function ChatSend(obj) { var o = obj.ChatValue; var msg = replace_em(o.value); if (o.value.length > 0) { $("#ChatContent").append( "<p align=/"right/"><strong>" + curUser + "(我) :</strong>" + msg + "</p>"); var number = $("#ChatContent").scrollTop(); number += 16; $("#ChatContent").scrollTop(number); if(ws!=null){ var json={"fromUserName":curUser,"targetUserName":chatUser,"message":o.value,"messageType":"1000"}; // encodeURI(o.value) console.log(json); ws.send(JSON.stringify(json)); } o.value = ''; } var img = obj.ChatFile; if (img.value.length > 0){ $("#ChatContent").append( "<p align=/"right/"><strong>" + nickName + "(我) :</strong>" + img.value + "</p><br/>"); imgName = nickName+'(我)'; fileImgSize = img.files.length; //alert(fileImgSize); $.ajaxFileUpload({ //处理文件上传操作的服务器端地址(可以传参数,已亲测可用) url:'im/fileUpload?userId='+muserId, secureuri:true, //是否启用安全提交,默认为false fileElementId:'ChatFile', //文件选择框的id属性 dataType:'text', //服务器返回的格式,可以是json或xml等 success:function(data, status){ //服务器响应成功时的处理函数 //$("#ChatContent").append("<p align=/"right/">" + data + "</p><br/>"); }, error:function(data, status, e){ //服务器响应失败时的处理函数 $("#ChatContent").append('<p align=/"right/">图片上传失败,请重试!!</p><br/>'); imgName = msgUser; } }); }} if (document.getElementById) { (function() { if (window.opera) { document.write("<input type='hidden' id='Q' value=' '>"); } var n = 500; var dragok = false; var y, x, d, dy, dx; function move(e) { if (!e) e = window.event; if (dragok) { d.style.left = dx + e.clientX - x + "px"; d.style.top = dy + e.clientY - y + "px"; return false; } } function down(e) { if (!e) e = window.event; var temp = (typeof e.target != "undefined") ? e.target : e.srcElement; if (temp.tagName != "HTML" | "BODY" && temp.className != "dragclass") { temp = (typeof temp.parentNode != "undefined") ? temp.parentNode : temp.parentElement; } if ('TR' == temp.tagName) { temp = (typeof temp.parentNode != "undefined") ? temp.parentNode : temp.parentElement; temp = (typeof temp.parentNode != "undefined") ? temp.parentNode : temp.parentElement; temp = (typeof temp.parentNode != "undefined") ? temp.parentNode : temp.parentElement; } if (temp.className == "dragclass") { if (window.opera) { document.getElementById("Q").focus(); } dragok = true; temp.style.zIndex = n++; d = temp; dx = parseInt(gs2(temp, "left")) | 0; dy = parseInt(gs2(temp, "top")) | 0; x = e.clientX; y = e.clientY; document.onmousemove = move; return false; } } function up() { dragok = false; document.onmousemove = null; } document.onmousedown = down; document.onmouseup = up; })(); } function toIndex(){ window.location.href= contextPath + "/index.jsp"; }</script><body><div id="main" class="dragclass" onclick="ChatRead()" style="left: 400px; top: 200px;"> <div id="ChatUsers" style="width:100px; padding:3px; font-size:15px;float:left; display:inline"></div> <div id="ChatHead"> <a href="#" onclick="ChatHidden();">-</a> <a href="#" onclick="ChatShow();">+</a> <a href="#" onclick="ChatClose();">x</a> </div> <div id="ChatBody"> <div id="ChatContent"></div> <div id="ChatBtn"> <form action="" name="chat" method="post"> <textarea name="ChatValue" id="saytext" rows="3" style="width: 350px"></textarea> <input name="Submit" type="button" value="发送" onclick="ChatSend(this.form);" /> <input name="ClearMsg" type="button" value="清空记录" onclick="ChatClear(this.form);" /> <input type="button" class="emotion" value="表情"> <input id="ChatFile" type="file" name="myfiles" multiple> </form> </div> </div></div><div id="modalAddUser" class="modal modal-fixed-footer" style="max-width:400px;max-height:400px"> <div class="modal-content"> <h4>生成用户名</h4> <div class="row center"> <input class="browser-default searchInput" placeholder="请输入用户名" style="margin-top:50px;margin-left:20px;max-width:300px" id="catoryAddText" type="text" > </div> <div class="row center"> <a class="waves-effect waves-light btn" id="userAddBtn" style="color:white;"><i class="material-icons" style="font-size:1.1rem">添用户</i></a> </div> </div> <div class="modal-footer"> <a href="#!" class=" modal-action modal-close waves-effect waves-green btn-flat">关闭</a> </div></div><div align="left" style="margin-top: 50px;margin-left: 20px;"> <p>欢迎您, <span id="userName">匿名用户</span> </p> <a id="addUser" class="btn waves-effect waves-light white cyan-text" style="border-radius: 40px;">添加用户</a> <p id="content"></p></div><script src="https://lib.baomitu.com/jquery/3.3.0/jquery.min.js"></script><script src="https://lib.baomitu.com/materialize/0.100.2/js/materialize.min.js"></script><script src="./js/websocket.js"></script><script src="./js/ajaxfileupload.js"></script><script src="./js/jquery-browser.js"></script><script src="./js/jquery.qqFace.js"></script><script>function getUser(){ $.ajax({ type : "get", url : "im/user", dataType : "json", data : {} , success : function(data) { if(data.errorCode == "0000"){ $("#userName").html(data.data); curUser = data.data; } }, error : function(XMLHttpRequest, textStatus, errorThrown) { alert(errorThrown); } });} function addUser(userName){ $.ajax({ type : "post", url : "im/setUser", dataType : "json", data : {"userName":userName} , success : function(data) { if(data.errorCode == "0000"){ $("#userName").html(userName); curUser = data.data; } }, error : function(XMLHttpRequest, textStatus, errorThrown) { alert(errorThrown); } });}function userList(){ $.ajax({ type : "get", url : "im/userList", dataType : "json", data : {} , success : function(data) { if(data.errorCode == "0000"){ var content = ""; for(var i =0;i<data.data.length;i++){ var userId = data.data[i]; content += "<img src=/"./img/msgget.gif/" id=/"" + userId + "/" alt=/"/" style=/"cursor: pointer/" width='40px' " + "onclick=/"ChatNew('"+userId+"')/" />" + userId + "<br><br>"; } $("#content").append(content); } }, error : function(XMLHttpRequest, textStatus, errorThrown) { alert(errorThrown); } });}window.onbeforeunload = function(){ disconnect(ws);}$(function () { $('.modal').modal({ dismissible: true, // 点击模态外面模态消失关闭 opacity: 0.1, // 相对于背景的不透明度 in_duration: 300, // 显示特效的时间 out_duration: 200, // 消失特效时间 starting_top: '80%', // 启动时的样式属性 ending_top: '20%', // 结束时的样式属性 ready: function(modal, trigger) { // 模态加载完成触发事件 }, complete: function() { } // 关闭时触发的事件 }); getUser(); $("#addUser").click(function() { $('#modalAddUser').modal('open'); }); $("#userAddBtn").click(function() { var catory = $('#catoryAddText').val(); addUser(catory); }); userList(); if (ws == null) { var url = getUrl(); //alert("url:"+url); if (!url) { return; } console.log(url); ws = new WebSocket(url); connect(ws); ChatClose(); }});</script></body><script>var _hmt = _hmt || [];(function() { var hm = document.createElement("script"); hm.src = "https://hm.baidu.com/hm.js?e553ae2bb23494dee9b6f43415a1ce5a"; var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(hm, s);})();</script></html>这个html需要websocket.js配合:

聊天室界面如下:







六、过程中用到的其他实体

MessageDTO:

ResultModel:

详细完整的代码,可以访问品茗IT-博客《SpringBoot入门建站全系列(二十七)WebSocket做简单的聊天室》进行查看

品茗IT-博客专题:https://www.pomit.cn/lecture.html汇总了Spring专题、Springboot专题、SpringCloud专题、web基础配置专题。

快速构建项目

Spring项目快速开发工具:

一键快速构建Spring项目工具

一键快速构建SpringBoot项目工具

一键快速构建SpringCloud项目工具

一站式Springboot项目生成

Mysql一键生成Mybatis注解Mapper

Spring组件化构建

SpringBoot组件化构建

SpringCloud服务化构建

喜欢这篇文章么,喜欢就加入我们一起讨论SpringBoot使用吧!



关键词:简单,入门,系列

74
73
25
news

版权所有© 亿企邦 1997-2025 保留一切法律许可权利。

为了最佳展示效果,本站不支持IE9及以下版本的浏览器,建议您使用谷歌Chrome浏览器。 点击下载Chrome浏览器
关闭