目录
一、需求分析与演示
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) {
            //修改全局对方头像和 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");
            }
        }c)我们该如何接收对方推送过来的消息呢?这时候我们就需要来重点实现 websocket 的 onmessage 方法了~ onmessage 方法中有一个参数,这个参数便是响应信息,通过 .data 获取这个参数的 JSON 数据,这个 JSON 格式数据需要通过 JSON.parse 方法转化成 js 对象,这样就拿到了我们需要的约定的数据(约定的数据是前后端交互时同一的数据格式)~
这里的响应有以下 4 种可能,我们通过约定数据格式中的 msg 字段进行区分:
- 初始化好友列表(init)
- 推送上线消息(online)
- 下线(offline)
- 聊天消息(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 {
    @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, "您尚未登录!"))));
        }
    }
}




















