> 文章列表 > “编程式 WebSocket” 实现简易 online QQ在线聊天项目

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

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

目录

一、需求分析与演示

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,为了方便处理用户下线 如下:

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

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

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

        //点击用户卡片,开启聊天框function startChat(nickname, photo, id) {//修改全局对方头像和 idotherPhoto = 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 = "";//对方的头像和idvar 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");}}

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

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

  1. 初始化好友列表(init)
  2. 推送上线消息(online)
  3. 下线(offline)
  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 代码就不展示了,大家可以自己下来设计一下)

2.2、服务器开发

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 中的信息。

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

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 {@Autowiredprivate UserService userService;@Autowiredprivate FollowService followService;private ObjectMapper objectMapper = new ObjectMapper();//用来存储每一个客户端对应的 websocketSession 信息public static ConcurrentHashMap<Integer, WebSocketSession> onlineUserManager = new ConcurrentHashMap<>();@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {//用户上线,加入到 onlineUserManager,推送上线消息给所有客户端//1.获取当前用户信息(是谁在建立连接)// 这里能够获取到 session 信息,依靠的是注册 websocket 的时候,// 加上的 addInterceptors(new HttpSessionHandshakeInterceptor()) 就是把 HttpSession 中的 Attribute 都拿给了 WebSocketSession 中//注意:此处的 userinfo 有可能为空,因为用户有可能直接通过 url 访问私信页面,此时 userinfo 就为 null,因此就需要 try catchtry {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;}@Overrideprotected 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);//对方的 idInteger 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))));}@Overridepublic 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, "您尚未登录!"))));}}@Overridepublic 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, "您尚未登录!"))));}}}