二.三 实体封装处理和控制器
二.三.一 封装用户信息 User
里面存入 id (uuid自动生成,避免重复) 和 nickName (用户输入昵称) 。 可以用数据库替换这个用户信息
package com.yjl.websocket.pojo; /** * 封装用户信息对象 * @author 两个蝴蝶飞 * */ public class User { /** * @param id 编号,是uuid * @param nickName 昵称,由用户自己输入 */ private String id; private String nickName; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getNickName() { return nickName; } public void setNickName(String nickName) { this.nickName = nickName; } @Override public String toString() { return "User [id=" + id + ", nickName=" + nickName + "]"; } }
二.三.二 封装消息内容 MyMessage
里面有 发送者,接收者,发送内容,发送时间等重要的信息
package com.yjl.websocket.bean; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; /** * 封装消息对象 * @author 两个蝴蝶飞 * */ public class MyMessage { /** * @param fromId 发送者 * @param fromNickName 发送者的昵称 * @param toId 接收者,如果是群发的话,为空 * @param text 发送的内容 * @param date 发送的时间,具体到秒 */ private String fromId; private String fromNickName; private String toId; private String text; //格式化成 yyyy-MM-dd HH:mm:ss的格式 @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") private Date date; public String getFromId() { return fromId; } public void setFromId(String fromId) { this.fromId = fromId; } public String getFromNickName() { return fromNickName; } public void setFromNickName(String fromNickName) { this.fromNickName = fromNickName; } public String getToId() { return toId; } public void setToId(String toId) { this.toId = toId; } public String getText() { return text; } public void setText(String text) { this.text = text; } public Date getDate() { return date; } public void setDate(Date date) { this.date = date; } @Override public String toString() { return "MyMessage [fromId=" + fromId + ", fromNickName=" + fromNickName + ", toId=" + toId + ", text=" + text + ", date=" + date + "]"; } }
二.三.三 封装在线用户列表 MyOnLineUserMap
package com.yjl.websocket.bean; import java.util.HashMap; import java.util.Map; import org.springframework.web.socket.WebSocketSession; import com.yjl.websocket.pojo.User; /** * 封装在线用户列表信息 * @author 两个蝴蝶飞 * */ public class MyOnLineUserMap { //定义id 与 session的集合,用于发送消息 private static Map<String,WebSocketSession> USER_ONLINE_SESSION_MAP; //定义id 与 user的集合,用于查询在线用户 private static Map<String,User> USER_ONLINE_MAP; static { //初始化,长度为16 USER_ONLINE_SESSION_MAP=new HashMap<String,WebSocketSession>(16); USER_ONLINE_MAP=new HashMap<String,User>(16); } public static Map<String, WebSocketSession> getUSER_ONLINE_SESSION_MAP() { return USER_ONLINE_SESSION_MAP; } public static Map<String, User> getUSER_ONLINE_MAP() { return USER_ONLINE_MAP; } }
二.三.四 编写控制器 UserAction
用于跳转到登录,登录方法和查询在线用户列表的方法
package com.yjl.websocket.action; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import com.yjl.websocket.bean.MyOnLineUserMap; import com.yjl.websocket.pojo.User; /** * * 处理用户跳转逻辑 * @author 两个蝴蝶飞 * */ @Controller @RequestMapping("/User") public class UserAction { /** * 跳转到登录的页面 * @return */ @RequestMapping("/toLogin") public String toLogin(HttpSession session){ //需要清空session中的loginUser, 如果有的话 if(session.getAttribute("loginUser")!=null){ //移除 session.removeAttribute("loginUser"); } return "login"; } /** * 登录操作 * @param nickName * @param req * @param session * @return */ @RequestMapping("/login") public String login(String nickName,HttpServletRequest req,HttpSession session){ //当前浏览器已经登录过了,那么就清空,保证每一个浏览器只能登录一个用户。 if(session.getAttribute("loginUser")!=null){ //移除 session.removeAttribute("loginUser"); } //编号为uuid User user=new User(); user.setId(UUID.randomUUID().toString()); user.setNickName(nickName); //放置到session 里面 session.setAttribute("loginUser",user); System.out.println("**********新用户nickeName["+nickName+"]登录*****************"); return "redirect:toMain.action"; } /** * 跳转到主页 * @param session * @return */ @RequestMapping("/toMain") public String toMain(HttpSession session){ //如果未登录,就跳转到登录页面 if(session.getAttribute("loginUser")==null){ return "redirect:toLogin"; } return "main"; } /** * 查询在线用户列表 * @return */ @RequestMapping(value="/onlineList") @ResponseBody public Map<String,Object> getOnlineUserList(){ Map<String,Object> resultMap=new HashMap<String,Object>(); List<User> allList=new ArrayList<User>(); allList.addAll(MyOnLineUserMap.getUSER_ONLINE_MAP().values()); resultMap.put("onlineList",allList); return resultMap; } }
上面,都是正常的逻辑操作,与WebSocket 无关。
可以发现,Spring 整合 WebSocket 时,没有很大的侵入性,是松耦合的。
二.四 WebSocket的三大件编写
基本与 一.二 讲解的内容差不多。
二.四.一 编写拦截器 MyHandshakeInterceptor
package com.yjl.websocket.websocket; import java.util.Map; import javax.servlet.http.HttpSession; 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.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeInterceptor; import com.yjl.websocket.pojo.User; /** * 配置拦截器,需要继承 HandshakeInterceptor * @author 两个蝴蝶飞 * */ @Component("myHandshakeInterceptor") public class MyHandshakeInterceptor implements HandshakeInterceptor{ /** * 先发送一个请求,请求连接 */ @Override public boolean beforeHandshake(ServerHttpRequest req, ServerHttpResponse resp, WebSocketHandler handler, Map<String, Object> attribute) throws Exception { if(req instanceof ServletServerHttpRequest){ System.out.println("属于ServletServerHttpRequest"); //先进行转换 ServletServerHttpRequest servletRequest=(ServletServerHttpRequest)req; //得到Session HttpSession session=servletRequest.getServletRequest().getSession(false); //取出里面的 loginUser 的登录用户 if(session.getAttribute("loginUser")!=null){ User user=(User)session.getAttribute("loginUser"); //放置到 map里面,这个map是 WebSocketSession的对象 attribute.put("loginUser",user); System.out.println("连接一个新用户:[id:"+user.getId()+",nickName:"+user.getNickName()); }else{ System.out.println("***********用户未登录,握手失败*****************"); return false; } }else{ System.out.println("不属于ServletServerHttpRequest"); } System.out.println("*********发送请求握手*************"); return true; } /** * 请求连接成功 */ @Override public void afterHandshake(ServerHttpRequest arg0, ServerHttpResponse arg1, WebSocketHandler arg2, Exception arg3) { System.out.println("*********握手成功*************"); } }
二.四.二 编写处理器 MyWebSocketHandler
package com.yjl.websocket.websocket; import java.io.IOException; import java.util.Date; import java.util.Map; import org.springframework.stereotype.Component; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.util.HtmlUtils; import com.fasterxml.jackson.databind.ObjectMapper; import com.yjl.websocket.bean.MyMessage; import com.yjl.websocket.bean.MyOnLineUserMap; import com.yjl.websocket.pojo.User; /** * 配置处理器 * @author 两个蝴蝶飞 * */ @Component("myWebSocketHandler") public class MyWebSocketHandler implements WebSocketHandler{ /** * 当连接成功之后,进行的处理操作,对应 @OnOpen * wsSession 指的是 连接的那个浏览器用户信息 */ @Override public void afterConnectionEstablished(WebSocketSession wsSession) throws Exception { System.out.println("进来了:onOpen"); //获取存于attribute的那个map Map<String,Object> attributes=wsSession.getAttributes(); //刚刚登录成功的那个user 信息 User user=(User)attributes.get("loginUser"); //将这个信息,放置到在线的map里面 MyOnLineUserMap.getUSER_ONLINE_SESSION_MAP().put(user.getId(),wsSession); MyOnLineUserMap.getUSER_ONLINE_MAP().put(user.getId(), user); //构建消息 MyMessage MyMessage message=new MyMessage(); message.setText("风骚的【"+user.getNickName()+"】进入了聊天室,大家欢迎"); message.setDate(new Date()); //构建TextMessage 对象,然后发送对象信息 ObjectMapper objMapper=new ObjectMapper(); String textResult=objMapper.writeValueAsString(message); System.out.println("输出消息内容:"+textResult.toString()); TextMessage textMessage=new TextMessage(textResult); //发送消息给所有人 sendMessageToAll(textMessage); } /** * 主动断开连接后的事件, 对应 @OnClose */ @Override public void afterConnectionClosed(WebSocketSession wsSession, CloseStatus closeStatus) throws Exception { System.out.println("进来了:onClose"); //获取该 wsSession 对应的那个User 信息 User closeUser=(User)wsSession.getAttributes().get("loginUser"); //构建 Message MyMessage message=new MyMessage(); message.setFromId(closeUser.getId()); message.setFromNickName(closeUser.getNickName()); message.setText("万众嘱目的【"+closeUser.getNickName()+"】有事先走了,大家继续聊..."); message.setDate(new Date()); //在线列表里面,去除掉这个人的信息 MyOnLineUserMap.getUSER_ONLINE_SESSION_MAP().remove(closeUser.getId()); MyOnLineUserMap.getUSER_ONLINE_MAP().remove(closeUser.getId()); //信息移除 wsSession.getAttributes().remove("loginUser"); ObjectMapper objMapper=new ObjectMapper(); String textResult=objMapper.writeValueAsString(message); TextMessage textMessage=new TextMessage(textResult); //发送消息给所有人 sendMessageToAll(textMessage); } /** * 浏览器发送消息之后,进行的处理操作, 对应 @OnMessage */ @Override public void handleMessage(WebSocketSession wsSession, WebSocketMessage<?> message) throws Exception { System.out.println("进来了:onMessage"); // 接收的消息,长度如果是0,表示没有消息,直接返回 if(message.getPayloadLength()==0){ return ; } ObjectMapper objMapper=new ObjectMapper(); MyMessage inputMessage=objMapper.readValue(message.getPayload().toString(),MyMessage.class); //设置日期 inputMessage.setDate(new Date()); //接收到的消息 String inputMsg=inputMessage.getText(); System.out.println("【"+inputMessage.getFromNickName()+"】发送的消息是:"+inputMsg); //将这个消息,进行转义 String escapeHTML=HtmlUtils.htmlEscape(inputMsg); //重新设置转义好的字符串 inputMessage.setText(escapeHTML); //定义Message TextMessage textMessage=new TextMessage(objMapper.writeValueAsString(inputMessage)); //接收到的消息, 看是群发,还是私发 if(inputMessage.getToId()==null||"-1".equals(inputMessage.getToId())){ //是群发 sendMessageToAll(textMessage); }else{ //是私发 sendMessageToOne(inputMessage.getToId(), textMessage); } } /** * 错误时的消息, 对应的是 @OnError */ @Override public void handleTransportError(WebSocketSession wsSession, Throwable throwable) throws Exception { System.out.println("进来了:onError"); //如果目前开启,那么执行关闭 if(wsSession.isOpen()){ wsSession.close(); } //获取该 wsSession 对应的那个User 信息 User closeUser=(User)wsSession.getAttributes().get("loginUser"); //构建 Message MyMessage message=new MyMessage(); message.setFromId(closeUser.getId()); message.setFromNickName(closeUser.getNickName()); message.setText("万众嘱目的【"+closeUser.getNickName()+"】退出聊天室"); message.setDate(new Date()); //在线列表里面,去除掉这个人的信息 MyOnLineUserMap.getUSER_ONLINE_SESSION_MAP().remove(closeUser.getId()); MyOnLineUserMap.getUSER_ONLINE_MAP().remove(closeUser.getId()); //信息移除 wsSession.getAttributes().remove("loginUser"); ObjectMapper objMapper=new ObjectMapper(); String textResult=objMapper.writeValueAsString(message); TextMessage textMessage=new TextMessage(textResult); //发送消息给所有人 sendMessageToAll(textMessage); } /** * 是否支持处理拆分消息,返回true返回拆分消息 */ //是否支持部分消息:如果设置为true,那么一个大的或未知尺寸的消息将会被分割,并会收到多次消息(会通过多次调用方法handleMessage(WebSocketSession, WebSocketMessage). ) //如果分为多条消息,那么可以通过一个api:org.springframework.web.socket.WebSocketMessage.isLast() 是否是某条消息的最后一部分。 //默认一般为false,消息不分割 @Override public boolean supportsPartialMessages() { return false; } /** * 发送给单个用户 * @param toId 用户编号 * @param textMessage 发送消息 */ private void sendMessageToOne(String toId,final TextMessage textMessage){ //没有接收人,则发送给全部的在线用户 if(toId==null){ sendMessageToAll(textMessage); } WebSocketSession toSession=MyOnLineUserMap.getUSER_ONLINE_SESSION_MAP().get(toId); //如果不存在,或者是未开启 if(toSession==null||!toSession.isOpen()){ return ; } try { toSession.sendMessage(textMessage); } catch (IOException e) { // TODO 自动生成的 catch 块 e.printStackTrace(); } } /** * 发送给全部的用户 * @param textMessage 发送消息 */ private void sendMessageToAll(final TextMessage textMessage) { //遍历所有的在线用户,包括自己 for(Map.Entry<String,WebSocketSession> wsSession:MyOnLineUserMap.getUSER_ONLINE_SESSION_MAP().entrySet()){ //获取 WebSocketSession WebSocketSession onLineSession=wsSession.getValue(); //是打开的状态 if(onLineSession.isOpen()){ //开启线程 new Thread(new Runnable() { @Override public void run() { if(onLineSession.isOpen()){ //发送消息 try { onLineSession.sendMessage(textMessage); System.out.println("发送消息成功"); } catch (IOException e) { e.printStackTrace(); } } } }).start(); } } } }
二.四.三 编写 注册工厂 WebSocketConfig
package com.yjl.websocket.websocket; import org.springframework.stereotype.Component; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; /** * 注册处理器和拦截器 * @author 两个蝴蝶飞 * */ @Component(value="webSocketConfig") //通过注解 EnableWebSocket @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer{ /** * 注册服务 */ @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new MyWebSocketHandler(),"/ws").addInterceptors(new MyHandshakeInterceptor()); /* * 在这里我们用到.withSockJS(),SockJS是spring用来处理浏览器对websocket的兼容性, * 目前浏览器支持websocket还不是很好,特别是IE11以下. * SockJS能根据浏览器能否支持websocket来提供三种方式用于websocket请求, * 三种方式分别是 WebSocket, HTTP Streaming以及 HTTP Long Polling */ registry.addHandler(new MyWebSocketHandler(),"ws/sockjs").addInterceptors(new MyHandshakeInterceptor()) .withSockJS(); } }
后端的处理,算是基本完成了。
二.五 处理前端的 main.jsp 页面
添加 js 脚本。 (不要忘记添加相应的 js 和样式表)
基本的东西,就不讲解了。
<script> var path='<%=basePath%>'; //定义MyMessage 需要用到的属性信息 //当前进入的id信息 var uid="{sessionScope.loginUser.id}"; var fromId=uid; var fromNickName='${sessionScope.loginUser.nickName}'; //默认是-1, 表示全部接收 var toId=-1; // 创建一个Socket实例 //参数为URL,ws表示WebSocket协议。onopen、onclose和onmessage方法把事件连接到Socket实例上。每个方法都提供了一个事件,以表示Socket的状态。 var webSocket; //不同浏览器的WebSocket对象类型不同 if ('WebSocket' in window) { webSocket = new WebSocket("ws://" + path + "ws"); //火狐 } else if ('MozWebSocket' in window) { webSocket = new MozWebSocket("ws://" + path + "ws"); } else { webSocket = new SockJS("http://" + path + "ws/sockjs"); } //定义四个事件, onopen,onclose,onmessage,onerror //打开Socket, webSocket.onopen = function(event) { //console.log("WebSocket:已连接"); refreshOnLineList(); } // 监听消息 //onmessage事件提供了一个data属性,它可以包含消息的Body部分。消息的Body部分必须是一个字符串,可以进行序列化/反序列化操作,以便传递更多的数据。 webSocket.onmessage = function(event) { var data=JSON.parse(event.data); //console.log("WebSocket:收到一条消息",data); //2种推送的消息 //1.用户聊天信息:发送消息触发 //2.系统消息:登录和退出触发 //判断是否是欢迎消息(没用户编号的就是欢迎消息) if(data.fromId==undefined||data.fromId==null||data.fromId==""){ //===系统消息 $("#contentUl").append("<li><b class='dateStyle'>"+data.date+"</b><em class='sysStyle'>系统消息:</em><span class='sysTextStyle'>"+data.text+"</span></li>"); }else{ //===普通消息 //处理一下个人信息的显示: if(data.fromNickName==fromNickName){ data.fromNickName="我 :"; $("#contentUl").append("<li><span style='display:block; float:right;'><em class='nickNameStyle'>"+data.fromNickName+"</em><span class='textStyle'>"+data.text+"</span><b class='dateStyle'>"+data.date+"</b></span></li><br/>"); }else{ $("#contentUl").append("<li><b class='dateStyle'>"+data.date+"</b><em class='nickNameStyle'>"+data.fromNickName+"</em><span class='textStyle'>"+data.text+"</span></li><br/>"); } } //刷新在线用户列表 refreshOnLineList(); scrollToBottom(); }; // 监听WebSocket的关闭 webSocket.onclose = function(event) { refreshOnLineList(); $("#contentUl").append("<li><b>"+new Date().Format("yyyy-MM-dd hh:mm:ss")+"</b><em>系统消息:</em><span>连接已断开!</span></li>"); scrollToBottom(); }; //监听异常 webSocket.onerror = function(event) { refreshOnLineList(); $("#contentUl").append("<li><b>"+new Date().Format("yyyy-MM-dd hh:mm:ss")+"</b><em>系统消息:</em><span>连接异常,建议重新登录</span></li>"); scrollToBottom(); }; //onload初始化 $(function(){ //发送消息 $("#sendBtn").on("click",function(){ sendMsg(); }); //给退出聊天绑定事件 $("#exitBtn").on("click",function(){ closeWebsocket(); //跳转到主页 location.href="${pageContext.request.contextPath}/index.jsp"; }); //给输入框绑定事件 $("#msg").on("keydown",function(event){ keySend(event); }); //初始化时如果有消息,则滚动条到最下面: scrollToBottom(); }); //使用ctrl+回车快捷键发送消息 function keySend(e) { var theEvent = window.event || e; var code = theEvent.keyCode || theEvent.which; if (theEvent.ctrlKey && code == 13) { var msg=$("#msg"); if (msg.innerHTML == "") { msg.focus(); return false; } sendMsg(); } } //发送消息 function sendMsg(){ //对象为空了 if(webSocket==undefined||webSocket==null){ //alert('WebSocket connection not established, please connect.'); alert('您的连接已经丢失,请退出聊天重新进入'); return; } //获取用户要发送的消息内容 var msg=$("#msg").val(); if(msg==""){ return; }else{ var data={}; data["fromId"]=fromId; data["fromNickName"]=fromNickName; data["toId"]=toId; data["text"]=msg; //发送消息 webSocket.send(JSON.stringify(data)); //发送完消息,清空输入框 $("#msg").val(""); } } //关闭Websocket连接 function closeWebsocket(){ if (webSocket != null) { webSocket.close(); webSocket = null; } } //div滚动条(scrollbar)保持在最底部 function scrollToBottom(){ //var div = document.getElementById('chatCon'); var div = document.getElementById('up'); div.scrollTop = div.scrollHeight; } //格式化日期 Date.prototype.Format = function (fmt) { //author: meizz var o = { "M+": this.getMonth() + 1, //月份 "d+": this.getDate(), //日 "h+": this.getHours(), //小时 "m+": this.getMinutes(), //分 "s+": this.getSeconds(), //秒 "q+": Math.floor((this.getMonth() + 3) / 3), //季度 "S": this.getMilliseconds() //毫秒 }; if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); for (var k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); return fmt; } /* 刷新在线用户列表 */ function refreshOnLineList(){ $.ajax({ type : "post", url : "../User/onlineList", dataType : "json", data : {} , success : function(data) { var onlineList=data.onlineList; //有值的话 if(onlineList){ $("#onlineNum").text(onlineList.length); $("#online").empty(); $.each(onlineList,function(idx,item){ var $li=$("<li><a href='javascript:void(0);' data-id='"+item.id+"'>"+item.nickName+"</a></li>"); $("#online").append($li); }) addAClickEvent($("#online li a")); } } }); /* 点击私聊事件,暂未处理 */ function addAClickEvent(target){ target.click(function(){ var clickId=target.attr("data-id"); if(clickId==fromId){ alert("自己不能跟自己聊天"); return ; } alert("你要私聊的人的id是:"+clickId); //打开模态框,输入私聊的信息,进行私聊。 //不在讲解范围之内,可看后续的聊天室项目。 return ; }) } } </script>
三. 运行服务器,测试
火狐浏览器打开网址: http://localhost:8080/chatroom/
输入昵称,“两个蝴蝶飞”
点击进入
谷歌浏览器打开网址: http://localhost:8080/chatroom/
输入昵称, “岳泽霖”
点击进入
这个时候, 两个蝴蝶飞的火狐浏览器显示:
经上一章节的各种测试行为,包括退出登录等,服务器均可以推送消息到客户端, WebSocket 功能整合成功。