其实原理非常简单,就是客户端用户通过websoket来连接websocket服务端。然后服务端,收集每个用户发出的消息, 进而将每条用户的消息通过广播的形式推送到每个连接到服务端的客户端。从而实现用户的实时聊天。
// TODO : 我主要是讲一下实现思路。并未完善其功能。
1.后端
依赖
<!--websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--huttol-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.11</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
webSocket配置类
@Configuration
public class WebSocketConfig
{
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
WebSocket类
类似于controller接口,只不过这个接口,用来专门处理websoket相关的。
package com.example.websocketdemo.websocket;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.example.websocketdemo.domain.User;
import com.example.websocketdemo.domain.UserMes;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
@Component
//定义websocket服务器端,它的功能主要是将目前的类定义成一个websocket服务器端。注解的值将被用于监听用户连接的终端访问URL地址
@ServerEndpoint("/websocket")
@Slf4j
public class WebSocket {
//实例一个session,这个session是websocket的session
private Session session;
private User user; // 每个websocket连接对应的用户信息
//存放websocket的集合(本次demo不会用到,聊天室的demo会用到)
private static CopyOnWriteArraySet<WebSocket> webSocketSet = new CopyOnWriteArraySet<>();
// 用户服务器数据存储结构体
private static List<UserMes> userMess = new ArrayList<>();
public List<UserMes> getUserMess(){
return userMess;
}
//前端请求时一个websocket时
@OnOpen
public void onOpen(Session session) {
this.session = session;
webSocketSet.add(this);
log.info("【websocket消息】有新的连接, 总数:{}", webSocketSet.size());
}
//前端关闭时一个websocket时
@OnClose
public void onClose() {
webSocketSet.remove(this);
log.info("【websocket消息】连接断开, 总数:{}", webSocketSet.size());
}
//前端向后端发送消息
@OnMessage
public void onMessage(String message) {
if(isUserStr(message)){
this.user = userStrConvertUser(message);
log.info("【websocket消息】客户端发来的连接请求:{}", message);
return;
}
userMess.add(new UserMes(user.getName(),message));
log.info("【websocket消息】收到客户端发来的消息:{}", message);
}
// 判断消息中是否包含用户信息的json字符串
private boolean isUserStr(String mes){
JSONObject response;
try
{
response = JSONUtil.parseObj(mes);
if(response.containsKey("name") && response.containsKey("age"))
{
return true;
}
return false;
}catch (Exception e){
return false;
}
}
// 将包装用户信息的json字符串转化为用户对象
private User userStrConvertUser(String mes){
JSONObject res = JSONUtil.parseObj(mes);
String name = res.getStr("name");
Integer age = res.getInt("age");
return new User(name,age);
}
//新增一个方法用于主动向客户端发送消息
public void sendMessage(String message) {
for (WebSocket webSocket: webSocketSet) {
log.info("【websocket消息】广播消息, message={}", message);
try {
webSocket.session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public List<User> getUserList()
{
ArrayList<User> users = new ArrayList<>();
for (WebSocket webSocket : webSocketSet)
{
users.add(webSocket.user);
}
return users;
}
//新增一个方法用于主动向客户端发送消息
// 卧槽消息推送方法
}
WebSocketTasks
利用定时器,实现服务端向客户端消息的推送。
websocket定时器,负责处理将客户端传入服务的消息,整合推送到相应的客户端。
package com.example.websocketdemo.tasks;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.example.websocketdemo.domain.User;
import com.example.websocketdemo.domain.UserMes;
import com.example.websocketdemo.websocket.WebSocket;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
/**
* websocket定时器
*
* @author: jzm
* @date: 2024-03-05 20:02
**/
@Component
public class WebSocketTasks
{
@Resource
private WebSocket webSocket;
// 每隔10s定时推送当前用户在线人数
@Scheduled(cron = "0/10 * * * * ?")
public void sendOlineUserInfo() throws InterruptedException
{
WebSocket ws = webSocket;
List<User> userList = ws.getUserList();
JSONObject res = new JSONObject();
res.set("size",userList.size());
res.set("users",userList);
webSocket.sendMessage(JSONUtil.toJsonStr(res));
}
@Scheduled(cron = "0/10 * * * * ?")
public void sendUserList() throws InterruptedException
{
WebSocket ws = webSocket;
List<UserMes> userMess = ws.getUserMess();
ws.sendMessage(JSONUtil.toJsonStr(userMess));
}
}
设计到的用户实体类 和其他配置类
/**
* 用户
*
* @author: jzm
* @date: 2024-03-06 08:11
**/
@Data
@AllArgsConstructor
public class User
{
private String name;
private Integer age;
}
/**
* 用户消息
*
* @author: jzm
* @date: 2024-03-06 08:34
**/
@Data
@AllArgsConstructor
public class UserMes
{
private String username;
private String message;
}
我是利用vue.js搭建的前端工程,是2个服务端口。会有跨域的影响。
还有就是我服务端口是: 8089
@Configuration
public class WebMvcConfig implements WebMvcConfigurer
{
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("*")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
2.前端
Index.vue
主要是这个Index.vue。用element-ui做ui。参考下面衔接,按照官方文档自扃安装以下。另外的我的vue版本是vue2.x的。
参考: 组件 | Element
<template>
<div class="index">
<div class="box" style="border: 1px solid black">
<el-card class="box-card">
<div slot="header" class="clearfix">
<h2 style="text-align: center">聊天室首页</h2>
</div>
<div class="box-main">
<div
class="box-main-line clearfix"
v-for="(item, index) in userMess"
:key="index"
>
<span class="avatar" :style="messageStyle(item)">
<i class="el-icon-user-solid" style="font-size: 20px"></i>
<h5>{{ item.username }}</h5>
</span>
<span class="message" :style="messageStyle(item)">{{ item.message }} </span>
</div>
</div>
<br />
<div class="box-input">
<el-input placeholder="请输入内容" v-model="mes" @keyup.enter.native="sendMe">
<template slot="prepend">
<el-button type="info" round @click="sendMe">发送</el-button>
</template>
</el-input>
</div>
</el-card>
</div>
<!-- 一开始弹出表单 -->
<el-dialog
title="请输入您的信息"
:visible.sync="isShowUserPage"
:before-close="checkUser"
width="30%"
style="padding: 0 10px"
>
<el-form :model="user" status-icon label-width="100px">
<el-form-item label="用户名" prop="pass">
<el-input type="name" v-model="user.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input v-model.number="user.age"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="configUserPage">确 定</el-button>
</span>
</el-dialog>
<!-- 表格展示页 -->
<template>
<el-table :data="users" style="width: 100%">
<el-table-column prop="name" label="用户名" width="180"> </el-table-column>
<el-table-column prop="age" label="年龄" width="180"> </el-table-column>
</el-table>
</template>
</div>
</template>
<script>
export default {
name: "FrontIndex",
data() {
return {
mes: "",
websocket: null,
isShowUserPage: false,
user: {
name: "",
age: null,
},
users: [],
// 用户信息列表
userMess: [],
}
},
mounted() {
// TODO
this.configUserPage()
},
methods: {
messageStyle(item) {
return {
float: item.username == this.user.name ? "right" : "left",
textAlign: item.username == this.user.name ? "right" : "left",
}
},
sendMe() {
let websocket = this.websocket
let mes = this.mes
if (mes == "") {
this.$message.warning("不能发送空消息")
return
}
websocket.send(this.mes)
this.mes = ""
},
// 连接服务器
connectServer() {
this.websocket = new WebSocket("ws://localhost:8089/websocket")
this.handWebSocketCallback()
},
// 处理websocket 连接回调函数
handWebSocketCallback() {
let websocket = this.websocket
websocket.addEventListener("open", (e) => {
this.$message.success("用户连接成功!")
websocket.send(JSON.stringify(this.user))
this.isShowUserPage = false
})
// 监听服务器消息
websocket.addEventListener("message", (e) => {
let mes = e.data
let obj = JSON.parse(mes)
if (this.checkUsersMessage(mes)) {
this.users = obj.users
} else {
this.userMess = obj
}
})
},
// 校验这个服务器消息是不是用户列表消息
checkUsersMessage(mes) {
let obj = JSON.parse(mes)
if (obj.users != undefined) {
return true
}
return false
},
// 确定、错误输入都是校验这个
configUserPage() {
let end = this.checkUser()
if (end) {
this.connectServer()
}
},
checkUser() {
let user = this.user
if (user.name == "") {
this.$message.error("用户名不能为空")
this.isShowUserPage = true
return false
}
if (user.age == null) {
this.$message.error("年龄不能为空")
this.isShowUserPage = true
return false
}
return true
},
},
}
</script>
<style>
.clearfix:before,
.clearfix:after {
display: table;
content: "";
}
.clearfix:after {
clear: both;
}
.index {
width: 600px;
margin: 10px auto;
}
.box-main {
height: 200px;
border: 1px solid black;
overflow-y: scroll;
}
.box-input {
width: 500px;
height: 100px;
margin: 10px auto;
}
/* 隐藏滚动条,保留滚动功能 */
/* 隐藏滚动条本身 */
.box-main::-webkit-scrollbar {
width: 0;
height: 0;
}
/* 为了保留滚动功能,使用伪元素来模拟滚动条 */
.box-main::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
}
.box-main::-webkit-scrollbar-thumb {
background-color: #888;
}
.box-main-line {
margin: 10xp 0 0 0;
}
.box-main-line .avatar {
display: inline-block;
width: 50px;
height: 50px;
border: 1px solid black;
border-radius: 50%;
text-align: center;
}
.box-main-line .message {
display: inline-block;
width: 88%;
padding: 15px 0;
margin: 0 0 0 10px;
box-shadow: rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, rgba(0, 0, 0, 0.3) 0px 30px 60px -30px,
rgba(10, 37, 64, 0.35) 0px -2px 6px 0px inset;
}
</style>