1. 效果展示
项目源码: https://gitee.com/wangzhi430/ChatSystem
2. 基础准备
2.1 项目创建
2.2 配置文件
spring.datasource.url=jdbc:mysql://localhost:3306/ChatSystem?characterEncoding=utf8&useSSL=true
spring.datasource.username=root
spring.datasource.password=0000
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
mybatis.mapper-locations=classpath:mapper/**Mapper.xml
debug=true
logging.level.root=INFO
logging.level.com.example.onlinemusic.mapper=debug
logging.level.druid.sql.Statement=DEBUG
logging.level.com.example=DEBUG
3. 数据库的设计与实现
数据表分为三个表,用户表、聊天关系表、聊天列表。
用户表用来存储用户的信息。
这里设计三个字段,用户Id,用户账户,用户密码。
聊天关系表用来存储聊天的两个用户的关系。
这里设计三个字段,关系Id,发送者Id,接收者Id。
聊天列表用来存储对应的聊天的信息,根据关系Id,来识别是哪两者的用户。
这里设计5个字段,列表Id,关系Id,发送用户Id,发送内容,发送时间。
实现代码:
create database if not exists ChatSystem;
use ChatSystem;
drop table if exists user;
-- 创建一个用户信息表
create table user (
userId int primary key auto_increment,
username varchar(128) unique,
password varchar(128) not null
);
drop table if exists user_link;
-- 聊天关系表
create table user_link (
`linkId` int primary key auto_increment,
`from` int not null,
`to` int not null
);
drop table if exists chat_list;
-- 聊天列表
create table chat_list(
listId int primary key auto_increment,
linkId int,
userId int,
content varchar(128) not null,
createtime datetime
);
4. 登录注册模块的设计与实现
4.1 登录注册统一响应类
这里登录注册后端返回的数据,统一是这个格式。
import lombok.Data;
@Data
public class ResponseBodyMessage<T> {
private int status;
private String message;
private T data;
public ResponseBodyMessage(int status,String message,T data) {
this.status = status;
this.message = message;
this.data = data;
}
}
4.2 BCrypt加密
4.2.1 添加依赖类
在pom.xml中添加BCrypt的依赖类
<!-- security依赖包 (加密)-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
4.2.2 在启动类中添加代码
@SpringBootApplication(exclude = {
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
4.2.3 在AppConfig类中注入Bean对象
@Configuration
public class AppConfig {
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
4.3 添加拦截器
防止未登录用户进入非法界面.
4.3.1 LoginInterceptor 类
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("user") != null){
return true;
}
response.setStatus(403);
response.sendRedirect("/login.html");
return false;
}
}
4.3.2 AppConfig 类
@Configuration
public class AppConfig implements WebMvcConfigurer{
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/**/*.js")
.excludePathPatterns("/**/*.jpg")
.excludePathPatterns("/**/*.css")
.excludePathPatterns("/**/*.png")
.excludePathPatterns("/**/login.html")
.excludePathPatterns("/**/register.html")
.excludePathPatterns("/**/login")
.excludePathPatterns("/**/register");
}
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
4.4 具体代码实现
之前代码中, 登录注册流程很清楚, 可以查看之前博客.
@RestController
public class UserController {
@Resource
private UserService userService;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@RequestMapping("/login")
public ResponseBodyMessage<Boolean> userLogin(@RequestBody User user, HttpServletRequest request) {
User truUser = userService.selectByUserName(user.getUsername());
if (truUser == null) {
System.out.println("登录失败!");
return new ResponseBodyMessage<>(-1, "用户名密码错误!", false);
} else {
boolean flg = bCryptPasswordEncoder.matches(user.getPassword(), truUser.getPassword());
if (!flg) {
return new ResponseBodyMessage<>(-1, "用户名密码错误!", false);
}
System.out.println("登录成功!");
HttpSession session = request.getSession(true);
System.out.println(session);
session.setAttribute("user", truUser);
return new ResponseBodyMessage<>(1, "登录成功!", true);
}
}
@RequestMapping("/register")
public ResponseBodyMessage<User> register(@RequestBody User user) {
if(user.getUsername() == null || "".equals(user.getUsername().trim())
|| user.getPassword() == null || "".equals(user.getPassword().trim())){
return new ResponseBodyMessage<>(-1,"输入内容为空!",null);
}
User truUser = userService.selectByUserName(user.getUsername());
if (truUser != null) {
return new ResponseBodyMessage<>(-1,"当前用户名已经存在!",null);
} else{
String password = bCryptPasswordEncoder.encode(user.getPassword());
user.setPassword(password);
userService.addUser(user);
return new ResponseBodyMessage<>(1,"注册成功!",user);
}
}
@RequestMapping("/logout")
public void userLogout(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession(false);
// 拦截器的拦截, 所以不可能出现session为空的情况
session.removeAttribute("user");
response.sendRedirect("login.html");
}
}
5. 私信模块的设计与实现
这里的私信功能, 主要运用到了WebSocket
5.1 基础配置
5.1.1 依赖类
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
5.1.2 在AppConfig中配置
@Configuration
@EnableWebSocket
public class AppConfig implements WebMvcConfigurer, WebSocketConfigurer {
@Autowired
private ChatController chatController;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatController,"/intoChat")
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
}
5.2 设计思路
在成功建立websocket连接的时候, 后端发送请求给前端, 让用户根据请求来绘制好友列表.
{
status: 1, // 这里的1为成功请求, -1 为失败的请求
message: "getUser",
users: "", // 列表用户
fromusername: "", // 当前用户
}
在绘制好好友列表之后, 可以选择好友来进行对话.在点击好友之后, 发送请求给服务器, 服务器根据两者的用户账户进行建立会话, 读取之前的聊天记录.
请求
{
from: , 发送用户账户
to: , 接收用户账户
message: "loadMessage",
}
响应
{
status: 1,
messages: "", // 以前的聊天记录, 每一条包含, 用户Id, 内容, 是否是发送者.
message: "loadMessage",
tousername: "", 接收用户账户
}
在进入聊天界面之后. 可以输入对应的消息, 然后点击发送, 进行发送消息.服务器就将消息发送给当前两者用户, 谁在线, 就更新列表. 不在线就不更新.
请求
{
from: "", // 发送用户账户
to: "", // 接收用户账户
content: "", // 发送的信息内容
message: "sendMessage"
}
响应
{
status: 1,
message: "sendMessage",
messages: "", // 发送的消息
}
用户异常退出或者用户退出的时候, 在在线状态中设置离线, 在进入的时候设置上线.
5.3 用户在线状态管理器
这里使用 ConcurrentHashMap
来进行存储, key
为用户Id, value
为WebSocketSession
主要是三个功能
① 进入私聊界面添加用户状态到哈希表中
② 退出私聊界面删除哈希表中的用户状态
③ 获取当前用户的状态.
@Component
public class OnlineUserManager {
// 哈希表存储的是用户的当前的状态,在线就存储到哈希表中
private ConcurrentHashMap<Integer, WebSocketSession> userState = new ConcurrentHashMap<>();
public void enterHall(int userId, WebSocketSession webSocketSession) {
userState.put(userId,webSocketSession);
}
public void exitHall(int userId) {
userState.remove(userId);
}
public WebSocketSession getState(int userId) {
return userState.get(userId);
}
}
5.4 设计数据库操作
主要是两个功能:
- 在点击用户头像的时候, 加载聊天界面, 并且读取聊天记录.
- 在点击发送按钮的时候, 发送消息, 并且加载消息到在线用户的窗口中.
实现功能1 需要去数据中, 查找两个用户的关系, 是否聊过天, 如果没有聊过天, 查询到的记录就是null. 如果聊过天, 根据关系去聊天列表中查询对应的记录.
功能1 涉及的数据库操作:
- 根据两个用户的Id, 在聊天关系表中查找linkId
- 根据linkId, 在聊天列表中查找对应的聊天记录
实习功能2, 需要去数据中, 查询是否两个用户聊过天, 是否存在linkId, 如果不存在, 就需要建立关系, 创建一个linkId, 并且根据linkId, 去插入数据, 如果存在, 直接根据linkId插入数据.
功能2 涉及的数据库操作:
- 在聊天关系表中, 插入一条数据, from, to分别为两者用户的Id
- 在聊天列表中, 根据linkId 插入一条数据, 添加发送该消息的用户Id,消息内容, 创建时间.
5.4.1 创建实体类
ChatList 实体类
@Data
public class ChatList {
private int listId;
private int linkId;
private int userId;
private String content;
private Timestamp createtime;
}
UserLink 实体类
@Data
public class UserLink {
private int linkId;
private int from;
private int to;
}
5.4.2 在mapper文件夹下创建对应xml
ChatListMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.ChatListMapper">
<select id="selectChat" resultType="com.example.demo.model.ChatList">
select * from chat_list where linkId = #{linkId} order by createtime asc;
</select>
<insert id="insertChat" keyProperty="listId" keyColumn="listId">
insert into chat_list(linkId,userId,content,createtime) values (#{linkId},#{fromId},#{content},#{timestamp});
</insert>
</mapper>
5.4.3 对应Mapper接口
ChatListMapper 接口
@Mapper
public interface ChatListMapper {
List<ChatList> selectChat(int linkId);
void insertChat(Integer linkId, Integer fromId, String content, Timestamp timestamp);
}
UserLinkMapper 接口
@Mapper
public interface UserLinkMapper {
Integer selectLinkId(int fromId,int toId);
void insertLink(int min, int max);
}
5.4.4 对应的Service类
ChatListService 类
@Service
public class ChatListService {
@Autowired
private ChatListMapper chatListMapper;
public List<ChatList> selectChat(int linkId){
return chatListMapper.selectChat(linkId);
}
public void insertChat(Integer linkId, Integer fromId, String content, Timestamp timestamp) {
chatListMapper.insertChat(linkId,fromId,content,timestamp);
}
}
UserLinkService
@Service
public class UserLinkService {
@Autowired
private UserLinkMapper userLinkMapper;
public Integer selectLinkId(int fromId,int toId){
return userLinkMapper.selectLinkId(fromId,toId);
}
public void insertLink(int min, int max) {
userLinkMapper.insertLink(min,max);
}
}
5.5 Controller类的实现
这里采用了webSocket方法, 主要是四个类来实现,
afterConnectionEstablished
连接成功的时候调用handleTextMessage
接收请求的时候调用handleTransportError
连接异常断开的时候调用afterConnectionClosed
连接断开的时候调用
5.5.1 连接成功的时候调用类
- 首先判断当前用户是否已经登录, 防止用户多开
- 将用户的在线状态设置为在线
- 从数据库中查找所有的用户
- 设置响应类, 并添加对应的信息
- 返回响应.
// 连接成功调用
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
ResponseMessage responseMessage = new ResponseMessage();
// 1. 首先判断当前用户是否已经登录, 防止用户多开
User user = (User) session.getAttributes().get("user");
if(onlineUserManager.getState(user.getUserId()) != null) {
responseMessage.setStatus(-1);
responseMessage.setMessage("当前用户已经登录了, 不要重复登录");
session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
return;
}
// 2. 将用户的在线状态设置为在线
onlineUserManager.enterHall(user.getUserId(),session);
// 3. 从数据库中查找所有的用户
// 4. 设置响应类, 并添加对应的信息
responseMessage.setStatus(1);
responseMessage.setMessage("getUser");
responseMessage.setFromusername(user.getUsername());
responseMessage.setUsers(userService.selectAllUser(user.getUsername()));
// 5. 返回响应
session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
}
5.5.2 接收请求的时候调用类
- 解析请求的内容
- 判断是加载消息记录, 还是发送消息
加载消息
2.a.1 根据两者的用户Id 查看 linkId, 这里让from始终最小,to始终最大,方便查找
2.a.2 判断当前的linkId是否存在, 不存在就不需要加载聊天记录了
2.a.3 存在聊天记录,需要加载
2.a.4 设置对应的响应, 并返回
发送消息
2.b.1 查找对应的linkId
2.b.2 判断当前linkId是否为空, 为空需要创建linkId
2.b.3 根据linkId, 在聊天列表中添加数据
2.b.4 设置对应的响应信息
2.b.5 获取两者用户的session, 并判断是否在线, 给在线的用户返回响应,刷新聊天框
// 连接成功收到的响应
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
ResponseMessage responseMessage = new ResponseMessage();
User user = (User) session.getAttributes().get("user");
// 1. 解析请求的内容
String payload = message.getPayload();
System.out.println(payload);
RequestMessage requestMessage = objectMapper.readValue(payload,RequestMessage.class);
// 2. 判断是加载消息记录, 还是发送消息
if(requestMessage.getMessage().equals("loadMessage")){
// 2.a.1 根据两者的用户Id 查看 linkId, 这里让from始终最小,to始终最大,方便查找
responseMessage.setMessage("loadMessage");
responseMessage.setTousername(requestMessage.getTo());
Integer fromId = userService.selectUserId(requestMessage.getFrom());
Integer toId = userService.selectUserId(requestMessage.getTo());
int min = Math.min(fromId,toId);
int max = Math.max(fromId,toId);
Integer linkId = userLinkService.selectLinkId(min,max);
// 2.a.2 判断当前的linkId是否存在, 不存在就不需要加载聊天记录了
if(linkId == null || linkId == 0) {
responseMessage.setStatus(1);
responseMessage.setMessages(null);
}else{
// 2.a.3 存在聊天记录,需要加载,
responseMessage.setStatus(1);
List<ChatList> chatLists = chatListService.selectChat(linkId);
List<Message> messages = new ArrayList<>();
for(ChatList chatList : chatLists) {
Message message1 = new Message();
message1.setMessage(chatList.getContent());
message1.setUserId(chatList.getUserId());
message1.setSender(chatList.getUserId() == user.getUserId());
messages.add(message1);
}
responseMessage.setMessages(messages);
}
// 2.a.4 设置对应的响应, 并返回
session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
}
if(requestMessage.getMessage().equals("sendMessage")) {
// 2.b.1 查找对应的linkId
Integer fromId = userService.selectUserId(requestMessage.getFrom());
Integer toId = userService.selectUserId(requestMessage.getTo());
int min = Math.min(fromId,toId);
int max = Math.max(fromId,toId);
Integer linkId = userLinkService.selectLinkId(min,max);
// 2.b.2 判断当前linkId是否为空, 为空需要创建linkId
if(linkId == null || linkId == 0) {
responseMessage.setStatus(1);
userLinkService.insertLink(min,max);
linkId = userLinkService.selectLinkId(min,max);
}
// 2.b.3 根据linkId, 在聊天列表中添加数据
String content = requestMessage.getContent();
chatListService.insertChat(linkId,fromId,content,new Timestamp(System.currentTimeMillis()));
// 2.b.4 设置对应的响应信息
responseMessage.setStatus(1);
responseMessage.setMessage("sendMessage");
Message message1 = new Message();
// 2.b.5 获取两者用户的session, 并判断是否在线, 给在线的用户返回响应, 刷新聊天框
WebSocketSession session1 = onlineUserManager.getState(fromId);
WebSocketSession session2 = onlineUserManager.getState(toId);
if(session1 != null) {
message1.setSender(true);
message1.setUserId(fromId);
message1.setMessage(content);
List<Message> list = new ArrayList<>();
list.add(message1);
responseMessage.setMessages(list);
session1.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
}
if(session2 != null) {
message1.setSender(false);
message1.setUserId(toId);
message1.setMessage(content);
List<Message> list = new ArrayList<>();
list.add(message1);
responseMessage.setMessages(list);
session2.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
}
}
}
5.5.3 连接异常断开的时候调用
// 连接异常调用
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
User user = (User) session.getAttributes().get("user");
WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId());
if(webSocketSession == session) {
// 2. 设置在线状态
onlineUserManager.exitHall(user.getUserId());
}
System.out.println("用户"+user.getUsername()+"退出");
}
5.5.4 连接断开的时候调用
// 连接关闭调用
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
User user = (User) session.getAttributes().get("user");
WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId());
if(webSocketSession == session) {
// 2. 设置在线状态
onlineUserManager.exitHall(user.getUserId());
}
System.out.println("用户"+user.getUsername()+"退出");
}
5.5 前端代码
5.5.1 html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>好友界面</title>
<link rel="stylesheet" href="css/index.css">
</head>
<body>
<div class="parent">
<div class="left">
<div class="titleList">
<img src="image/头像.jpg" class="pg">
<span class="username">1234124124</span>
</div>
<ul class="chatList">
<!-- <li class="touser">
<img src="image/头像.jpg" class="pg">
<div class="information">
<span class="tousername">1233</span>
<span class="preview">dmwlfmwfmw</span>
</div>
</li> -->
</ul>
</div>
<div class="right">
<!-- <div class="touserName"><span>1233123213</span></div>
<div class="MessageList">
<div class="tous">
<img src="image/头像.jpg" class="pg">
<div class="sendMes">123</div>
</div>
<div class="fromus">
<img src="image/头像.jpg" class="pg">
<div class="sendMes">123</div>
</div>
</div>
<div class="inputList">
<textarea οninput="updateNum()" maxlength="100" id="textareaContent"></textarea>
<div class="commentText">
<div class="textLine">还能输入<em>100</em>个字符</div>
<button class="sendComment">发送</button>
</div>
</div> -->
</div>
</div>
</body>
</html>
<script src="js/jquery.min.js"></script>
<script>
function updateNum() {
let text = $("#textareaContent").val();
$("em").html(100-text.length);
}
let websocketUrl = 'ws://'+ location.host +'/intoChat';
let websocket = new WebSocket(websocketUrl);
websocket.onopen = function() {
console.log("房间链接成功!");
}
websocket.onclose = function() {
console.log("房间断开链接");
}
websocket.onerror = function() {
console.log("房间出现异常");
}
window.onbeforeunload = function() {
websocket.close();
}
websocket.onmessage = function(e) {
console.log(e.data);
let resp = JSON.parse(e.data);
if(resp.status == -1) {
alert(resp.message);
location.assign("login.html");
return;
}else{
if(resp.message == "getUser"){
let username = document.querySelector('.username');
username.innerHTML = resp.fromusername;
createList(resp.users);
}
if(resp.message == "loadMessage") {
createChatList(resp);
}
if(resp.message == "sendMessage") {
let MessageList = document.querySelector('.MessageList');
for(let message of resp.messages) {
if(message.isSender){
let fromus = document.createElement('div');
fromus.className = 'fromus';
let img = document.createElement('img');
img.src='image/头像.jpg';
img.className = 'pg';
let sendMes = document.createElement('div');
sendMes.className = 'sendMes';
sendMes.innerHTML = message.message;
fromus.appendChild(img);
fromus.appendChild(sendMes);
MessageList.appendChild(fromus);
}else{
let tous = document.createElement('div');
tous.className = 'tous';
let img = document.createElement('img');
img.src='image/头像.jpg';
img.className = 'pg';
let sendMes = document.createElement('div');
sendMes.className = 'sendMes';
sendMes.innerHTML = message.message;
tous.appendChild(img);
tous.appendChild(sendMes);
MessageList.appendChild(tous);
}
}
}
}
}
function createChatList(resp){
let messages = resp.messages;
let tousername = resp.tousername;
let s = "";
s += "<div class='touserName'><span>"+tousername+"</span></div>";
s += "<div class='MessageList'>";
if(messages != null) {
for(let message of messages) {
if(message.isSender){
s+="<div class='fromus'>";
s+="<img src='image/头像.jpg' class='pg'>"
s+="<div class='sendMes'>"+message.message+"</div>"
s+="</div>";
}else{
s+="<div class='tous'>";
s+="<img src='image/头像.jpg' class='pg'>"
s+="<div class='sendMes'>"+message.message+"</div>";
s+="</div>";
}
}
}
s += "</div>";
s += "<div class='inputList'>"
s += "<textarea οninput='updateNum()' maxlength='100' id='textareaContent'></textarea>";
s += "<div class='commentText'>"
s += "<div class='textLine'>还能输入<em>100</em>个字符</div>"
s += "<button class='sendComment' οnclick=btnOn('"+tousername+"')>发送</button>";
s += "</div></div>";
$('.right').html(s);
}
function createList(users) {
let s = "";
for(let user of users) {
s += "<li class='touser' οnclick=Chat('"+user.username+"')>";
s += "<img src='image/头像.jpg' class='pg'>";
s += "<div class='information'>";
s += "<span class='tousername'>"+user.username+"</span>";
s += "<span class='preview'></span></div></li>"
}
$('.chatList').html(s);
}
function Chat(s) {
let username = document.querySelector('.username');
let req = {
from: username.textContent,
to: s,
message: "loadMessage",
}
websocket.send(JSON.stringify(req));
}
function btnOn(tousername) {
let username = document.querySelector('.username');
let text = $("#textareaContent").val().trim();
if(text == ""){
alert("请输入内容,不要输入空格");
return;
}
let req = {
from : username.textContent,
to: tousername,
content: text,
message: "sendMessage",
}
websocket.send(JSON.stringify(req));
let text2 = document.querySelector("#textareaContent");
text2.value = "";
}
</script>
5.5.2 css文件
* {
margin: 0;
padding: 0;
box-sizing: border-box;
/* background-image: url(../image/1.jpg); */
}
html,body{
height: 100%;
background-image: url(../image/1.jpg);
background-position: center center;
background-size: cover;
background-repeat: no-repeat;
}
.parent{
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
}
.left{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 80%;
width: 300px;
}
.titleList{
background-color: #2e2e2e;
width: 100%;
height: 75px;
padding-left: 10px;
display: flex;
justify-content: flex-start;
align-items: center;
border-bottom: 1px solid white;
}
.chatList{
background-color: #bdb0b0;
width: 100%;
height: calc(100% - 75px);
}
.right{
background-color: rgba(235, 229, 229, 0.9);
height: 80%;
width: 700px;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.pg{
border-radius: 50%;
height: 50px;
width: 50px;
}
.username{
color: white;
margin-left: 10px;
}
.touser{
padding: 10px;
display: flex;
justify-content: flex-start;
align-items: center;
border-bottom: 1px solid white;
}
.tousername{
color: white;
}
.preview{
color: white;
}
.information{
margin-left: 10px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
#textareaContent{
display: block;
width: 97%;
background: rgba(248,249,251,0.8);
border: none;
padding: 0 16px;
border-radius: 4px;
resize: none;
height: 88px;
font-size: 14px;
line-height: 22px;
margin: 8px;
}
.commentText{
padding: 0 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.sendComment{
display: block;
width: 77px;
height: 24px;
background: #fc5531;
color: #fff;
border-radius: 16px;
font-size: 14px;
text-align: center;
line-height: 24px;
border: none;
}
.sendComment:active{
color: #fc5531;
background: #fff;
}
.textLine{
font-size: 8px;
}
em{
color: #222226;
margin: 0 4px;
font-style: normal;
}
.touserName{
height: 64px;
width: 100%;
border-bottom: 2px solid black;
}
.touserName span{
margin-left: 14px;
line-height: 64px;
}
.MessageList{
margin: 10px;
height: 66%;
overflow:auto;
}
.inputList{
border-top: 2px solid black;
height: calc(34% - 64px);
}
.tous{
display: flex;
align-items: center;
justify-content: flex-start;
}
.fromus{
display: flex;
align-items: center;
flex-direction: row-reverse;
}
.sendMes{
margin: 10px;
line-height: 28px;
padding: 4px 12px;
color: #222226;
background: #cad9ff;
border-radius: 5px;
}
6. 群聊功能
这里页面没设计, 主要是通过在用户在线状态管理器中添加几个类, 返回在线的所有用户, 在界面的时候一个用户发送消息, 直接返回给所有的用户.
6.1 用户状态管理器
public WebSocketSession getState(int userId) {
return userState.get(userId);
}
public int getOnlinePeople() {
return userState.size();
}
6.2 代码实现
6.2.1 后端代码
package com.example.demo.controller;
import com.example.demo.model.User;
import com.example.demo.room.OnlineUserManager;
import com.example.demo.room.RequestMessage;
import com.example.demo.room.ResponseMessage;
import com.fasterxml.jackson.databind.ObjectMapper;
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.util.Collection;
@Component
public class RoomController extends TextWebSocketHandler {
private ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private OnlineUserManager onlineUserManager;
// 连接成功调用
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
User user = (User) session.getAttributes().get("user");
onlineUserManager.enterHall(user.getUserId(),session);
System.out.println("当前人数: " + onlineUserManager.getOnlinePeople());
ResponseMessage responseMessage = new ResponseMessage();
responseMessage.setMessage("people");
responseMessage.setNumber(onlineUserManager.getOnlinePeople());
Collection<WebSocketSession> collection = onlineUserManager.getAllSession();
for(WebSocketSession s : collection) {
s.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
}
}
// 连接成功收到的响应
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
User user = (User) session.getAttributes().get("user");
String payload = message.getPayload();
System.out.println(payload);
RequestMessage requestMessage = objectMapper.readValue(payload,RequestMessage.class);
ResponseMessage responseMessage = new ResponseMessage();
responseMessage.setMessage("chatMessage");
responseMessage.setContent(requestMessage.getContent());
Collection<WebSocketSession> collection = onlineUserManager.getAllSession();
for(WebSocketSession s : collection) {
s.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage)));
}
}
// 连接异常调用
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
User user = (User) session.getAttributes().get("user");
WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId());
if(webSocketSession == session) {
// 2. 设置在线状态
onlineUserManager.exitHall(user.getUserId());
}
System.out.println("用户"+user.getUsername()+"退出");
}
// 连接关闭调用
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
User user = (User) session.getAttributes().get("user");
WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId());
if(webSocketSession == session) {
// 2. 设置在线状态
onlineUserManager.exitHall(user.getUserId());
}
System.out.println("用户"+user.getUsername()+"退出");
}
}
6.2.2 前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text" id="content">
<input type="submit" id="up">
<hr>
<div id="chat">
<div id="notic">当前在线用户: <span id="totalPeople">0</span></div>
</div>
</body>
</html>
<script>
let websocketUrl = 'ws://'+ location.host +'/intoRoom';
let websocket = new WebSocket(websocketUrl);
websocket.onopen = function() {
console.log("房间链接成功!");
}
websocket.onclose = function() {
console.log("房间断开链接");
}
websocket.onerror = function() {
console.log("房间出现异常");
}
window.onbeforeunload = function() {
websocket.close();
}
websocket.onmessage = function(e) {
console.log(e.data);
let resp = JSON.parse(e.data);
let chat = document.querySelector('#chat');
let div = document.createElement('div');
if(resp.message == 'people'){
let total = document.querySelector("#totalPeople");
total.innerHTML = resp.number;
}
if(resp.message == 'chatMessage'){
div.innerHTML=resp.content;
}
chat.appendChild(div);
}
let submit = document.querySelector("#up");
submit.onclick = function() {
let content = document.querySelector("#content");
let req = {
content: content.value,
}
websocket.send(JSON.stringify(req));
}
</script>