本文主要是介绍websocket + stomp搭建的简易聊天室的实现,本demo聊天室实现了单、群聊天功能;demo后端服务没有使用其他存储数据来做数据支持,除了内定的好友和其好友列表之外,其他数据只有都是临时的,即服务器关掉和重启就不会存在;主要是一个demo尽可能简单明了的来介绍相关功能即可。有什么问题可以在留言哦!并在文章末尾附上demo源码下载!
一、概念介绍
WebSocket:是一种在Web应用程序中实现实时双向通信的协议。与传统的基于HTTP的请求-响应模型不同,WebSocket允许服务器主动向客户端发送消息,而不需要客户端发起请求。客户端通过发起一个特殊的握手请求来与服务器建立WebSocket连接。
TOMP:是一种简单而灵活的文本消息传递协议,用于在客户端和服务器之间进行实时通信。它基于帧(frame)的概念,用于在不同的消息中传递数据。建立在底层的WebSocket协议之上,用于在客户端和服务器之间进行异步消息传递,支持发布-订阅(pub-sub)和请求-响应(request-response)模式。还可以通过不同的命令(例如CONNECT、SEND、SUBSCRIBE、UNSUBSCRIBE、DISCONNECT等)和帧头(frame header)来定义不同类型的操作。
二、项目结构及demo演示
后端采用springboot项目结构;前端采用的vue框架搭建(后面附源码地址或文件)
演示视频
演示视频
三、后端核心代码
在创建好了springboot项目之后
1、导入依赖和模拟静态数据构建
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.27</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
(实际情况是不可能是模拟静态的数据,都是动态生成的,如添加好友、添加群等等)
package com.jdh.common;
import com.jdh.model.ChatMessage;
import com.jdh.model.User;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
/**
* @ClassName: Constant
* @Author: jdh
* @CreateTime: 2022-11-19
* @Description: 该类主要是模拟数据库等存储的相关数据信息;
* 如系统存在多少用户、每个用户存在好友和群列表、每个群用于的用户列表、用户与好友或者群对于的历史聊天信息等等
* 如果是实际情况下,这些数据就不是像下面这样以静态的方法存储的,本demo主要不像纳入第三方功能,减少配置等等
*/
@Configuration
public class Constant {
/**
* 系统中所有存储的用户列表
*/
public static final HashMap<String, User> allUserDataList = new HashMap<>();
/**
* 存储用户拥有的好友/群列表
*/
public static final HashMap<String, List<String>> ownFriendList = new HashMap<>();
/**
* 存储群含有用户列表
*/
public static final HashMap<String, List<String>> ownGroupList = new HashMap<>();
/**
* 所有用户列表
*/
public static final List<String> allUserList = new ArrayList<>();
/**
* 在线用户列表
*/
public static final List<String> onlineUserList = new ArrayList<>();
/**
* 存储当前已经连接上的用户,以连接的session为key
*/
public static final HashMap<String, User> connectUserList = new HashMap<>();
/**
* 存储用户拥有的聊天消息(仅服务启动期间有效)
*/
public static final HashMap<String, List<ChatMessage>> ownChatMsgList = new HashMap<>();
/**
* 下面主要是模拟初始化一些相关联的列表数据;
* 实际情况这些一个是存在某个专门存储这些数据的中间件或者数据库中;
* 然后他们直接的关联关系是通过各自的操作来完成的,比如添加好友、加群等等
*/
@Bean
public static void initChatRoomData() {
allUserDataList.put("A001", new User("A001", "刘一", "1"));
allUserDataList.put("A002", new User("A002", "陈二", "1"));
allUserDataList.put("A003", new User("A003", "张三", "1"));
allUserDataList.put("A004", new User("A004", "李四", "1"));
allUserDataList.put("A005", new User("A005", "王五", "1"));
allUserDataList.put("A006", new User("A006", "赵六", "1"));
allUserDataList.put("A007", new User("A007", "孙七", "1"));
allUserDataList.put("A008", new User("A008", "周八", "1"));
allUserDataList.put("A009", new User("A009", "吴九", "1"));
allUserList.addAll(Arrays.asList("A001", "A002", "A003", "A004", "A005", "A006", "A007", "A008", "A009"));
allUserDataList.put("B001", new User("B001", "交流群1", "2"));
allUserDataList.put("B002", new User("B002", "交流群2", "2"));
allUserDataList.put("B003", new User("B003", "交流群3", "2"));
//下面是对各个用户的好友列表初始化
ownFriendList.put("A003", new ArrayList<>(
Arrays.asList("A001", "A002", "A004", "A005", "A006", "A007", "A008", "A009", "B001", "B002", "B003")));
ownFriendList.put("A004", new ArrayList<>(
Arrays.asList("A001", "A002", "A003", "A005", "A006", "A007", "A008", "A009", "B001", "B002", "B003")));
ownFriendList.put("A005", new ArrayList<>(
Arrays.asList("A001", "A002", "A003", "A004", "A006", "A007", "A008", "A009", "B001", "B002", "B003")));
ownFriendList.put("A001", new ArrayList<>(Arrays.asList("A003", "A004", "A005", "A002", "A006")));
ownFriendList.put("A002", new ArrayList<>(Arrays.asList("A003", "A004", "A005", "A001")));
ownFriendList.put("A006", new ArrayList<>(Arrays.asList("A003", "A004", "A005", "A001")));
ownFriendList.put("A007", new ArrayList<>(Arrays.asList("A003", "A004", "A005", "A008", "A009", "B001", "B003")));
ownFriendList.put("A008", new ArrayList<>(Arrays.asList("A003", "A004", "A005", "A007", "A009", "B001", "B002")));
ownFriendList.put("A009", new ArrayList<>(Arrays.asList("A003", "A004", "A005", "A007", "A008", "B001", "B003")));
//下面是对群成员初始化
ownGroupList.put("B001", new ArrayList<>(Arrays.asList("A003", "A004", "A005", "A007", "A008", "A009")));
ownGroupList.put("B002", new ArrayList<>(Arrays.asList("A003", "A004", "A005", "A008")));
ownGroupList.put("B003", new ArrayList<>(Arrays.asList("A003", "A004", "A005", "A007", "A009")));
}
/**
* 传入唯一标识的数据,无论在什么组合形式下,获取唯一标识
* @param uuids
* @return
*/
public static String getOnlyTag(String ... uuids){
String str = "";
for (String uuid : uuids) {
str += uuid;
}
char[] charArray = str.toCharArray();
Arrays.sort(charArray);
return DigestUtils.md5Hex(String.valueOf(charArray));
}
}
2、配置websocket以及监听器
package com.jdh.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* @ClassName: WebsocketConfig
* @Author: jdh
* @CreateTime: 2022-11-17
* @Description: websocket + stomp 的相关配置类
*/
@Configuration
@EnableWebSocketMessageBroker //使能STOMP协议来传输基于代理(MessageBroker)的消息;controller就支持@MessageMapping,就像是使用@requestMapping一样。
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 配置消息代理
* @param config 消息代理注册对象配置
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 配置服务端推送消息给客户端的代理路径
config.enableSimpleBroker("/topic", "/own"); // 定义消息代理前缀
// 客户端向服务端发送消息需有/app 前缀
config.setApplicationDestinationPrefixes("/app"); // 定义消息请求前缀
// 指定用户发送(一对一)的前缀 /user 不配置的话,默认是 /user
// config.setUserDestinationPrefix("/user/");
}
/**
* 注册stomp端点
* @param registry stomp端点注册对象
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat") // 注册一个 WebSocket 端点 客户端连接地址ws://127.0.0.1:8097/chat
.setAllowedOrigins("*");
// .withSockJS(); // 支持 SockJS
}
/**
* 采用自定义拦截器,获取connect时候传递的参数
* 使用客户端绑定配置,
* @param registration
*/
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
// registration.interceptors(getHeaderParamInterceptor);
}
}
package com.jdh.config;
import com.alibaba.fastjson2.JSONArray;
import com.jdh.common.Constant;
import com.jdh.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.*;
import java.util.Map;
/**
* @ClassName: WebSocketEventListener
* @Author: jdh
* @CreateTime: 2022-05-11
* @Description: 对socket的连接和断连事件进行监听,这样我们才能广播用户进来和出去等操作。
*/
@Slf4j
@Component
public class WebSocketEventListener {
@Autowired
private SimpMessageSendingOperations simpMessageSendingOperations;
/**
* 有新用户连接上
*
* @param event
*/
@EventListener
public void handleWebSocketConnectListener(SessionConnectEvent event) {
// System.out.println("连接-event => " + event);
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String sessionId = event.getMessage().getHeaders().get("simpSessionId").toString();
User user = JSONArray.parseArray(
((Map) event.getMessage().getHeaders().get("nativeHeaders")).get("user").toString(), User.class).get(0);
System.out.println("用户成功连接:" + sessionId + "; 用户:" + user);
//添加到用户连接列表中
Constant.connectUserList.put(sessionId, user);
}
/**
* 有客户端断开连接
*
* @param event
*/
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
// System.out.println("断开-event => " + event);
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String sessionId = event.getSessionId();
// 在此处执行连接断开时的逻辑处理
System.out.println("用户断开连接:" + event.getSessionId());
//移除在线集合中的用户数据
Constant.onlineUserList.remove(Constant.connectUserList.get(sessionId).getUuid());
//移除用户连接列表中当前连接
Constant.connectUserList.remove(sessionId);
}
/***
* 订阅消息
* @param event
*/
@EventListener
public void handleWebSocketSubscribeListener(SessionSubscribeEvent event) {
// log.info("handleWebSocketSubscribeListener - 接收到一个订阅主题的消息:{},用户是:{}", event.getMessage(), event.getUser());
}
/***
* 取消订阅消息
* @param event
*/
@EventListener
public void handleWebSocketUnSubscribeListener(SessionUnsubscribeEvent event) {
// log.info("handleWebSocketUnSubscribeListener - 接收到一个取消订阅主题的消息:{},用户是:{}", event.getMessage(), event.getUser());
}
}
3、相关的实体类
package com.jdh.model;
import lombok.Data;
/**
* @ClassName: ChatMessage
* @Author: jdh
* @CreateTime: 2022-11-17
* @Description: 用于发送消息时的数据载体
*/
@Data
public class ChatMessage {
/**
* 发送者的uuid,或是账号id,或是唯一标识
*/
private String senderUuid;
/**
* 发送者名称
*/
private String senderName;
/**
* 发送时间
*/
private String sendTime;
/**
* 发送消息内容
*/
private String sendContent;
/**
* 接受者uuid,选中当前聊天室的用户uuid
*/
private String receiverUuid;
/**
* 接受者名称,选中当前聊天室的用户名称
*/
private String receiverName;
/**
* 当前消息类型,1-单聊 2-群聊
*/
private String msgType;
}
package com.jdh.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @ClassName: User
* @Author: jdh
* @CreateTime: 2022-11-19
* @Description: 所有用户数据,这里群也认为是一个用户
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
/**
* 用户uuid
*/
private String uuid;
/**
* 用户名称
*/
private String name;
/**
* 用户类型 1-个人 2-群
*/
private String type;
}
package com.jdh.vo;
import lombok.Data;
/**
* @ClassName: UserRoomDataVo
* @Author: jdh
* @CreateTime: 2022-11-19
* @Description: 当前聊天历史消息
*/
@Data
public class UserRoomVo {
/**
* 当前用户
*/
private String uuid;
/**
* 当前聊天室,好友聊天为好友的uuid,群聊为群的uuid
*/
private String roomId;
/**
* 当前房间类型 1-好友 2-群聊
*/
private String type;
}
======================================================
package com.jdh.vo;
import com.jdh.model.User;
import lombok.Data;
import java.util.List;
/**
* @ClassName: UserRoomDataVo
* @Author: jdh
* @CreateTime: 2022-11-19
* @Description: 返回当前登录的用户信息和对于的好友列表
*/
@Data
public class UserRoomDataVo {
private User user;
private List<User> friendList;
}
4、编写控制类
其中包括了websocket的消息接收和消息转发以及常规的http请求控制类;
注意:由于是一个demo,就没有使用service层来实现业务代码了,全部在controller里面实现业务功能的。
package com.jdh.controller;
import com.jdh.common.Constant;
import com.jdh.model.ChatMessage;
import com.jdh.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @ClassName: ChatController
* @Author: jdh
* @CreateTime: 2022-11-17
* @Description: websocket 接收和转发客户端消息的api接口类
*/
@RestController
public class ChatController {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
/**
* 本demo的精髓所在,所有的聊天或者群聊都是这个接口中实现的,好友一对一聊天也是通过订阅地址的不同来完成的
* 其实可用用sendToUser来完成,但是为了方便就没有写,ChatRoomController中预留了一个@sendToUser或者convertAndSendToUser的测试接口
* @param chatMessage
* @param user
* @param headerAccessor
* @return
*/
@MessageMapping("/user.sendMessage") // 定义消息发送的请求地址
public String userSendMessage(@Payload ChatMessage chatMessage, User user, SimpMessageHeaderAccessor headerAccessor) {
String subUrl = "";
if ("1".equals(chatMessage.getMsgType())) {
String onlyTag = Constant.getOnlyTag(chatMessage.getSenderUuid(), chatMessage.getReceiverUuid());
//如果是单发,就记录发送者的消息,key采用发送者uuid+接收者uuid
List<ChatMessage> messageList = Constant.ownChatMsgList.get(onlyTag);
if (messageList == null) {
messageList = new ArrayList<>();
}
messageList.add(chatMessage);
Constant.ownChatMsgList.put(onlyTag, messageList);
//需要转发的地址,此处是转发给指定好友订阅的路径
subUrl = "/own/" + chatMessage.getReceiverUuid() + "/" + chatMessage.getSenderUuid() + "/messages";
} else {
//如果是群发,就记录为该群对应的消息
List<ChatMessage> messageList = Constant.ownChatMsgList.get(chatMessage.getReceiverUuid());
if (messageList == null) {
messageList = new ArrayList<>();
}
messageList.add(chatMessage);
Constant.ownChatMsgList.put(chatMessage.getReceiverUuid(), messageList);
//需要转发的地址,此处是转发给指定群消息订阅路径
subUrl = "/topic/" + chatMessage.getReceiverUuid() + "/queue/messages";
}
String info = chatMessage.getSenderUuid() + " 发送消息 '" + chatMessage.getSendContent() + "' 给 " + chatMessage.getReceiverUuid();
System.out.println(info + "; 转发路径:" + subUrl);
//因为转发的路径是不同的,所以就没有使用@sendTo或者@sendToUser注解来实现消息转发
simpMessagingTemplate.convertAndSend(subUrl, chatMessage);
return "消息推送成功!";
}
/**
* 用于stompClient.send('/app/chat.sendMessage',header, JSON.stringify(body));测试接口
* 以及消息转发的"/topic/public"的测试接口
*
* @param chatMessage
* @param user
* @return
*/
@MessageMapping("/chat.sendMessage") // 定义消息发送的请求地址
@SendTo("/topic/public") // 定义发送消息的目标地址
public ChatMessage sendMessage(@Payload ChatMessage chatMessage, User user, SimpMessageHeaderAccessor headerAccessor) {
System.out.println("系统接收到的消息:" + chatMessage);
// System.out.println(user);
ChatMessage message = new ChatMessage();
message.setSenderUuid(chatMessage.getReceiverUuid());
message.setSenderName(chatMessage.getReceiverName());
message.setSendContent("系统收到'" + chatMessage.getSendContent() + "'的消息");
message.setSendTime(new Date().toString());
message.setReceiverUuid(chatMessage.getSenderUuid());
message.setReceiverName(chatMessage.getReceiverName());
message.setMsgType("1");
System.out.println("系统回复消息:" + message);
return message;
}
/**
*
* @param authorization
* @param user
*/
@SubscribeMapping("/ws://localhost:8083/chat")
public void handleWebSocketConnect(@Header("Authorization") String authorization, @Header("user") String user) {
// 访问和使用authorization和user参数
System.out.println("Authorization: " + authorization);
System.out.println("user: " + user);
// 执行其他操作...
}
}
package com.jdh.controller;
import com.jdh.common.Constant;
import com.jdh.model.ChatMessage;
import com.jdh.model.User;
import com.jdh.util.Result;
import com.jdh.vo.UserRoomDataVo;
import com.jdh.vo.UserRoomVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @ClassName: ChatController
* @Author: jdh
* @CreateTime: 2022-11-17
* @Description: 一些常规的http请求类
*/
@RestController
@RequestMapping("/chatRoom")
public class ChatRoomController {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
/**
* 获取当前未被登录的用户信息
* @return
*/
@PostMapping("/getAllUserList")
public Result<List<User>> getAllUserList() {
HashMap<String, User> allUserData = Constant.allUserDataList;
List<String> onlineUserList = Constant.onlineUserList;
//查询出当前可用的用户集合,即未登录的账户
List<User> newUserList = allUserData.entrySet().stream()
.filter(entry -> (!onlineUserList.contains(entry.getKey()) && Constant.allUserList.contains(entry.getKey())))
.map(Map.Entry::getValue)
.collect(Collectors.toList());
return Result.success(newUserList);
}
/**
* 获取当前登录的用户信息,包括好友列表
* @param user
* @return
*/
@PostMapping("/getUserData")
public Result<UserRoomDataVo> getUserData(@RequestBody User user) {
if (Constant.onlineUserList.contains(user.getUuid())) {
return Result.error("当前账户已在别处登录;请重新获取再选择账户!");
}
User ownUser = Constant.allUserDataList.get(user.getUuid());
List<String> list = Constant.ownFriendList.get(user.getUuid());
//查询出当前用户对应的好友列表
List<User> friendList = Constant.allUserDataList.entrySet().stream()
.filter(entry -> (list.contains(entry.getKey())))
.map(Map.Entry::getValue)
.collect(Collectors.toList());
UserRoomDataVo userRoomDataVo = new UserRoomDataVo();
userRoomDataVo.setUser(ownUser);
userRoomDataVo.setFriendList(friendList);
//当前用户设置为已上线的账户
Constant.onlineUserList.add(user.getUuid());
return Result.success(userRoomDataVo);
}
/**
* 用户点击下线,通过请求告知服务器接口
* @param user
* @return
*/
@PostMapping("/offLine")
public Result<UserRoomDataVo> offLine(@RequestBody User user) {
Constant.onlineUserList.remove(user.getUuid());
String name = Constant.allUserDataList.get(user.getUuid()).getName();
return Result.success(name + "下线了");
}
/**
* 当用户选取某个好友\群聊天室的时候,获取当前用户和当前聊天室的历史消息
* @param userRoomVo
* @return
*/
@PostMapping("/getUserRoomMsg")
public Result<List<ChatMessage>> getUserRoomMsg(@RequestBody UserRoomVo userRoomVo) {
String onlyTag = "";
if ("1".equals(userRoomVo.getType())) {
onlyTag = Constant.getOnlyTag(userRoomVo.getUuid(), userRoomVo.getRoomId());
} else {
onlyTag = userRoomVo.getRoomId();
}
List<ChatMessage> chatMessages = Constant.ownChatMsgList.get(onlyTag);
return Result.success(chatMessages);
}
/**
* 用户点击刷新的时候清除所有已登录的账户,更改为未登录状态,这个接口只是方便模拟前端登录而已
* @param uuids
* @return
*/
@PostMapping("/cleanAllChatRoomData")
public Result<String> cleanAllChatRoomData(String[] uuids) {
// Constant.onlineUserList.retainAll(new ArrayList<>(uuids));
Constant.onlineUserList.clear();
return Result.success();
}
/**
* 测试消息推送接口
* @return
*/
@PostMapping("/checkChat")
public String checkChat() {
System.out.println("接口触发checkChat");
ChatMessage chatMessage = new ChatMessage();
chatMessage.setSenderUuid("0000");
chatMessage.setSenderName("system");
chatMessage.setMsgType("1");
chatMessage.setSendContent("接口触发checkChat");
simpMessagingTemplate.convertAndSend("/topic/public", chatMessage);
simpMessagingTemplate.convertAndSend("/own/A003/A004/messages", chatMessage);
// simpMessagingTemplate.convertAndSendToUser("123", "/queue/private", chatMessage);
return "触发完成";
}
}
四、前端核心代码
前端直接将整个页面的代码全部复制的,后面会有源码地址或者文件
<template>
<div>
<page-nav></page-nav>
<el-divider content-position="center">'websocket + stomp' 聊天室 demo</el-divider>
<div class="chatRoomTag">
<div class="userTag">
<!-- 头部操作栏 -->
<div class="headerHandleTag">
<div class="onlineChatTag">
<el-button type="primary" icon="el-icon-switch-button" size="mini" plain @click="userFirstOnlineHandle()" class="onlineChatTag">
{{ this.userFirst.isOnline ? '下线' : '上线' }}</el-button>
</div>
<div class="chatUserTag">
<el-tag type="" size="medium" class="chatUserTag">
<i class="el-icon-chat-dot-square"></i> {{ this.userFirst.chatRoomTitle }}
</el-tag>
</div>
<div class="userNameTag">
<el-tag type="" size="medium" class="userNameTag">
<i class="el-icon-user-solid"></i> {{ this.userFirst.userName }}
</el-tag>
</div>
</div>
<!-- 聊天室主体栏 -->
<div class="bodyHandleTag">
<!-- 好友列表 -->
<div class="friendListTag">
<el-table :data="this.userFirst.friendList" :show-header="false" :stripe="true" height="750" style="width: 100%">
<el-table-column label="好友列表" show-overflow-tooltip width="120">
<template slot-scope="scope">
<i class="el-icon-user"></i>
<el-button type="text" size="mini" @click="userFirstOpenFriendRoomHandle(scope.$index, scope.row)">
{{ scope.row.type == 2 ? '(群)' : '' }}{{ scope.row.name }}</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 聊天消息主体 -->
<div class="chatRoomMsgTag">
<div class="selectLoginUserTag" v-if="!this.userFirst.isOnline">
<el-button plain @click="userFirstSelectUserListHandle()">点击获取可用账户</el-button>
<el-select v-model="userFirst.uuid" placeholder="请选择一个账号..." style="width: 150px;">
<el-option v-for="item in this.userFirst.allUserList" :key="item.uuid" :label="item.name" :value="item.uuid"></el-option>
</el-select>
<el-button type="primary" icon="el-icon-switch-button" size="medium" plain @click="userFirstOnlineHandle()" class="onlineChatTag">上线</el-button>
</div>
<!-- 消息展示区域 -->
<div ref="chatMsgTag" class="chatMsgTag" v-if="this.userFirst.isOnline">
<ul ref="recMsgRef1" class="recMsgTag">
<li v-for="(item, index) in this.userFirst.chatMsgList" :key="index"
:style="{'text-align': (userFirst.uuid == item.senderUuid) ? 'right' : 'left'}">
<span span v-if="userFirst.uuid !== item.senderUuid" class="pointTag"></span>
<span style="color: rgb(159, 110, 207);font-size: 12px;">{{item.senderName + ' (' + item.sendTime + ')' }}</span>
<span span v-if="userFirst.uuid == item.senderUuid" class="pointTag"></span>
<br/><span style="color: rgb(123, 12, 12);font-family: Arial, Helvetica, sans-serif;">{{ item.sendContent }}</span>
</li>
</ul>
</div>
<!-- 发送消息区域 -->
<div class="sendMsgTag" v-if="this.userFirst.isOnline">
<el-input v-model="userFirst.sendMsg" size="small" @keydown.enter.native="userFirstSendMsgHandle()" placeholder="输入聊天..." style="width: 85%;"></el-input>
<el-button size="small" @click="userFirstSendMsgHandle()">发送</el-button>
</div>
</div>
</div>
</div>
<div class="userTag">
<!-- 头部操作栏 -->
<div class="headerHandleTag">
<div class="onlineChatTag">
<el-button type="primary" icon="el-icon-switch-button" size="mini" plain @click="userSecondOnlineHandle()" class="onlineChatTag">
{{ this.userSecond.isOnline ? '下线' : '上线' }}</el-button>
</div>
<div class="chatUserTag">
<el-tag type="" size="medium" class="chatUserTag">
<i class="el-icon-chat-dot-square"></i> {{ this.userSecond.chatRoomTitle }}
</el-tag>
</div>
<div class="userNameTag">
<el-tag type="" size="medium" class="userNameTag">
<i class="el-icon-user-solid"></i> {{ this.userSecond.userName }}
</el-tag>
</div>
</div>
<!-- 聊天室主体栏 -->
<div class="bodyHandleTag">
<!-- 好友列表 -->
<div class="friendListTag">
<el-table :data="this.userSecond.friendList" :show-header="false" :stripe="true" height="750" style="width: 100%">
<el-table-column label="好友列表" show-overflow-tooltip width="120">
<template slot-scope="scope">
<i class="el-icon-user"></i>
<el-button type="text" size="mini" @click="userSecondOpenFriendRoomHandle(scope.$index, scope.row)">
{{ scope.row.type == 2 ? '(群)' : '' }}{{ scope.row.name }}</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 聊天消息主体 -->
<div class="chatRoomMsgTag">
<div class="selectLoginUserTag" v-if="!this.userSecond.isOnline">
<el-button plain @click="userSecondSelectUserListHandle()">点击获取可用账户</el-button>
<el-select v-model="userSecond.uuid" placeholder="请选择一个账号..." style="width: 150px;">
<el-option v-for="item in this.userSecond.allUserList" :key="item.uuid" :label="item.name" :value="item.uuid"></el-option>
</el-select>
<el-button type="primary" icon="el-icon-switch-button" size="medium" plain @click="userSecondOnlineHandle()" class="onlineChatTag">上线</el-button>
</div>
<!-- 消息展示区域 -->
<div ref="chatMsgTag" class="chatMsgTag" v-if="this.userSecond.isOnline">
<ul ref="recMsgRef2" class="recMsgTag">
<li v-for="(item, index) in this.userSecond.chatMsgList" :key="index"
:style="{'text-align': (userSecond.uuid == item.senderUuid) ? 'right' : 'left'}">
<span span v-if="userSecond.uuid !== item.senderUuid" class="pointTag"></span>
<span style="color: rgb(159, 110, 207);font-size: 12px;">{{item.senderName + ' (' + item.sendTime + ')' }}</span>
<span span v-if="userSecond.uuid == item.senderUuid" class="pointTag"></span>
<br/><span style="color: rgb(123, 12, 12);font-family: Arial, Helvetica, sans-serif;">{{ item.sendContent }}</span>
</li>
</ul>
</div>
<!-- 发送消息区域 -->
<div class="sendMsgTag" v-if="this.userSecond.isOnline">
<el-input v-model="userSecond.sendMsg" size="small" @keydown.enter.native="userSecondSendMsgHandle()" placeholder="输入聊天..." style="width: 85%;"></el-input>
<el-button size="small" @click="userSecondSendMsgHandle()">发送</el-button>
</div>
</div>
</div>
</div>
<div class="userTag">
<!-- 头部操作栏 -->
<div class="headerHandleTag">
<div class="onlineChatTag">
<el-button type="primary" icon="el-icon-switch-button" size="mini" plain @click="userThirdOnlineHandle()" class="onlineChatTag">
{{ this.userThird.isOnline ? '下线' : '上线' }}</el-button>
</div>
<div class="chatUserTag">
<el-tag type="" size="medium" class="chatUserTag">
<i class="el-icon-chat-dot-square"></i> {{ this.userThird.chatRoomTitle }}
</el-tag>
</div>
<div class="userNameTag">
<el-tag type="" size="medium" class="userNameTag">
<i class="el-icon-user-solid"></i> {{ this.userThird.userName }}
</el-tag>
</div>
</div>
<!-- 聊天室主体栏 -->
<div class="bodyHandleTag">
<!-- 好友列表 -->
<div class="friendListTag">
<el-table :data="this.userThird.friendList" :show-header="false" :stripe="true" height="750" style="width: 100%">
<el-table-column label="好友列表" show-overflow-tooltip width="120">
<template slot-scope="scope">
<i class="el-icon-user"></i>
<el-button type="text" size="mini" @click="userThirdOpenFriendRoomHandle(scope.$index, scope.row)">
{{ scope.row.type == 2 ? '(群)' : '' }}{{ scope.row.name }}</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 聊天消息主体 -->
<div class="chatRoomMsgTag">
<div class="selectLoginUserTag" v-if="!this.userThird.isOnline">
<el-button plain @click="userThirdSelectUserListHandle()">点击获取可用账户</el-button>
<el-select v-model="userThird.uuid" placeholder="请选择一个账号..." style="width: 150px;">
<el-option v-for="item in this.userThird.allUserList" :key="item.uuid" :label="item.name" :value="item.uuid"></el-option>
</el-select>
<el-button type="primary" icon="el-icon-switch-button" size="medium" plain @click="userThirdOnlineHandle()" class="onlineChatTag">上线</el-button>
</div>
<!-- 消息展示区域 -->
<div ref="chatMsgTag" class="chatMsgTag" v-if="this.userThird.isOnline">
<ul ref="recMsgRef3" class="recMsgTag">
<li v-for="(item, index) in this.userThird.chatMsgList" :key="index"
:style="{'text-align': (userThird.uuid == item.senderUuid) ? 'right' : 'left'}">
<span span v-if="userThird.uuid !== item.senderUuid" class="pointTag"></span>
<span style="color: rgb(159, 110, 207);font-size: 12px;">{{item.senderName + ' (' + item.sendTime + ')' }}</span>
<span span v-if="userThird.uuid == item.senderUuid" class="pointTag"></span>
<br/><span style="color: rgb(123, 12, 12);font-family: Arial, Helvetica, sans-serif;">{{ item.sendContent }}</span>
</li>
</ul>
</div>
<!-- 发送消息区域 -->
<div class="sendMsgTag" v-if="this.userThird.isOnline">
<el-input v-model="userThird.sendMsg" size="small" @keydown.enter.native="userThirdSendMsgHandle()" placeholder="输入聊天..." style="width: 85%;"></el-input>
<el-button size="small" @click="userThirdSendMsgHandle()">发送</el-button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
// import { Client, Message } from '@stomp/stompjs';
export default {
name: 'StudyDemoChatRoom',
data() {
return {
urlPrefix: 'http://localhost:8083',
socket: null,
stompClient: null,
subscription: null,
userFirst:{
isOnline: false,
stompClient: null,
subscription: null,
checkSub: null,
chatRoomTitle: '聊天室',//当前正在聊天的聊天室
uuid: '',//当前登录的用户账户
userName: 'admin',//当前登录的用户名
sendMsg: '',//当前输入的聊天消息
allUserList:[],//当前可选的登录账号
openFriend: {},//打开的当前聊天室
friendList: [],//好友列表
chatMsgList: []//当前聊天室的聊天内容
},
userSecond:{
isOnline: false,
stompClient: null,
chatRoomTitle: '聊天室',
uuid: '',
userName: 'admin',
sendMsg: '',
allUserList:[],
openFriend: {},
friendList: [],
chatMsgList: []
},
userThird:{
isOnline: false,
stompClient: null,
chatRoomTitle: '聊天室',
uuid: '',
userName: 'admin',
sendMsg: '',
allUserList:[],
openFriend: {},
friendList: [],
chatMsgList: []
},
openFriend: {
uuid: '',
name: '',
type: ''
},
chatMsg: {
senderUuid: '',//发送者uuid
senderName: '',//发送者姓名
sendTime: '',//发送时间
sendContent: '',//发送内容
receiver: '',//接受者,即当前登录用户uuid
msgType: ''//消息类型,单聊还是群聊
}
};
},
created() {
// this.socket = new WebSocket('ws://localhost:8080/chat');
// this.stompClient = Stomp.client('ws://localhost:8083/chat')
// 添加beforeunload事件监听器
window.addEventListener('load', this.handlePageRefresh);
},
beforeDestroy() {
// 在组件销毁前移除beforeunload事件监听器
window.removeEventListener('load', this.handlePageRefresh);
},
mounted() {
},
methods: {
handlePageRefresh(){
let url = this.urlPrefix + "/chatRoom/cleanAllChatRoomData"
let uuids = [];
// uuids.push(this.userFirst.uuid)
// uuids.push(this.userSecond.uuid)
// uuids.push(this.userThird.uuid)
// console.log(uuids)
// this.$post(url,uuids,null,resp => {
// let res = resp.data
// if(res.code == 200){
// }else{
// }
// })
},
//第一个用户聊天室
userFirstSelectUserListHandle(){//获取当前可登录用户
this.getUserListHandle(this.userFirst)
},
async userFirstOnlineHandle(){//用户上线和下线
//如果已经在线,则是下线操作
if(this.userFirst.isOnline){
this.userOffline(this.userFirst)
Object.assign(this.userFirst, this.$options.data().userFirst)
return
}
//上线操作
if(!(this.userFirst.uuid == null)){
this.userOnline(this.userFirst)
}
},
userFirstOpenFriendRoomHandle(index,row){//选取好友列表进行聊天
this.userFirst.chatMsgList = []
this.userFirst.openFriend = row
this.userFirst.chatRoomTitle = (row.type == 1 ? "" : "(群)") + row.name
this.openFriendRoomHandle(this.userFirst,row)
},
userFirstSendMsgHandle(){//发送消息
this.sendMsgHandle(this.userFirst)
},
//第二个用户聊天室
userSecondSelectUserListHandle(){//获取当前可登录用户
this.getUserListHandle(this.userSecond)
},
async userSecondOnlineHandle(){//用户上线和下线
//如果已经在线,则是下线操作
if(this.userSecond.isOnline){
this.userOffline(this.userSecond)
Object.assign(this.userSecond, this.$options.data().userSecond)
return
}
//上线操作
if(!(this.userSecond.uuid == null)){
this.userOnline(this.userSecond)
}
},
userSecondOpenFriendRoomHandle(index,row){//选取好友列表进行聊天
this.userSecond.chatMsgList = []
this.userSecond.openFriend = row
this.userSecond.chatRoomTitle = (row.type == 1 ? "" : "(群)") + row.name
this.openFriendRoomHandle(this.userSecond,row)
},
userSecondSendMsgHandle(){//发送消息
this.sendMsgHandle(this.userSecond)
},
//第三个用户聊天室
userThirdSelectUserListHandle(){//获取当前可登录用户
this.getUserListHandle(this.userThird)
},
async userThirdOnlineHandle(){//用户上线和下线
//如果已经在线,则是下线操作
if(this.userThird.isOnline){
this.userOffline(this.userThird)
Object.assign(this.userThird, this.$options.data().userThird)
return
}
//上线操作
if(!(this.userThird.uuid == null)){
this.userOnline(this.userThird)
}
},
userThirdOpenFriendRoomHandle(index,row){//选取好友列表进行聊天
this.userThird.chatMsgList = []
this.userThird.openFriend = row
this.userThird.chatRoomTitle = (row.type == 1 ? "" : "(群)") + row.name
this.openFriendRoomHandle(this.userThird,row)
},
userThirdSendMsgHandle(){//发送消息
this.sendMsgHandle(this.userThird)
},
//公共方法
getUserListHandle(user){//获取可用账号
let url = this.urlPrefix + "/chatRoom/getAllUserList"
this.$post(url,null,null,resp => {
let res = resp.data
if(res.code == 200){
user.allUserList = res.data
}else{
this.$message.error(res.msg)
}
})
},
connetChatRoom(stompClient,user){//websocket连接
return new Promise((resolve, reject) => {
// 设置WebSocket连接以及Stomp客户端方式1
// const socket = new WebSocket('ws://localhost:8080/chat');
// this.stompClient = Stomp.over(socket);
// 创建Stomp客户端方式2
// this.stompClient = Stomp.client('ws://localhost:8083/chat')
// 连接成功的回调函数
const onConnect = (frame) => {
console.log('连接成功:',frame);
resolve(true)
};
// 连接错误的回调函数
const onError = (error) => {
console.log('连接错误:',error);
resolve(false)
};
// 连接断开的回调函数
const onDisconnect = (offLine) => {
console.log('连接断开:',offLine);
};
// 建立连接
stompClient.connect({"Authorization": user.uuid, "user": JSON.stringify(user)}, onConnect, onError);
// 监听客户端断开事件
stompClient.onDisconnect = onDisconnect;
})
},
userOnline(user){//用户上线
let url = this.urlPrefix + "/chatRoom/getUserData"
this.$post(url,{uuid:user.uuid},null,async resp => {
let res = resp.data
if(res.code == 200){
user.stompClient = Stomp.client('ws://localhost:8083/chat')
console.log("连接对象:",user.stompClient)
let connect = await this.connetChatRoom(user.stompClient,{uuid: user.uuid,name: user.userName,type: res.data.user.type})
if(connect){
console.log("返回数据:",res.data)
user.friendList = res.data.friendList
user.uuid = res.data.user.uuid
user.userName = res.data.user.name
// this.userFirst.type = res.data.user.type
user.isOnline = !user.isOnline
// this.subscribeChatRoom(user.stompClient,{uuid:user.uuid,name:user.userName,type:'1'})
}else{
return
}
}else{
this.$message.error(res.msg)
return
}
})
},
userOffline(user){//用户下线
let url = this.urlPrefix + "/chatRoom/offLine"
user.stompClient.disconnect(() => {
console.log(user.uuid,' 下线了!');
//告诉服务器下线通知
this.$post(url,{uuid:user.uuid},null,resp => {
let res = resp.data
if(res.code == 200){
this.$message.success(res.msg)
}else{
this.$message.error(res.msg)
return
}
})
});
user.isOnline = !user.isOnline
},
openFriendRoomHandle(user,row){//打开好友聊天室
if(row.type == 1){
console.log(user.uuid," 当前选中的是好友:",row.name)
var destination = '/own/' + user.uuid + '/' + row.uuid + '/messages';
this.subscribeChatRoomMssages(user,destination)
}else{
console.log(user.uuid," 当前选中的是群:",row.name)
var destination = '/topic/' + row.uuid + '/queue/messages';
//监听来自当前群消息
this.subscribeChatRoomMssages(user,destination)
}
let url = this.urlPrefix + "/chatRoom/getUserRoomMsg"
let userRoomMsg = {
uuid: user.uuid,
roomId: row.uuid,
type: row.type
}
//去获取历史聊天记录
this.$post(url,userRoomMsg,null,resp => {
let res = resp.data
if(res.code == 200){
user.chatMsgList = [...res.data, ...user.chatMsgList]
console.log("历史聊天消息:",user.chatMsgList)
this.scrollToBottom()
}else{
this.$message.error(res.msg)
}
})
},
sendMsgHandle(user){//发送消息
if(user.openFriend.uuid == undefined) {
console.log("发送的消息无接收者,发送者:",user.uuid)
return
}
if(user.sendMsg == '' || user.sendMsg == undefined) {
console.log("发送的消息无数据,发送者:",user.uuid)
this.scrollToBottom()
return
}
let message = {
senderUuid: user.uuid,//发送者uuid,即当前登录用户uuid
senderName: user.userName,//发送者姓名
sendTime: this.$moment().format('MM-DD HH:mm:ss'),//发送时间
sendContent: user.sendMsg,//发送内容
receiverUuid: user.openFriend.uuid,//接受者uuid,选中当前聊天室的用户uuid
receiverName: user.openFriend.name,//接受者名称,选中当前聊天室的用户名称
msgType: user.openFriend.type//消息类型,单聊还是群聊,当前选中当前聊天室的类型
};
console.log(user.uuid," 发送的消息:",JSON.stringify(message))
//如果是群发消息,那就不在添加自己发送的消息到列表
user.openFriend.type == 1 ? user.chatMsgList.push(message) : ''
user.stompClient.send('/app/user.sendMessage', {}, JSON.stringify(message));
user.sendMsg = ''
this.scrollToBottom()
},
subscribeChatRoomMssages(user,destination){//监听聊天室消息
user.subscription = user.stompClient.subscribe(destination, function (message) {
let msg = JSON.parse(message.body)
let info = "新消息 => 当前账号:" + user.uuid + " ; 路径" + destination + " ; 接收者: " + msg.receiverUuid + " ; 发送者: " + msg.senderUuid;
console.log(info)
let chatMsg = {
senderUuid: msg.senderUuid,//发送者uuid
senderName: msg.senderName,//发送者姓名
sendTime: msg.sendTime,//发送时间
sendContent: msg.sendContent,//发送内容
receiverUuid: msg.receiverUuid,//接受者,即当前登录用户uuid
receiverName: msg.receiverName,
msgType: msg.msgType//消息类型,单聊还是群聊
}
if(msg.msgType == 1){
user.openFriend.uuid == msg.senderUuid ? user.chatMsgList.push(chatMsg) : ''
}else{
user.openFriend.uuid == msg.receiverUuid ? user.chatMsgList.push(chatMsg) : ''
}
this.scrollToBottom()
});
},
subscribeChatRoom(stompClient,user){//测试接口
stompClient.subscribe('/topic/public', function (message) {
console.log('/own/测试接口订阅的消息: ' + message.body);
// 处理接收到的消息
});
},
scrollToBottom() {
this.$nextTick(() => {
var container1 = this.$refs.recMsgRef1;
if(container1){
container1.scrollTop = container1.scrollHeight;
}
var container2 = this.$refs.recMsgRef2;
if(container2){
container2.scrollTop = container2.scrollHeight;
}
var container3 = this.$refs.recMsgRef3;
if(container3){
container3.scrollTop = container3.scrollHeight;
}
console.log(container1," = ",container2," = ",container3)
});
}
}
};
</script>
<style lang="less" scoped>
.chatRoomTag{
text-align: center;
width: 100%;
display: flex;
flex-direction: row; /*弹性布局的方向*/
.userTag{
border: 1px red solid;
flex-grow: 1;
height: 800px;
.headerHandleTag{
width: 100%;
display: flex;
// flex-direction: row; /*弹性布局的方向*/
.onlineChatTag{
display: inline-block;
width: 120px;
}
.chatUserTag{
display: inline-block;
width: 320px;
}
.userNameTag{
display: inline-block;
width: 100px;
}
}
.bodyHandleTag{
width: 100%;
display: flex;
.friendListTag{
width: 120px;
}
.chatRoomMsgTag{
// height: 100px;
width: 415px;
.selectLoginUserTag{
margin-top: 100px;
}
.chatMsgTag{
border: 1px red solid;
height: 700px;
.recMsgTag{
list-style-type: none;
list-style: none;
padding-left: 10px;
max-height: 700px;
overflow: scroll;
font-size: 12px;
.pointTag{
display: inline-block;
width: 3px;
height: 6px;
border-radius: 50%;
background-color: #0251fd;
}
}
.sendMsgTag{
display: flex;
}
}
}
}
}
}
.chat-room {
height: 200px;
overflow-y: scroll;
}
</style>
五、源码和结尾
后端gitee地址:https://gitee.com/java_utils_demo/java-websocket-chat-room-demo.git
前端gitee地址:vue2_demo: 各实现功能的前端页面支撑,采用vue2
总结:
六、附加sendToUser测试代码
候补