赞
踩
目录
需求:实现一个 online QQ在线聊天项目,当用户登录上自己的账号后,将会显示在线,并可以向自己的好友进行在线聊天,退出登录后,将会把当前用户下线的消息推送给该用户的所有好友,并标识“下线”。
分析:以上需求中,当用户上线后,将玩家上线消息推送给他所有的好友,以及在聊天时,将消息及时的推送给好友,最核心的就是基于 WebSocket 的消息推送机制,接下里我们就来看看如何使用 WebSocket + Spring Boot 实现在线聊天功能~
js 代码编写:创建 WebSocket 实例,重写以下四个方法:
另外,我们可以再加一个方法,用来监听页面关闭(刷新、跳转)事件,进行手动关闭 websocket,为了方便处理用户下线 如下:
- //监听页面关闭事件,页面关闭之前手动操作 websocket
- window.onbeforeunload = function() {
- websocket.close();
- }
a)首先用户点击对应的聊天对象,开启聊天框(使用字符串拼接,最后使用 jQuery 的 html 方法填充即可),并使用 sessionStorage 通过对方的 id 获取聊天信息(这里我们约定以用户 id 为键,聊天信息为值进行存储)。
Ps:sessionStotage 类似于 服务器开发中的 HttpSession ,以键值对的方式通过 setItem 方法存储当前用户,通过 getItem 方法获取会话信息。
- //点击用户卡片,开启聊天框
- function startChat(nickname, photo, id) {
- //修改全局对方头像和 id
- otherPhoto = photo;
- otherId = id;
- var containerRight = "";
- containerRight += '<div class="userInfo">';
- containerRight += '<span>'+nickname+'</span>';
- containerRight += '</div>';
- containerRight += '<div class="chatList">';
- containerRight += '</div>';
- containerRight += '<div class="editor">';
- containerRight += '<textarea id="messageText" autofocus="autofocus" maxlength="500" placeholder="请在这里输入您想发送的消息~"></textarea>';
- containerRight += '<div class="sendMsg">';
- containerRight += '<button id="sendButton" onclick="sendMsg()">发送</button>';
- containerRight += '</div>';
- containerRight += '</div>';
- //拼接
- jQuery(".container-right").html(containerRight);
- //清空聊天框
-
- //使用 sessionStorage 获取对话信息
- var chatData = sessionStorage.getItem(otherId);
- if(chatData != null) {
- //说明之前有聊天
- jQuery(".chatList").html(chatData);
- }
- }
为了方便获取当前用户,和对方信息,创建以下三个全局变量:
- //自己的头像
- var selfPhoto = "";
- //对方的头像和id
- var otherPhoto = "";
- var otherId = -1;
当前用户信息通过 ajax 获取即可,如下:
- //获取当前登录用户信息
- function getCurUserInfo() {
- jQuery.ajax({
- type: "POST",
- url: "/user/info",
- data: {},
- async: false,
- success: function(result) {
- if(result != null && result.code == 200) {
- //获取成功,展示信息
- jQuery(".mycard > .photo").attr("src", result.data.photo);
- jQuery(".mycard > .username").text(result.data.nickname+"(自己)");
- //修改全局头像(selfPhoto)
- selfPhoto = result.data.photo;
- } else {
- alert("当前登录用户信息获取失败!");
- }
- }
- });
- }
- getCurUserInfo();
b)接下来就是当前用户发送消息给对方了,这时就需要用到我们的 websocket 消息推送机制,具体的,创建 websocket 实例,实现以下四大方法:
- //创建 WebSocket 实例
- //TODO: 上传云服务器的时候需要进行修改路径
- var host = window.location.host;
- var websocket = new WebSocket("ws://"+host+"/chat");
-
- websocket.onopen = function() {
- console.log("连接成功");
- }
-
- websocket.onclose = function() {
- console.log("连接关闭");
- }
-
- websocket.onerror = function() {
- console.log("连接异常");
- }
-
- //监听页面关闭事件,页面关闭之前手动操作 websocket
- window.onbeforeunload = function() {
- websocket.close();
- }
-
- //处理服务器返回的响应(一会重点实现)
- websocket.onmessage = function(e) {
- //获取服务器推送过来的消息
-
- }
接着创建一个 sendMsg() 方法,用来发送聊天信息,首先还是一如既往的非空校验(发送的聊聊天消息不能为空),接着还需要校验消息推送的工具 websocket 是否连接正常(websocket.readState == websocket.OPEN 成立表示连接正常),连接正常后,首先将用户发送的消息在客户端界面上进行展示,再将发送的消息使用 JSON 格式(JSON.stringify)进行封装,这是 websocket 的 send 方法发送消息约定的格式,包装后使用 websocket.send 发送数据,接着不要忘记使用 sessionStorage 存储当前用户发送的消息,最后清除输入框内容,如下 js 代码:
- //发送信息
- function sendMsg() {
- //非空校验
- var message = jQuery("#messageText");
- if(message.val() == "") {
- alert("发送信息不能为空!");
- return;
- }
- //触发 websocket 请求前,先检验 websocket 连接是否正常(readyState == OPEN 表示连接正常)
- if (websocket.readyState == websocket.OPEN) {
- //客户端展示
- var chatDiv = "";
- chatDiv += '<div class="self">';
- chatDiv += '<div class="msg">'+message.val()+'</div>';
- chatDiv += '<img src="'+selfPhoto+'" class="photo" alt="">';
- chatDiv += '</div>';
- jQuery(".chatList").append(chatDiv);
- //消息发送给服务器
- var json = {
- "code": otherId,
- "msg": message.val()
- };
- websocket.send(JSON.stringify(json));
- //使用 sessionStorage 存储对话信息
- var chatData = sessionStorage.getItem(otherId);
- if(chatData != null) {
- chatDiv = chatData + chatDiv;
- }
- sessionStorage.setItem(otherId, chatDiv);
- //清除输入框内容
- message.val("");
- } else {
- alert("当前您的连接已经断开,请重新登录!");
- location.assign("/login.html");
- }
- }
Ps:textarea 控件 必须要具有 name 值,才能使用 jQuery 通过 id 获取到,并使用 val() 方法获取到相应的值!!!
c)我们该如何接收对方推送过来的消息呢?这时候我们就需要来重点实现 websocket 的 onmessage 方法了~ onmessage 方法中有一个参数,这个参数便是响应信息,通过 .data 获取这个参数的 JSON 数据,这个 JSON 格式数据需要通过 JSON.parse 方法转化成 js 对象,这样就拿到了我们需要的约定的数据(约定的数据是前后端交互时同一的数据格式)~
这里的响应有以下 4 种可能,我们通过约定数据格式中的 msg 字段进行区分:
前三个响应都很好处理,这里主要讲一下第四个:“拿到聊天消息后,首先进行检验,只有对方的 id 和我们发送给对方消息时携带的对方 id 相同时,才将消息进行展示,最后使用 sessionStorage 存储对象信息”,如下代码:
- //处理服务器返回的响应
- websocket.onmessage = function(e) {
- //获取服务器推送过来的消息
- var jsonInfo = e.data;
- //这里获取到的 jsonInfo 是 JSON 格式的数据,我们需要将他转化成 js 对象
- var result = JSON.parse(jsonInfo);
- if(result != null) {
- //这里的响应有四种可能:1.初始化好友列表(init) 2.推送上线消息(online) 3.下线(offline) 4.聊天消息(msg)
- if(result.msg == "init") {
- //1.初始化好友列表
- var friendListDiv = "";
- for(var i = 0; i < result.data.length; i++) {
- //获取每一个好友信息
- var friendInfo = result.data[i];
- friendListDiv += '<div class="friend-card" id="'+friendInfo.id+'" onclick="javascript:startChat(\''+friendInfo.nickname+'\', \''+friendInfo.photo+'\','+friendInfo.id+')">';
- friendListDiv += '<img src="'+friendInfo.photo+'" class="photo" alt="">';
- friendListDiv += '<span class="username">'+friendInfo.nickname+'</span>';
- //判断是否在线
- if(friendInfo.online == "在线") {
- friendListDiv += '<span class="state" id="state-yes">'+friendInfo.online+'</span>';
- } else {
- friendListDiv += '<span class="state" id="state-no">'+friendInfo.online+'</span>';
- }
- friendListDiv += '</div>';
- }
- //拼接
- jQuery("#friends").html(friendListDiv);
- } else if(result.msg == "online") {
- //2.推送上线消息
- var state = jQuery("#"+result.data+" > .state");
- state.text("在线");
- state.attr("id", "state-yes");
- } else if(result.msg == "offline"){
- //3.推送下线消息
- var state = jQuery("#"+result.data+" > .state");
- state.text("离线");
- state.attr("id", "state-no");
- } else if(result.msg == "msg"){
- //4.聊天消息
- var chatDiv = "";
- chatDiv += '<div class="other">';
- chatDiv += '<img src="'+otherPhoto+'" class="photo" alt="">';
- chatDiv += '<div class="msg">'+result.data+'</div>';
- chatDiv += '</div>';
- //只有和我聊天的人的 id 和我们发送的对象 id 一致时,才将消息进行拼接
- if(otherId == result.code) {
- jQuery(".chatList").append(chatDiv);
- }
- //使用 sessionStorage 存储对话信息
- var chatData = sessionStorage.getItem(result.code);
- if(chatData != null) {
- chatDiv = chatData + chatDiv;
- }
- sessionStorage.setItem(result.code, chatDiv);
-
- } else {
- //5.错误情况
- alert(result.msg);
- }
- } else {
- alert("消息推送错误!");
- }
- }
这样客户端开发就完成了~(这里的 html 和 css 代码就不展示了,大家可以自己下来设计一下)
a)首先需要引入 websocket 依赖,如下:
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-websocket</artifactId>
- </dependency>
b)创建一下两个类:
1. WebSocketAPI:继承 TextWebSocketHandler ,重写那四大方法,实现相应逻辑。
WebSocketConfig:用来配置 WebSocket 的类(让 Spring 框架知道程序中使用了 WebSocket),重写 registerWebSocketHandlers 方法,就是用来注册刚刚写到的 WebSocketAPI 类,将他与客户端创建的 WebSocket 对象联系起来,如下:
2. 其中 addInterceptors(new HttpSessionHandshakeInterceptor()) 就是在把 HttpSession 中的信息注册到 WebSocketSession 中,让 WebSocketSession 能拿到 HttpSession 中的信息。
如下代码:
- package com.example.demo.config;
-
- import org.springframework.beans.factory.annotation.Autowired;
- 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 org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
-
- @Configuration
- @EnableWebSocket //让 spring 框架知道这是一个配置 WebSocket 的类
- public class WebSocketConfig implements WebSocketConfigurer {
-
- @Autowired
- private WebSocketAPI webSocketAPI;
-
- /**
- * 注意:addInterceptors(new HttpSessionHandshakeInterceptor()) 就是在把
- * HttpSession 中的信息注册到 WebSocketSession 中
- * @param registry
- */
- @Override
- public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
- registry.addHandler(webSocketAPI, "/chat").
- addInterceptors(new HttpSessionHandshakeInterceptor());
- }
-
- }
这里直接上代码,每段代码的意思我都十分详细的写在上面了,如果还有不懂的 -> 私信我~
- import com.example.demo.common.AjaxResult;
- import com.example.demo.common.AppVariable;
- import com.example.demo.common.UserSessionUtils;
- import com.example.demo.entity.ChatMessageInfo;
- import com.example.demo.entity.FollowInfo;
- import com.example.demo.entity.UserInfo;
- import com.example.demo.entity.vo.UserinfoVO;
- import com.example.demo.service.FollowService;
- import com.example.demo.service.UserService;
- import com.fasterxml.jackson.core.JsonProcessingException;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import org.springframework.beans.BeanUtils;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Component;
- 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 java.io.IOException;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.concurrent.ConcurrentHashMap;
-
- @Component
- public class WebSocketAPI extends TextWebSocketHandler {
-
- @Autowired
- private UserService userService;
-
- @Autowired
- private FollowService followService;
-
- private ObjectMapper objectMapper = new ObjectMapper();
-
- //用来存储每一个客户端对应的 websocketSession 信息
- public static ConcurrentHashMap<Integer, WebSocketSession> onlineUserManager = new ConcurrentHashMap<>();
-
- @Override
- public void afterConnectionEstablished(WebSocketSession session) throws Exception {
- //用户上线,加入到 onlineUserManager,推送上线消息给所有客户端
-
- //1.获取当前用户信息(是谁在建立连接)
- // 这里能够获取到 session 信息,依靠的是注册 websocket 的时候,
- // 加上的 addInterceptors(new HttpSessionHandshakeInterceptor()) 就是把 HttpSession 中的 Attribute 都拿给了 WebSocketSession 中
- //注意:此处的 userinfo 有可能为空,因为用户有可能直接通过 url 访问私信页面,此时 userinfo 就为 null,因此就需要 try catch
- try {
- UserInfo userInfo = (UserInfo) session.getAttributes().get(AppVariable.USER_SESSION_KEY);
- //2.先判断当前用户是否正在登录,如果是就不能进行后面的逻辑
- WebSocketSession socketSession = onlineUserManager.get(userInfo.getId());
- if(socketSession != null) {
- //当前用户已登录,就要告诉客户端重复登录了
- session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(AjaxResult.fail(403, "当前用户正在登录,禁止多开!"))));
- session.close();
- return;
- }
- //3.拿到身份信息之后就可以把当前登录用户设置成在线状态了
- onlineUserManager.put(userInfo.getId(), session);
- System.out.println("用户:" + userInfo.getUsername() + "进入聊天室");
- //4.将当前在线的用户名推送给所有的客户端
- //4.1、获取当前用户的好友(相互关注)中所有在线的用户
- //注意:这里的 init 表示告诉客户端这是在初始化好友列表
- List<ChatMessageInfo> friends = getCurUserFriend(session);
- AjaxResult ajaxResult = AjaxResult.success("init", friends);
- //把好友列表消息推送给当前用户
- session.sendMessage(new TextMessage(objectMapper.writeValueAsString(ajaxResult)));
- //将当前用户上线消息推送给所有他的好友(通过 id)
- for(ChatMessageInfo friend : friends) {
- WebSocketSession friendSession = onlineUserManager.get(friend.getId());
- if(friendSession != null) {
- friendSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.success("online", userInfo.getId()))));
- }
- }
- } catch (NullPointerException e) {
- e.printStackTrace();
- //说明此时的用户未登录
- //先通过 ObjectMapper 包装成一个 JSON 字符串
- //然后用 TextMessage 进行包装,表示是一个 文本格式的 websocket 数据包
- session.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.fail(403, "您尚未登录!"))));
- }
- }
-
- /**
- * 获取所有在线用户信息
- * @return
- */
- private List<ChatMessageInfo> getCurUserFriend(WebSocketSession session) throws IOException {
- //1.筛选出当前用户相互关注的用户
- //1.1获取当前用户所有关注的用户列表
- List<ChatMessageInfo> resUserinfoList = new ArrayList<>();
- try {
- UserInfo curUserInfo = (UserInfo) session.getAttributes().get(AppVariable.USER_SESSION_KEY);
- List<FollowInfo> followInfos = followService.getConcernListByUid(curUserInfo.getId());
- //好友列表(相互关注的用户列表)
- for(FollowInfo followInfo : followInfos) {
- //1.2获取被关注的人的 id 列表,检测是否出现了关注当前用户的人
- List<FollowInfo> otherList = followService.getConcernListByUid(followInfo.getFollow_id());
- for(FollowInfo otherInfo : otherList) {
-
- //1.3检测被关注的人是否也关注了自己
- if(followInfo.getUid().equals(otherInfo.getFollow_id())) {
- //1.4相互关注的用户
- UserInfo friendInfo = userService.getUserById(otherInfo.getUid());
- ChatMessageInfo chatMessageInfo = new ChatMessageInfo();
- chatMessageInfo.setId(friendInfo.getId());
- chatMessageInfo.setNickname(friendInfo.getNickname());
- chatMessageInfo.setPhoto(friendInfo.getPhoto());
- //设置在线信息(在 onlineUserManager 中说明在线)
- if(onlineUserManager.get(friendInfo.getId()) != null) {
- chatMessageInfo.setOnline("在线");
- } else {
- chatMessageInfo.setOnline("离线");
- }
- resUserinfoList.add(chatMessageInfo);
- }
- }
- }
- } catch (NullPointerException e) {
- e.printStackTrace();
- session.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.fail(403, "您尚未登录!"))));
- }
- return resUserinfoList;
- }
-
- @Override
- protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
- //实现处理发送消息操作
- UserInfo userInfo = (UserInfo) session.getAttributes().get(AppVariable.USER_SESSION_KEY);
- //获取客户端发送过来的数据(数据载荷)
- String payload = message.getPayload();
- //当前这个数据载荷是一个 JSON 格式的字符串,就需要解析成 Java 对象
- AjaxResult request = objectMapper.readValue(payload, AjaxResult.class);
- //对方的 id
- Integer otherId = request.getCode();
- //要发送给对方的消息
- String msg = request.getMsg();
- //将消息发送给对方
- WebSocketSession otherSession = onlineUserManager.get(otherId);
- if(otherSession == null) {
- session.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.fail(403,"对方不在线!"))));
- return;
- }
- otherSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.success(userInfo.getId(),"msg", msg))));
- }
-
- @Override
- public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
- try {
- //用户下线,从 onlineUserManager 中删除
- UserInfo userInfo = (UserInfo) session.getAttributes().get(AppVariable.USER_SESSION_KEY);
- onlineUserManager.remove(userInfo.getId());
- //通知该用户的所有好友,当前用户已下线
- List<ChatMessageInfo> friends = getCurUserFriend(session);
- //将当前用户下线消息推送给所有他的好友(通过 id)
- for(ChatMessageInfo friend : friends) {
- WebSocketSession friendSession = onlineUserManager.get(friend.getId());
- if(friendSession != null) {
- friendSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.success("offline", userInfo.getId()))));
- }
- }
- } catch(NullPointerException e) {
- e.printStackTrace();
- //说明此时的用户未登录
- //先通过 ObjectMapper 包装成一个 JSON 字符串
- //然后用 TextMessage 进行包装,表示是一个 文本格式的 websocket 数据包
- session.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.fail(403, "您尚未登录!"))));
- }
- }
-
- @Override
- public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
- try {
- //用户下线,从 onlineUserManager 中删除
- UserInfo userInfo = (UserInfo) session.getAttributes().get(AppVariable.USER_SESSION_KEY);
- onlineUserManager.remove(userInfo.getId());
- //通知该用户的所有好友,当前用户已下线
- List<ChatMessageInfo> friends = getCurUserFriend(session);
- //将当前用户下线消息推送给所有他的好友(通过 id)
- for(ChatMessageInfo friend : friends) {
- WebSocketSession friendSession = onlineUserManager.get(friend.getId());
- if(friendSession != null) {
- friendSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.success("offline", userInfo.getId()))));
- }
- }
- } catch (NullPointerException e) {
- e.printStackTrace();
- //说明此时的用户未登录
- //先通过 ObjectMapper 包装成一个 JSON 字符串
- //然后用 TextMessage 进行包装,表示是一个 文本格式的 websocket 数据包
- session.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.fail(403, "您尚未登录!"))));
- }
- }
-
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。