目录
一、需求分析与演示
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, "您尚未登录!"))));
}
}
}