当前位置:   article > 正文

“编程式 WebSocket” 实现简易 online QQ在线聊天项目_nestjs 使用websockt 实现聊天

nestjs 使用websockt 实现聊天

目录

一、需求分析与演示

1.1、需求分析

1.2、效果演示

二、客户端、服务器开发

2.1、客户端开发

2.2、服务器开发


一、需求分析与演示


1.1、需求分析

需求:实现一个 online QQ在线聊天项目,当用户登录上自己的账号后,将会显示在线,并可以向自己的好友进行在线聊天,退出登录后,将会把当前用户下线的消息推送给该用户的所有好友,并标识“下线”。

分析:以上需求中,当用户上线后,将玩家上线消息推送给他所有的好友,以及在聊天时,将消息及时的推送给好友,最核心的就是基于 WebSocket 的消息推送机制,接下里我们就来看看如何使用 WebSocket + Spring Boot 实现在线聊天功能~

1.2、效果演示

二、客户端、服务器开发


2.1、客户端开发

js 代码编写:创建 WebSocket 实例,重写以下四个方法:

  • onopen:websocket 连接建立成功触发该方法.
  • onmessage(重点实现):接收到服务器 websocket 响应后触发该请求.
  • onerror:websocket 连接异常时触发该方法.
  • onclose:websocket 连接断开时触发该方法.

另外,我们可以再加一个方法,用来监听页面关闭(刷新、跳转)事件,进行手动关闭 websocket,为了方便处理用户下线 如下:

  1. //监听页面关闭事件,页面关闭之前手动操作 websocket
  2. window.onbeforeunload = function() {
  3. websocket.close();
  4. }

a)首先用户点击对应的聊天对象,开启聊天框(使用字符串拼接,最后使用 jQuery 的 html 方法填充即可),并使用 sessionStorage 通过对方的 id 获取聊天信息(这里我们约定以用户 id 为键,聊天信息为值进行存储)

Ps:sessionStotage 类似于 服务器开发中的 HttpSession ,以键值对的方式通过 setItem 方法存储当前用户,通过 getItem 方法获取会话信息。

  1. //点击用户卡片,开启聊天框
  2. function startChat(nickname, photo, id) {
  3. //修改全局对方头像和 id
  4. otherPhoto = photo;
  5. otherId = id;
  6. var containerRight = "";
  7. containerRight += '<div class="userInfo">';
  8. containerRight += '<span>'+nickname+'</span>';
  9. containerRight += '</div>';
  10. containerRight += '<div class="chatList">';
  11. containerRight += '</div>';
  12. containerRight += '<div class="editor">';
  13. containerRight += '<textarea id="messageText" autofocus="autofocus" maxlength="500" placeholder="请在这里输入您想发送的消息~"></textarea>';
  14. containerRight += '<div class="sendMsg">';
  15. containerRight += '<button id="sendButton" onclick="sendMsg()">发送</button>';
  16. containerRight += '</div>';
  17. containerRight += '</div>';
  18. //拼接
  19. jQuery(".container-right").html(containerRight);
  20. //清空聊天框
  21. //使用 sessionStorage 获取对话信息
  22. var chatData = sessionStorage.getItem(otherId);
  23. if(chatData != null) {
  24. //说明之前有聊天
  25. jQuery(".chatList").html(chatData);
  26. }
  27. }

为了方便获取当前用户,和对方信息,创建以下三个全局变量:

  1. //自己的头像
  2. var selfPhoto = "";
  3. //对方的头像和id
  4. var otherPhoto = "";
  5. var otherId = -1;

当前用户信息通过 ajax 获取即可,如下:

  1. //获取当前登录用户信息
  2. function getCurUserInfo() {
  3. jQuery.ajax({
  4. type: "POST",
  5. url: "/user/info",
  6. data: {},
  7. async: false,
  8. success: function(result) {
  9. if(result != null && result.code == 200) {
  10. //获取成功,展示信息
  11. jQuery(".mycard > .photo").attr("src", result.data.photo);
  12. jQuery(".mycard > .username").text(result.data.nickname+"(自己)");
  13. //修改全局头像(selfPhoto)
  14. selfPhoto = result.data.photo;
  15. } else {
  16. alert("当前登录用户信息获取失败!");
  17. }
  18. }
  19. });
  20. }
  21. getCurUserInfo();

b)接下来就是当前用户发送消息给对方了,这时就需要用到我们的 websocket 消息推送机制,具体的,创建 websocket 实例,实现以下四大方法:

  1. //创建 WebSocket 实例
  2. //TODO: 上传云服务器的时候需要进行修改路径
  3. var host = window.location.host;
  4. var websocket = new WebSocket("ws://"+host+"/chat");
  5. websocket.onopen = function() {
  6. console.log("连接成功");
  7. }
  8. websocket.onclose = function() {
  9. console.log("连接关闭");
  10. }
  11. websocket.onerror = function() {
  12. console.log("连接异常");
  13. }
  14. //监听页面关闭事件,页面关闭之前手动操作 websocket
  15. window.onbeforeunload = function() {
  16. websocket.close();
  17. }
  18. //处理服务器返回的响应(一会重点实现)
  19. websocket.onmessage = function(e) {
  20. //获取服务器推送过来的消息
  21. }

接着创建一个 sendMsg() 方法,用来发送聊天信息,首先还是一如既往的非空校验(发送的聊聊天消息不能为空),接着还需要校验消息推送的工具 websocket 是否连接正常(websocket.readState == websocket.OPEN 成立表示连接正常),连接正常后,首先将用户发送的消息在客户端界面上进行展示,再将发送的消息使用 JSON 格式(JSON.stringify)进行封装,这是 websocket 的 send 方法发送消息约定的格式,包装后使用 websocket.send 发送数据,接着不要忘记使用 sessionStorage 存储当前用户发送的消息,最后清除输入框内容,如下 js 代码:

  1. //发送信息
  2. function sendMsg() {
  3. //非空校验
  4. var message = jQuery("#messageText");
  5. if(message.val() == "") {
  6. alert("发送信息不能为空!");
  7. return;
  8. }
  9. //触发 websocket 请求前,先检验 websocket 连接是否正常(readyState == OPEN 表示连接正常)
  10. if (websocket.readyState == websocket.OPEN) {
  11. //客户端展示
  12. var chatDiv = "";
  13. chatDiv += '<div class="self">';
  14. chatDiv += '<div class="msg">'+message.val()+'</div>';
  15. chatDiv += '<img src="'+selfPhoto+'" class="photo" alt="">';
  16. chatDiv += '</div>';
  17. jQuery(".chatList").append(chatDiv);
  18. //消息发送给服务器
  19. var json = {
  20. "code": otherId,
  21. "msg": message.val()
  22. };
  23. websocket.send(JSON.stringify(json));
  24. //使用 sessionStorage 存储对话信息
  25. var chatData = sessionStorage.getItem(otherId);
  26. if(chatData != null) {
  27. chatDiv = chatData + chatDiv;
  28. }
  29. sessionStorage.setItem(otherId, chatDiv);
  30. //清除输入框内容
  31. message.val("");
  32. } else {
  33. alert("当前您的连接已经断开,请重新登录!");
  34. location.assign("/login.html");
  35. }
  36. }

Ps:textarea 控件 必须要具有 name 值,才能使用 jQuery 通过 id 获取到,并使用 val() 方法获取到相应的值!!!

c)我们该如何接收对方推送过来的消息呢?这时候我们就需要来重点实现 websocket 的 onmessage 方法了~ onmessage 方法中有一个参数,这个参数便是响应信息,通过 .data 获取这个参数的 JSON 数据,这个 JSON 格式数据需要通过 JSON.parse 方法转化成 js 对象,这样就拿到了我们需要的约定的数据(约定的数据是前后端交互时同一的数据格式)~ 

这里的响应有以下 4 种可能,我们通过约定数据格式中的 msg 字段进行区分:

  1. 初始化好友列表(init)
  2. 推送上线消息(online)
  3. 下线(offline)
  4. 聊天消息(msg)

前三个响应都很好处理,这里主要讲一下第四个:“拿到聊天消息后,首先进行检验,只有对方的 id 和我们发送给对方消息时携带的对方 id 相同时,才将消息进行展示,最后使用 sessionStorage 存储对象信息”,如下代码:

  1. //处理服务器返回的响应
  2. websocket.onmessage = function(e) {
  3. //获取服务器推送过来的消息
  4. var jsonInfo = e.data;
  5. //这里获取到的 jsonInfo 是 JSON 格式的数据,我们需要将他转化成 js 对象
  6. var result = JSON.parse(jsonInfo);
  7. if(result != null) {
  8. //这里的响应有四种可能:1.初始化好友列表(init) 2.推送上线消息(online) 3.下线(offline) 4.聊天消息(msg)
  9. if(result.msg == "init") {
  10. //1.初始化好友列表
  11. var friendListDiv = "";
  12. for(var i = 0; i < result.data.length; i++) {
  13. //获取每一个好友信息
  14. var friendInfo = result.data[i];
  15. friendListDiv += '<div class="friend-card" id="'+friendInfo.id+'" onclick="javascript:startChat(\''+friendInfo.nickname+'\', \''+friendInfo.photo+'\','+friendInfo.id+')">';
  16. friendListDiv += '<img src="'+friendInfo.photo+'" class="photo" alt="">';
  17. friendListDiv += '<span class="username">'+friendInfo.nickname+'</span>';
  18. //判断是否在线
  19. if(friendInfo.online == "在线") {
  20. friendListDiv += '<span class="state" id="state-yes">'+friendInfo.online+'</span>';
  21. } else {
  22. friendListDiv += '<span class="state" id="state-no">'+friendInfo.online+'</span>';
  23. }
  24. friendListDiv += '</div>';
  25. }
  26. //拼接
  27. jQuery("#friends").html(friendListDiv);
  28. } else if(result.msg == "online") {
  29. //2.推送上线消息
  30. var state = jQuery("#"+result.data+" > .state");
  31. state.text("在线");
  32. state.attr("id", "state-yes");
  33. } else if(result.msg == "offline"){
  34. //3.推送下线消息
  35. var state = jQuery("#"+result.data+" > .state");
  36. state.text("离线");
  37. state.attr("id", "state-no");
  38. } else if(result.msg == "msg"){
  39. //4.聊天消息
  40. var chatDiv = "";
  41. chatDiv += '<div class="other">';
  42. chatDiv += '<img src="'+otherPhoto+'" class="photo" alt="">';
  43. chatDiv += '<div class="msg">'+result.data+'</div>';
  44. chatDiv += '</div>';
  45. //只有和我聊天的人的 id 和我们发送的对象 id 一致时,才将消息进行拼接
  46. if(otherId == result.code) {
  47. jQuery(".chatList").append(chatDiv);
  48. }
  49. //使用 sessionStorage 存储对话信息
  50. var chatData = sessionStorage.getItem(result.code);
  51. if(chatData != null) {
  52. chatDiv = chatData + chatDiv;
  53. }
  54. sessionStorage.setItem(result.code, chatDiv);
  55. } else {
  56. //5.错误情况
  57. alert(result.msg);
  58. }
  59. } else {
  60. alert("消息推送错误!");
  61. }
  62. }

这样客户端开发就完成了~(这里的 html 和 css 代码就不展示了,大家可以自己下来设计一下)

2.2、服务器开发

a)首先需要引入 websocket 依赖,如下:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-websocket</artifactId>
  4. </dependency>

b)创建一下两个类:

1. WebSocketAPI:继承 TextWebSocketHandler ,重写那四大方法,实现相应逻辑。

WebSocketConfig:用来配置 WebSocket 的类(让 Spring 框架知道程序中使用了 WebSocket),重写 registerWebSocketHandlers 方法,就是用来注册刚刚写到的 WebSocketAPI 类,将他与客户端创建的 WebSocket 对象联系起来,如下:

2. 其中  addInterceptors(new HttpSessionHandshakeInterceptor()) 就是在把 HttpSession 中的信息注册到 WebSocketSession 中,让 WebSocketSession 能拿到 HttpSession 中的信息。

如下代码:

  1. package com.example.demo.config;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.socket.config.annotation.EnableWebSocket;
  5. import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
  6. import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
  7. import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
  8. @Configuration
  9. @EnableWebSocket //让 spring 框架知道这是一个配置 WebSocket 的类
  10. public class WebSocketConfig implements WebSocketConfigurer {
  11. @Autowired
  12. private WebSocketAPI webSocketAPI;
  13. /**
  14. * 注意:addInterceptors(new HttpSessionHandshakeInterceptor()) 就是在把
  15. * HttpSession 中的信息注册到 WebSocketSession 中
  16. * @param registry
  17. */
  18. @Override
  19. public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
  20. registry.addHandler(webSocketAPI, "/chat").
  21. addInterceptors(new HttpSessionHandshakeInterceptor());
  22. }
  23. }

这里直接上代码,每段代码的意思我都十分详细的写在上面了,如果还有不懂的 -> 私信我~

  1. import com.example.demo.common.AjaxResult;
  2. import com.example.demo.common.AppVariable;
  3. import com.example.demo.common.UserSessionUtils;
  4. import com.example.demo.entity.ChatMessageInfo;
  5. import com.example.demo.entity.FollowInfo;
  6. import com.example.demo.entity.UserInfo;
  7. import com.example.demo.entity.vo.UserinfoVO;
  8. import com.example.demo.service.FollowService;
  9. import com.example.demo.service.UserService;
  10. import com.fasterxml.jackson.core.JsonProcessingException;
  11. import com.fasterxml.jackson.databind.ObjectMapper;
  12. import org.springframework.beans.BeanUtils;
  13. import org.springframework.beans.factory.annotation.Autowired;
  14. import org.springframework.stereotype.Component;
  15. import org.springframework.web.socket.CloseStatus;
  16. import org.springframework.web.socket.TextMessage;
  17. import org.springframework.web.socket.WebSocketSession;
  18. import org.springframework.web.socket.handler.TextWebSocketHandler;
  19. import java.io.IOException;
  20. import java.util.ArrayList;
  21. import java.util.List;
  22. import java.util.concurrent.ConcurrentHashMap;
  23. @Component
  24. public class WebSocketAPI extends TextWebSocketHandler {
  25. @Autowired
  26. private UserService userService;
  27. @Autowired
  28. private FollowService followService;
  29. private ObjectMapper objectMapper = new ObjectMapper();
  30. //用来存储每一个客户端对应的 websocketSession 信息
  31. public static ConcurrentHashMap<Integer, WebSocketSession> onlineUserManager = new ConcurrentHashMap<>();
  32. @Override
  33. public void afterConnectionEstablished(WebSocketSession session) throws Exception {
  34. //用户上线,加入到 onlineUserManager,推送上线消息给所有客户端
  35. //1.获取当前用户信息(是谁在建立连接)
  36. // 这里能够获取到 session 信息,依靠的是注册 websocket 的时候,
  37. // 加上的 addInterceptors(new HttpSessionHandshakeInterceptor()) 就是把 HttpSession 中的 Attribute 都拿给了 WebSocketSession 中
  38. //注意:此处的 userinfo 有可能为空,因为用户有可能直接通过 url 访问私信页面,此时 userinfo 就为 null,因此就需要 try catch
  39. try {
  40. UserInfo userInfo = (UserInfo) session.getAttributes().get(AppVariable.USER_SESSION_KEY);
  41. //2.先判断当前用户是否正在登录,如果是就不能进行后面的逻辑
  42. WebSocketSession socketSession = onlineUserManager.get(userInfo.getId());
  43. if(socketSession != null) {
  44. //当前用户已登录,就要告诉客户端重复登录了
  45. session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(AjaxResult.fail(403, "当前用户正在登录,禁止多开!"))));
  46. session.close();
  47. return;
  48. }
  49. //3.拿到身份信息之后就可以把当前登录用户设置成在线状态了
  50. onlineUserManager.put(userInfo.getId(), session);
  51. System.out.println("用户:" + userInfo.getUsername() + "进入聊天室");
  52. //4.将当前在线的用户名推送给所有的客户端
  53. //4.1、获取当前用户的好友(相互关注)中所有在线的用户
  54. //注意:这里的 init 表示告诉客户端这是在初始化好友列表
  55. List<ChatMessageInfo> friends = getCurUserFriend(session);
  56. AjaxResult ajaxResult = AjaxResult.success("init", friends);
  57. //把好友列表消息推送给当前用户
  58. session.sendMessage(new TextMessage(objectMapper.writeValueAsString(ajaxResult)));
  59. //将当前用户上线消息推送给所有他的好友(通过 id)
  60. for(ChatMessageInfo friend : friends) {
  61. WebSocketSession friendSession = onlineUserManager.get(friend.getId());
  62. if(friendSession != null) {
  63. friendSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.success("online", userInfo.getId()))));
  64. }
  65. }
  66. } catch (NullPointerException e) {
  67. e.printStackTrace();
  68. //说明此时的用户未登录
  69. //先通过 ObjectMapper 包装成一个 JSON 字符串
  70. //然后用 TextMessage 进行包装,表示是一个 文本格式的 websocket 数据包
  71. session.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.fail(403, "您尚未登录!"))));
  72. }
  73. }
  74. /**
  75. * 获取所有在线用户信息
  76. * @return
  77. */
  78. private List<ChatMessageInfo> getCurUserFriend(WebSocketSession session) throws IOException {
  79. //1.筛选出当前用户相互关注的用户
  80. //1.1获取当前用户所有关注的用户列表
  81. List<ChatMessageInfo> resUserinfoList = new ArrayList<>();
  82. try {
  83. UserInfo curUserInfo = (UserInfo) session.getAttributes().get(AppVariable.USER_SESSION_KEY);
  84. List<FollowInfo> followInfos = followService.getConcernListByUid(curUserInfo.getId());
  85. //好友列表(相互关注的用户列表)
  86. for(FollowInfo followInfo : followInfos) {
  87. //1.2获取被关注的人的 id 列表,检测是否出现了关注当前用户的人
  88. List<FollowInfo> otherList = followService.getConcernListByUid(followInfo.getFollow_id());
  89. for(FollowInfo otherInfo : otherList) {
  90. //1.3检测被关注的人是否也关注了自己
  91. if(followInfo.getUid().equals(otherInfo.getFollow_id())) {
  92. //1.4相互关注的用户
  93. UserInfo friendInfo = userService.getUserById(otherInfo.getUid());
  94. ChatMessageInfo chatMessageInfo = new ChatMessageInfo();
  95. chatMessageInfo.setId(friendInfo.getId());
  96. chatMessageInfo.setNickname(friendInfo.getNickname());
  97. chatMessageInfo.setPhoto(friendInfo.getPhoto());
  98. //设置在线信息(在 onlineUserManager 中说明在线)
  99. if(onlineUserManager.get(friendInfo.getId()) != null) {
  100. chatMessageInfo.setOnline("在线");
  101. } else {
  102. chatMessageInfo.setOnline("离线");
  103. }
  104. resUserinfoList.add(chatMessageInfo);
  105. }
  106. }
  107. }
  108. } catch (NullPointerException e) {
  109. e.printStackTrace();
  110. session.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.fail(403, "您尚未登录!"))));
  111. }
  112. return resUserinfoList;
  113. }
  114. @Override
  115. protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
  116. //实现处理发送消息操作
  117. UserInfo userInfo = (UserInfo) session.getAttributes().get(AppVariable.USER_SESSION_KEY);
  118. //获取客户端发送过来的数据(数据载荷)
  119. String payload = message.getPayload();
  120. //当前这个数据载荷是一个 JSON 格式的字符串,就需要解析成 Java 对象
  121. AjaxResult request = objectMapper.readValue(payload, AjaxResult.class);
  122. //对方的 id
  123. Integer otherId = request.getCode();
  124. //要发送给对方的消息
  125. String msg = request.getMsg();
  126. //将消息发送给对方
  127. WebSocketSession otherSession = onlineUserManager.get(otherId);
  128. if(otherSession == null) {
  129. session.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.fail(403,"对方不在线!"))));
  130. return;
  131. }
  132. otherSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.success(userInfo.getId(),"msg", msg))));
  133. }
  134. @Override
  135. public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
  136. try {
  137. //用户下线,从 onlineUserManager 中删除
  138. UserInfo userInfo = (UserInfo) session.getAttributes().get(AppVariable.USER_SESSION_KEY);
  139. onlineUserManager.remove(userInfo.getId());
  140. //通知该用户的所有好友,当前用户已下线
  141. List<ChatMessageInfo> friends = getCurUserFriend(session);
  142. //将当前用户下线消息推送给所有他的好友(通过 id)
  143. for(ChatMessageInfo friend : friends) {
  144. WebSocketSession friendSession = onlineUserManager.get(friend.getId());
  145. if(friendSession != null) {
  146. friendSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.success("offline", userInfo.getId()))));
  147. }
  148. }
  149. } catch(NullPointerException e) {
  150. e.printStackTrace();
  151. //说明此时的用户未登录
  152. //先通过 ObjectMapper 包装成一个 JSON 字符串
  153. //然后用 TextMessage 进行包装,表示是一个 文本格式的 websocket 数据包
  154. session.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.fail(403, "您尚未登录!"))));
  155. }
  156. }
  157. @Override
  158. public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
  159. try {
  160. //用户下线,从 onlineUserManager 中删除
  161. UserInfo userInfo = (UserInfo) session.getAttributes().get(AppVariable.USER_SESSION_KEY);
  162. onlineUserManager.remove(userInfo.getId());
  163. //通知该用户的所有好友,当前用户已下线
  164. List<ChatMessageInfo> friends = getCurUserFriend(session);
  165. //将当前用户下线消息推送给所有他的好友(通过 id)
  166. for(ChatMessageInfo friend : friends) {
  167. WebSocketSession friendSession = onlineUserManager.get(friend.getId());
  168. if(friendSession != null) {
  169. friendSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.success("offline", userInfo.getId()))));
  170. }
  171. }
  172. } catch (NullPointerException e) {
  173. e.printStackTrace();
  174. //说明此时的用户未登录
  175. //先通过 ObjectMapper 包装成一个 JSON 字符串
  176. //然后用 TextMessage 进行包装,表示是一个 文本格式的 websocket 数据包
  177. session.sendMessage(new TextMessage(objectMapper.writeValueAsString(AjaxResult.fail(403, "您尚未登录!"))));
  178. }
  179. }
  180. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/601199
推荐阅读
相关标签
  

闽ICP备14008679号