失踪人口回归兄弟们,差不多连续5天没有发布博文了,许久不见。当然也是最近确实不在状态而且比较忙,所以就没有去更新博文,加上最近作业时真的多,各种大作业顶不住。
那么今天也是给大家展示一个小dome,基于SpringBoot + mybatis + websocket 做的在线聊天器。本来是要做在线群聊的,但是这个前端我是真的不想调了,本来是想嫖的页面的,结果怎么说,还是自己做吧,可控。
代码也比较简单,后端搭建写起来其实也就两三个小时,主要是前端,那玩意零零散散花了两天,然后还有这个调试,总体代码从星期一开始写,到星期三写完了,后面没空不想写,于是拖到今天调测完,砍了不少功能。
ok,我们先来看看这个效果图哈。
大概就这样,当然还有登录,注册。
大概的话就是这个样子。
OK,现在呢,咱们就进入到这个代码阶段。
首先我们先看到前端。
既然是从0开始,那么我们就从o开始说。咱们这个是使用vue2 + elementui 写的,为什么是vue2,简单,我没升级嘛。能跑就行,我也不是专门写前端的。
首先省略 使用 vue-cli 开始项目哈。
我这里就说我这里使用到的依赖。
elementUI
axios
websocket
就这几个主要的。
这里其实就三个页面。
在此之前,我们先来看看这个路由设计哈。
routes: [
path: '/',
name: 'mian',
component: main
path: '/login',
name: 'login',
component: login
path: '/register',
name: 'register',
component: register
mode: "history"
就非常的简单哈。
验证码部分
在这里我们先来说说,这个验证码部分吧。
这个呢,是由前端生成的。是这个组件在干活
<template>
<div class="s-canvas">
<canvas id="s-canvas" :width="contentWidth" :height="contentHeight"></canvas>
</div>
</template>
<script>
export default {
name: "SIdentify",
props: {
identifyCode: {
type: String,
default: '1234'
fontSizeMin: {
type: Number,
default: 25
fontSizeMax: {
type: Number,
default: 30
backgroundColorMin: {
type: Number,
default: 255
backgroundColorMax: {
type: Number,
default: 255
colorMin: {
type: Number,
default: 0
colorMax: {
type: Number,
default: 160
lineColorMin: {
type: Number,
default: 100
},lineColorMax: {
type: Number,
default: 255
dotColorMin: {
type: Number,
default: 0
dotColorMax: {
type: Number,
default: 255
contentWidth: {
type: Number,
default: 112
contentHeight: {
type: Number,
default: 31
methods: {
randomNum(min, max) {
return Math.floor(Math.random() * (max - min) + min)
randomColor(min, max) {
let r = this.randomNum(min, max)
let g = this.randomNum(min, max)
let b = this.randomNum(min, max)
return 'rgb(' + r + ',' + g + ',' + b + ')'
drawPic() {
let canvas = document.getElementById('s-canvas')
let ctx = canvas.getContext('2d')
ctx.textBaseline = 'bottom'
ctx.fillStyle = this.randomColor(this.backgroundColorMin, this.backgroundColorMax)
ctx.fillRect(0, 0, this.contentWidth, this.contentHeight)
for (let i = 0; i < this.identifyCode.length; i++) {
this.drawText(ctx, this.identifyCode[i], i)
this.drawLine(ctx)
this.drawDot(ctx)
drawText(ctx, txt, i) {
ctx.fillStyle = this.randomColor(this.colorMin, this.colorMax)
ctx.font = this.randomNum(this.fontSizeMin, this.fontSizeMax) + 'px SimHei'
let x = (i + 1) * (this.contentWidth / (this.identifyCode.length + 1))
let y = this.randomNum(this.fontSizeMax, this.contentHeight - 5)
var deg = this.randomNum(-45, 45)
ctx.translate(x, y)
ctx.rotate(deg * Math.PI / 180)
ctx.fillText(txt, 0, 0)
ctx.rotate(-deg * Math.PI / 180)
ctx.translate(-x, -y)
drawLine(ctx) {
for (let i = 0; i < 5; i++) {
ctx.strokeStyle = this.randomColor(this.lineColorMin, this.lineColorMax)
ctx.beginPath()
ctx.moveTo(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight))
ctx.lineTo(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight))
ctx.stroke()
drawDot(ctx) {
for (let i = 0; i < 80; i++) {
ctx.fillStyle = this.randomColor(0, 255)
ctx.beginPath()
ctx.arc(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight), 1, 0, 2 * Math.PI)
ctx.fill()
watch: {
identifyCode() {
this.drawPic()
mounted() {
this.drawPic()
</script>
<style scoped>
.s-canvas {
height: 38px;
.s-canvas canvas{
margin-top: 1px;
margin-left: 8px;
</style>
之后就是引用,这个是很简单的。
在开始之前,我还要说一下这个代理转发,这个代理我是直接在前端做的,我的原则是压力给到前端~
ok ,到这里咱们就能够放代码了
<template>
<el-form :model="formLogin" :rules="rules" ref="ruleForm" label-width="0px" class="login-bok">
<el-form-item prop="account">
<el-input v-model="formLogin.account" placeholder="账号">
<i slot="prepend" class="el-icon-s-custom"/>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" placeholder="密码" v-model="formLogin.password">
<i slot="prepend" class="el-icon-lock"/>
</el-input>
</el-form-item>
<el-form-item prop="code">
<el-row :span="24">
<el-col :span="12">
<el-input v-model="formLogin.code" auto-complete="off" placeholder="请输入验证码" size=""></el-input>
</el-col>
<el-col :span="12">
<div class="login-code" @click="refreshCode">
<s-identify :identifyCode="identifyCode"></s-identify>
</div>
</el-col>
</el-row>
</el-form-item>
<el-form-item>
<div class="login-btn">
<el-button type="primary" @click="goregist()" style="margin-left: auto;width: 35%" >注册</el-button>
<el-button type="primary" @click="submitForm()" style="margin-left: 27%;width: 35%">登录</el-button>
</div>
</el-form-item>
</el-form>
</div>
</template>
<script>
import SIdentify from "../../components/SIdentify";
export default {
name: "login",
components: { SIdentify },
data() {
return{
formLogin: {
account: "",
password: "",
code: "",
token: '',
success: '',
identifyCodes: '1234567890abcdefjhijklinopqrsduvwxyz',
identifyCode: '',
rules: {
account:
{ required: true, message: "请输入用户名", trigger: "blur" }
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
code: [{ required: true, message: "请输入验证码", trigger: "blur" }]
mounted () {
this.identifyCode = ''
this.makeCode(this.identifyCodes, 4)
methods:{
refreshCode () {
this.identifyCode = ''
this.makeCode(this.identifyCodes, 4)
makeCode (o, l) {
for (let i = 0; i < l; i++) {
this.identifyCode += this.identifyCodes[this.randomNum(0, this.identifyCodes.length)]
randomNum (min, max) {
return Math.floor(Math.random() * (max - min) + min)
goregist(){
this.$router.push("/register")
logincount(){
this.axios({
url: "/boot/login",
method: 'post',
headers: { "type": "hello" },
data: {
account: this.formLogin.account,
password: this.formLogin.password.toLowerCase(),
}).then(res =>{
this.formLogin.success = res.data.success
this.formLogin.token = res.data.token
if(this.formLogin.success =='1'){
localStorage.setExpire("token",this.formLogin.token,604800000);
alert("登录成功~")
this.$router.push("/")
else {
alert("用户名或密码错误!")
submitForm(){
if (this.formLogin.code.toLowerCase() !== this.identifyCode.toLowerCase()) {
this.$message.error('请填写正确验证码')
this.refreshCode()
else {
this.logincount()
</script>
<style scoped>
.login-bok{
width: 30%;
margin: 150px auto;
border: 1px solid #DCDFE6;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 30px #DCDFE6;
</style>
<template>
<el-form :model="formRegist" :rules="rules" ref="ruleForm" label-width="0px" class="login-bok">
<el-form-item prop="account">
<el-input v-model="formRegist.account" placeholder="创建账号" :maxlength="16" >
<i slot="prepend" class="el-icon-s-custom"/>
</el-input>
</el-form-item>
<el-form-item prop="username">
<el-input v-model="formRegist.username" placeholder="创建用户" :maxlength="16" >
<i slot="prepend" class="el-icon-s-custom"/>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" placeholder="输入密码" :maxlength="16" v-model="formRegist.password">
<i slot="prepend" class="el-icon-lock"/>
</el-input>
</el-form-item>
<el-form-item prop="againpassword">
<el-input type="password" placeholder="再次输入密码" :maxlength="16" v-model="formRegist.againpassword">
<i slot="prepend" class="el-icon-lock"/>
</el-input>
</el-form-item>
<el-form-item prop="code">
<el-row :span="24">
<el-col :span="12">
<el-input v-model="formRegist.code" auto-complete="off" placeholder="请输入验证码" size=""></el-input>
</el-col>
<el-col :span="12">
<div class="login-code" @click="refreshCode">
<s-identify :identifyCode="identifyCode"></s-identify>
</div>
</el-col>
</el-row>
</el-form-item>
<el-form-item>
<div class="login-btn">
<el-button type="primary" @click="gologin()" style="margin-left: auto;width: 35%">返回登录</el-button>
<el-button type="primary" @click="submitForm()" style="margin-left: 27%;width: 35%" >确定</el-button>
</div>
</el-form-item>
</el-form>
</div>
</template>
<script>
import SIdentify from "../../components/SIdentify";
import axios from "axios";
export default {
name: "register",
components: {SIdentify},
data() {
return {
formRegist: {
account: "",
username: "",
password: "",
againpassword: "",
code: ""
flag: '',
pass: '1',
identifyCodes: '1234567890abcdefjhijklinopqrsduvwxyz',
identifyCode: '',
rules: {
username:
{required: true, message: "请输入用户名", trigger: "blur"}
account:
{required: true, message: "请输入账号", trigger: "blur"}
password: [{required: true, message: "请输入密码", trigger: "blur"}],
againpassword: [{required: true, message: "请再次输入密码", trigger: "blur"}],
code: [{required: true, message: "请输入验证码", trigger: "blur"}]
mounted() {
this.identifyCode = ''
this.makeCode(this.identifyCodes, 4)
methods: {
refreshCode() {
this.identifyCode = ''
this.makeCode(this.identifyCodes, 4)
makeCode(o, l) {
for (let i = 0; i < l; i++) {
this.identifyCode += this.identifyCodes[this.randomNum(0, this.identifyCodes.length)]
randomNum(min, max) {
return Math.floor(Math.random() * (max - min) + min)
gologin() {
this.$router.push("/login")
RightDataInput(){
if(this.formRegist.account.length<4){
this.pass='0'
alert("账号长度不得小于4")
if(this.formRegist.password.length<8){
this.pass='0'
alert("账号长度不得小于8")
if(this.formRegist.account===null || this.formRegist.password===null){
this.pass='0'
this.$message.error('请填写账号或密码')
if(this.formRegist.password.toLowerCase() !== this.formRegist.againpassword.toLowerCase()){
this.pass='0'
this.$message.error('密码与上次输入不匹配')
alert('密码与上次输入不匹配')
Register(){
this.axios({
url: "/boot/register",
method: 'post',
headers: { "type": "hello" },
data: {
account: this.formRegist.account,
username: this.formRegist.username,
password: this.formRegist.password.toLowerCase(),
}).then(res =>{
this.flag = res.data.flag;
if(this.flag =='1'){
alert("注册成功")
this.$router.push("/login")
else {
alert("注册失败!")
submitForm() {
this.RightDataInput()
if (this.formRegist.code.toLowerCase() !== this.identifyCode.toLowerCase()) {
this.$message.error('请填写正确验证码')
this.refreshCode()
else {
if(this.pass=='1'){
console.log("账号提交注册")
this.Register()
</script>
<style scoped>
.login-bok{
width: 30%;
margin: 150px auto;
border: 1px solid #DCDFE6;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 30px #DCDFE6;
</style>
我这里连函数都没有封装,是很能够看懂的哈。
这个就是我们的重点了。
首先这里调用了三个接口。
这三个接口一目了然是吧。
不过在开始之前,我们先简单来说说,这玩意的调用流程,这个非常重要。
大概就是这样的,因为我们是有token的,所以我们只需要拿到token就可以去确定用户身份,然后设置session。
token 是为了做七天保持登录,如果用session,浏览器关了就没了,存储在localstage得加个密,所以直接使用token。
websocket
这几个点就不用多说了。
主要是,第一用户来了消息要显示出来,就是那个角标要出来,消息提示要出来。
这个是这样做的。
有个集合,
消息过来了,就
读完了,我就把这编号删掉,这样就搞定了。
那么这个就是websocket
loadmessage
然后是信息加载。这个没啥好说的。
访问之后,把那个数据渲染一下。
这个消息发送也简单,主要是修改数据就可以。
ok,这个比较主要的点说完了,我们来看看这个完整代码。
<template>
<div style="width: 70%;height: 500px;margin: 0 auto">
<el-container>
<el-header style="color: white">
<p>欢迎来到随聊</p>
<el-button type="success" round style="position: fixed;left: 70%;top: 12%" @click="loginout">退出登录</el-button>
</el-header>
<div style="height: 40px;width: 100%;background-color: #1794c9">
<p style="margin-right: 85%;color: white">在线聊友</p>
</div>
<el-container>
<el-aside width="200px">
<div style="width: 180px;height: 500px;margin: 20px auto">
<el-row>
<el-card style="height: 70px" shadow="hover" v-for="(user,index) in Users" :key="index">
<el-button v-if="selectId==user.id" type="primary"
style="width: 100%" @click="select(user.id,user.username)"
<p v-if="user.id!=currentId">
<i>{{user.username}}</i>
<p v-else>ME</p>
</el-button>
<el-button v-else type="primary" plain
style="width: 100%" @click="select(user.id,user.username)"
<p v-if="user.id!=currentId">
<el-badge v-if="badgeMes(user.id)" value="new" class="item">
<i>{{user.username}}
</el-badge>
<el-badge v-else>
<i>{{user.username}}</i>
</el-badge>
<p v-else>ME</p>
</el-button>
</el-card>
</el-row>
</div>
</el-aside>
<el-main>
<el-main class="show" style="width: 90%;margin: 0 auto;height: 350px;
background-color: #f0faff;border: 5px #2981ce;border-radius: 10px;
<div style="width: 90%;height: 80px;margin: 0 auto"
v-for="(Message,index) in MessagesList" :key="index"
<div v-model="currentId" v-if="currentId!==Message.fromID && selectId==Message.fromID">
<div style="display:inline-block;width: 10%;
border: 1px solid #14e0bf;;font-size: 3px;border-radius: 100px"
<p style="text-align: center;">{{Message.fromName}}:</p>
</div>
<div style="display:inline-block;width: 60%;
border-radius: 10px;border: 1px solid #0c93ef;"
<p style="width: 100%;text-align: left">{{Message.message.message}}</p>
</div>
</div>
<div v-model="currentId" v-if="currentId===Message.fromID && selectId==Message.message.toID"
<div style="display:inline-block;width: 60%;
border-radius: 10px;border: 1px solid #0c93ef;"
<p style="width: 100%;text-align: right">{{Message.message.message}}</p>
</div>
<div style="display:inline-block;width: 10%;
border: 1px solid #14e0bf;;font-size: 3px;border-radius: 100px"
<p style="text-align: center;">:{{currentName}}</p>
</div>
</div>
</div>
</div>
</el-main>
<div style="width: 90%;margin: 0 auto;
background-color: white;border-radius: 5px;
<el-input v-model="sendMsg" type="textarea" :rows="3"
placeholder="说点什么吧~"
></el-input>
<br><br>
<el-button style="width: 15%;margin-left: 85%" @click="submit" type="primary">
</el-button>
</div>
</el-main>
</el-container>
</el-container>
</div>
<router-view/>
</div>
</template>
<script>
export default {
name: "main",
data() {
return {
read: new Set(),
lastselectId: -2,
selectId: -1,
currentId: null,
currentName: null,
sendMsg: null,
sending: null,
MessagesList: [
Users:[
{"id":1,"username":"小米"},
{"id":2,"username":"小明"},
{"id":3,"username":"小铭"},
{"id":4,"username":"小敏"},
beforeRouteEnter: (to, from, next) => {
console.log("准备进入主页");
let islogin = localStorage.getExpire("token")
if(!islogin){
next({path:'/login'});
next();
methods:{
freshMyMessage(sendMsg){
let MyMessage={
"isSystem": false,
"fromID": this.currentId,
"fromName": this.currentName,
"message":sendMsg
this.MessagesList.push(MyMessage)
badgeMes(ID){
return this.read.has(Number(ID));
loadMessage(selectID,userName){
this.axios({
url: "/boot/loadmessage",
method: 'post',
data: {
currentId: this.currentId,
currentName: this.currentName,
selectID: selectID,
userName: userName
}).then(res =>{
let data = res.data
this.MessagesList = data
select(selectID,userName){
this.selectId = selectID
this.read.delete(Number(selectID))
if(this.selectId==this.currentId) {
this.MessagesList=[]
if(this.lastselectId!=this.selectId && this.selectId!=this.currentId){
this.loadMessage(selectID,userName)
this.lastselectId = this.selectId
}else {
if(this.lastselectId==this.selectId){
alert("正在和当前用户聊天")
submit(){
if(this.selectId==-1){
alert("请选择聊天对象")
return;
if(this.sendMsg===null || this.sendMsg.length<1){
alert("请输入您的消息")
return
}if(this.currentId==this.selectId){
alert("不能和自己聊天哟~")
return;
this.sendMessage()
this.sendMsg=null
console.log("已发送信息")
loginout(){
localStorage.removeItem("token");
this.$router.push('/login')
getUserInfo(){
this.axios({
url: "/boot/main",
method: 'get',
headers: { "token": localStorage.getExpire("token") },
}).then(res =>{
let data = res.data
if(data.success == '0'){
alert("数据获取异常,请重新登录!")
this.loginout()
return;
if(data.success=='2'){
alert("登录过期请重新登录")
this.loginout()
this.currentId = data.currentId
this.currentName = data.currentName
sendMessage() {
let toName = null
for(var i=0;i<this.Users.length;i++){
if(this.Users[i].id==this.selectId){
toName = this.Users[i].username
break
this.sending={
"toID":this.selectId,
"toName":toName,
"message":this.sendMsg
this.freshMyMessage(this.sending)
this.socket.send(JSON.stringify(this.sending));
init() {
if (typeof WebSocket === "undefined") {
alert("您的浏览器不支持socket");
} else {
this.socket = new WebSocket("ws://localhost:8000/chat");
this.socket.onopen = this.open;
this.socket.onerror = this.error;
this.socket.onmessage = this.getMessage;
this.socket.onclose = this.close;
open() {
console.log("socket连接成功");
error(err) {
console.log("连接错误" + err);
getMessage(msg) {
let dataJson = JSON.parse(msg.data)
if(dataJson.system){
this.Users = dataJson.message
}else {
console.log(dataJson)
this.MessagesList.push(dataJson)
this.read.add(dataJson.fromID)
close(event) {
this.socket.close()
console.log("断开链接成功");
mounted() {
this.getUserInfo()
this.init()
</script>
<style scoped>
.el-header, .el-footer {
background-color: #1794c9;
color: #333;
text-align: center;
line-height: 60px;
.show:hover{
box-shadow: 0px 15px 30px rgb(30, 136, 225);
margin-top: 20px;
</style>
那么现在我们把目光搞到后端部分。
我们先来看到环境部分,这个部分呢,用到真实的依赖其实不多。然后这一次是用mybatis来做数据存储的。
然后使用hashmap来存储在线用户的标识,这样的作用和redis的作用其实一样的,都是直接存在内存里面的,所以说速度是可以的,而且没有连接的延时,不过坏处是,redis可以部署到专门的服务器里面,这个不行,不过理论上,我们可以考虑搞一个服务器专门hashmap存储内容,然后发送连接拿到数据也可以,但是这样的话不如直接使用redis不过,这个也不失为一种想法,因为用这玩意我可以自定义复杂对象。
好了,废话不多说了,我们来看看这个依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
<relativePath/>
</parent>
<groupId>com.huterox</groupId>
<artifactId>second</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>second</name>
<description>second</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
自己看着复制哈(其实这个是我第二次Java作业,所以不方便直接给项目文件)
完整的项目结构长这个样子
server :
port : 8000
spring:
devtools:
restart:
enabled: true
datasource:
druid:
username: Huterox
password: 865989840
url: jdbc:mysql://localhost:3306/second?useSSL=false&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
aop-patterns: com.atguigu.admin.*
filters: stat,wall,slf4j
stat-view-servlet:
enabled: true
login-username: admin
login-password: admin
resetEnable: false
web-stat-filter:
enabled: true
urlPattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
filter:
stat:
slow-sql-millis: 1000
logSlowSql: true
enabled: true
wall:
enabled: true
config:
drop-table-allow: false
mybatis:
mapperLocations: classpath:mapper/*.xml
config-location: classpath:mybatis-config.xml
type-aliases-package: com.huterox.second.dap.pojo
数据库设计
这个呢,我设计了四个数据库,不过用到的只有三个,因为原来想做的是有在线群聊的玩意,后来发现这个前端不好写,所以砍掉了,不过如果你感兴趣拓展其实也很简单,因为所有用户的消息其实我是已经发到前端了,只是前端怎么渲染的问题,怎么设计的问题,然后后端继续加几个接口。这个很简单的,没啥说的。
这个表的关联应该很好看懂吧,引入了几个外键,不过我是没有在数据库里面直接使用外键的,都是在代码层面去做的。
然后是我们对应的pojo类
<?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.huterox.second.dao.mapper.FriendMapper">
<select id="getAllFriends" resultType="com.huterox.second.dao.pojo.Friend">
select * from friend
</select>
</mapper>
<?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.huterox.second.dao.mapper.MessageMapper">
<insert id="addMessage">
INSERT INTO message (message,talkid) VALUES (#{message},#{talkid})
</insert>
<select id="getAllMessages" resultType="com.huterox.second.dao.pojo.Message">
select * from message
</select>
<select id="getMessagesByTalkID" resultType="com.huterox.second.dao.pojo.Message">
select * from message where talkid=#{talkid}
</select>
</mapper>
<?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.huterox.second.dao.mapper.TalkMapper">
<insert id="addTalk">
INSERT INTO talk (mytalk,shetalk) VALUES (#{mytalk},#{shetalk})
</insert>
<select id="getAllTalks" resultType="com.huterox.second.dao.pojo.Talk">
select * from talk
</select>
<select id="findTalk" resultType="com.huterox.second.dao.pojo.Talk">
select * from talk where mytalk=#{mytalk} and shetalk=#{shetalk}
</select>
</mapper>
<?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.huterox.second.dao.mapper.UserMapper">
<insert id="AddUser">
INSERT INTO user (account, username, password) VALUES (#{account},#{username},#{password})
</insert>
<select id="getAllUsers" resultType="com.huterox.second.dao.pojo.User">
select * from user
</select>
<select id="selectUserByAccount" resultType="com.huterox.second.dao.pojo.User">
select * from user where account=#{account}
</select>
<select id="selectUserByAccountAndPassword" resultType="com.huterox.second.dao.pojo.User">
select * from user where account=#{account} and password=#{password}
</select>
<select id="selectUserById" resultType="com.huterox.second.dao.pojo.User">
select * from user where id=#{id}
</select>
</mapper>
到这里dao其实就差不多了
Dao相关的服务
服务就三个
package com.huterox.second.server;
import com.huterox.second.dao.mapper.MessageMapper;
import com.huterox.second.dao.pojo.Message;
import org.apache.ibatis.annotations.Param;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class MessageService {
@Autowired
MessageMapper messageMapper;
public Long SaveMessage(String message,Long talkid){
return messageMapper.addMessage(message, talkid);
public List<Message> getMessagesByTalkID(Long talkid){
return messageMapper.getMessagesByTalkID(talkid);
package com.huterox.second.server;
import com.huterox.second.dao.mapper.TalkMapper;
import com.huterox.second.dao.pojo.Talk;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class TalkService {
@Autowired
TalkMapper talkMapper;
public Long getTalkId(Long mytalk,Long shetalk){
Talk talk = talkMapper.findTalk(mytalk, shetalk);
if(talk!=null){
return talk.getTid();
}else {
talkMapper.addTalk(mytalk,shetalk);
talk = talkMapper.findTalk(mytalk, shetalk);
return talk.getTid();
package com.huterox.second.server;
import com.huterox.second.dao.mapper.UserMapper;
import com.huterox.second.dao.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
@Autowired
UserMapper userMapper;
public User selectUserById(Integer id){return userMapper.selectUserById(id);}
public User selectUserByAccount(String account){
return userMapper.selectUserByAccount(account);
public User selectUserByAccountAndPassword(String account,String password){
return userMapper.selectUserByAccountAndPassword(account,password);
@Transactional(rollbackFor = Exception.class)
public int addUser(String account,String username,String password) throws Exception {
int flag = 1;
try {
userMapper.AddUser(account, username, password);
}catch (Exception e){
flag = -1;
throw new Exception("用户添加异常");
return flag;
登录注册实现
在此之前,我们还需要明确一下这个后端会返回给前端的数据。
这个很重要
package com.huterox.second.controller;
import com.huterox.second.dao.message.LoginMessage;
import com.huterox.second.dao.pojo.User;
import com.huterox.second.server.UserService;
import com.huterox.second.utils.TokenProccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpSession;
import java.util.Map;
@Controller
@ResponseBody
public class Login {
@Autowired
UserService userService;
@Autowired
LoginMessage loginMessage;
@Autowired
TokenProccessor tokenProccessor;
@PostMapping(value = "/login" )
public LoginMessage login(@RequestBody Map<String,Object> accountMap, HttpSession session){
String account;
String username;
String password;
User user = null;
try {
account = (String) accountMap.get("account");
password = (String) accountMap.get("password");
user = userService.selectUserByAccountAndPassword(account,password);
}catch (Exception e){
e.printStackTrace();
loginMessage.setSuccess(-1);
if(user!= null){
String token = tokenProccessor.createToken(String.valueOf(user.getId()));
loginMessage.setSuccess(1);
loginMessage.setToken(token);
}else {
loginMessage.setSuccess(-1);
return loginMessage;
package com.huterox.second.controller;
import com.huterox.second.dao.message.RegisterMessage;
import com.huterox.second.server.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Map;
@Controller
@ResponseBody
public class Register {
@Autowired
RegisterMessage registerMessage;
@Autowired
UserService userService;
@PostMapping("/register")
public RegisterMessage Register(@RequestBody Map<String, Object> userMap) throws Exception {
String account = (String) userMap.get("account");
String username = (String)userMap.get("username");
String password = (String) userMap.get("password");
if(account!=null && password!=null){
userService.addUser(account,username,password);
registerMessage.setFlag(1);
}else {
registerMessage.setFlag(-1);
return registerMessage;
这样要说的是这个拦截器,和token加密
token生产解析
这个是使用jwt来做的。首先这里封装了一个工具类。
package com.huterox.second.utils;
import com.huterox.second.dao.pojo.User;
import com.huterox.second.server.UserService;
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@Component
public class TokenProccessor {
@Autowired
UserService userService;
private static final long EXPIRE_TIME=60*60*1000*24*7;
private static final String KEY = "huterox";
* 生成token
* 由于只有当账号密码正确之后才会生成token所以这边只需要用户名进行识别
* @param account 用户账号
* @return
public String createToken(String account){
Map<String,Object> header = new HashMap<String,Object>();
header.put("typ","JWT");
header.put("alg","HS256");
JwtBuilder builder = Jwts.builder().setHeader(header)
.setExpiration(new Date(System.currentTimeMillis()+EXPIRE_TIME))
.setSubject(account)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,KEY);
return builder.compact();
* 验证token是否有效
* @param token 请求头中携带的token
* @return token验证结果 2-token过期;1-token认证通过;0-token认证失败
public int verify(String token){
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(KEY).parseClaimsJws(token).getBody();
}catch (ExpiredJwtException e){
return 2;
String id = claims.getSubject();
if(id != null){
return 1;
}else{
return 0;
public String GetIdByToken(String token){
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(KEY).parseClaimsJws(token).getBody();
}catch (ExpiredJwtException e){
return null;
String id = claims.getSubject();
return id;
之后是拦截器
package com.huterox.second.config;
import com.huterox.second.server.UserService;
import com.huterox.second.utils.TokenProccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class TokenConfig implements WebMvcConfigurer {
@Autowired
UserService userService;
@Autowired
TokenProccessor tokenProccessor;
@Override
public void addInterceptors(InterceptorRegistry registry){
List<String> excludePath = new ArrayList<>();
excludePath.add("/register");
excludePath.add("/login");
excludePath.add("/chat");
excludePath.add("/loadmessage");
excludePath.add("/static/**");
excludePath.add("/assets/**");
registry.addInterceptor(new TokenInterceptor(tokenProccessor))
.addPathPatterns("/**")
.excludePathPatterns(excludePath);
WebMvcConfigurer.super.addInterceptors(registry);
package com.huterox.second.config;
import com.alibaba.fastjson.JSON;
import com.huterox.second.dao.message.TokenInMessage;
import com.huterox.second.server.UserService;
import com.huterox.second.utils.TokenProccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class TokenInterceptor implements HandlerInterceptor {
TokenProccessor tokenProccessor;
public TokenInterceptor(TokenProccessor tokenProccessor) {
this.tokenProccessor = tokenProccessor;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler)throws Exception{
if(request.getMethod().equals("OPTIONS")){
response.setStatus(HttpServletResponse.SC_OK);
return true;
response.setCharacterEncoding("utf-8");
String token = request.getHeader("token");
int result = 0;
if(token != null){
result = tokenProccessor.verify(token);
if(result == 1){
return true;
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
try{
TokenInMessage tokenInMessage = new TokenInMessage();
tokenInMessage.setSuccess(result);
response.getWriter().append(JSON.toJSONString(tokenInMessage));
}catch (Exception e){
e.printStackTrace();
response.sendError(500);
return false;
return false;
到这里的话一个完整的用户登录流程是开发好了。
那么接下来就是基于这个破玩意来干活了
现在进入我们的另一个大头。
首先是配置。
package com.huterox.second.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
进入聊天室
进入这个聊天室的话,需要给上session,这样才能辨别用户。
package com.huterox.second.controller;
import com.huterox.second.dao.message.MainMessage;
import com.huterox.second.dao.pojo.User;
import com.huterox.second.server.UserService;
import com.huterox.second.utils.TokenProccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Objects;
@Controller
@ResponseBody
public class Main {
@Autowired
MainMessage mainMessage;
@Autowired
UserService userService;
@Autowired
TokenProccessor tokenProccessor;
@RequestMapping("/main")
public MainMessage main(HttpServletRequest request, HttpSession session){
String token = request.getHeader("token");
Long id = (Long) session.getAttribute("id");
String name = (String) session.getAttribute("name");
System.out.println("id:"+id+"-name:"+name+"进入Mian");
if(id!=null && name!=null){
mainMessage.setSuccess(1);
mainMessage.setCurrentId(id);
mainMessage.setCurrentName(name);
}else {
User user = userService.selectUserById(
Integer.parseInt(Objects.requireNonNull(tokenProccessor.GetIdByToken(token)))
if (user!=null){
session.setAttribute("id",user.getId());
session.setAttribute("name",user.getUsername());
mainMessage.setSuccess(1);
mainMessage.setCurrentId(user.getId());
mainMessage.setCurrentName(user.getUsername());
}else {
mainMessage.setSuccess(0);
return mainMessage;
首先聊天有两个部分,一个是广播所有用户,一个是转发,因为有用户登录的时候需要告诉别的用户。
这个部分,有代码注释,我在这里就不说了。
这里主要使用到两个东西。
一个是让websocket获取session的玩意
package com.huterox.second.websocket;
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession = (HttpSession) request.getHttpSession();
sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
还有一个是转化信息的工具类
package com.huterox.second.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.huterox.second.dao.message.ResultMessage;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
@Component
public class MessageUtils {
public static String getMessage(boolean isSystemMessage,Long fromID,String fromName,Object message){
try {
ResultMessage result = new ResultMessage();
result.setSystem(isSystemMessage);
result.setFromName(fromName);
result.setMessage(message);
if (fromID!=null){
result.setFromID(fromID);
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(result);
}catch (JsonProcessingException e){
e.printStackTrace();
return null;
当然还有存储的服务,不过这个在service里面做了。
ok,现在看看完整代码
package com.huterox.second.websocket;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.huterox.second.dao.message.MessageSend;
import com.huterox.second.server.MessageService;
import com.huterox.second.server.TalkService;
import com.huterox.second.utils.MessageUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@RestController
@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfigurator.class)
public class ChatEndpoint {
private static TalkService talkService;
private static MessageService messageService;
@Autowired
public void setTalkService(TalkService talkService){
ChatEndpoint.talkService = talkService;
@Autowired
public void setMessageService(MessageService messageService){
ChatEndpoint.messageService = messageService;
private static Map<Long,ChatEndpoint> onlineUsers = new ConcurrentHashMap<>();
private static Map<Long,String> onlineUserNames = new ConcurrentHashMap<>();
private static Map<String,Long> talkID = new ConcurrentHashMap<>();
private Session session;
private HttpSession httpSession;
@OnOpen
public void onOpen(Session session, EndpointConfig config){
this.session = session;
HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
this.httpSession = httpSession;
Long userID = (Long) httpSession.getAttribute("id");
String name = (String) httpSession.getAttribute("name");
System.out.println("id:"+userID+"-name:"+name+"进入聊天室");
onlineUsers.put(userID,this);
onlineUserNames.put(userID,name);
String message = MessageUtils.getMessage(true, null, null,getUsers());
broadcastAllUsers(message);
private void broadcastAllUsers(String message){
try {
Set<Long> IDS = onlineUsers.keySet();
for (Long ID : IDS) {
ChatEndpoint chatEndpoint = onlineUsers.get(ID);
chatEndpoint.session.getBasicRemote().sendText(message);
}catch (Exception e){
e.printStackTrace();
private Set<Long> getId(){
return onlineUsers.keySet();
private Set<Map<String,String>> getUsers(){
Set<Map<String,String>> set = new HashSet<>();
for (Map.Entry<Long, String> entry : onlineUserNames.entrySet()) {
Map<String,String> temp = new HashMap<>();
temp.put("id",String.valueOf(entry.getKey()));
temp.put("username",entry.getValue());
set.add(temp);
return set;
private Long getTalkID(Long mytalk,Long shetalk){
String Key = mytalk+"to"+shetalk;
if (talkID.get(Key)!=null){
return talkID.get(Key);
}else {
Long talkId = talkService.getTalkId(mytalk, shetalk);
talkID.put(Key,talkId);
return talkId;
private Long SaveMessage(Long mytalk,Long shetalk,String message){
Long talkID = this.getTalkID(mytalk, shetalk);
return messageService.SaveMessage(message, talkID);
@OnMessage
public void onMessage(String message, Session session){
try {
ObjectMapper mapper =new ObjectMapper();
System.out.println(message);
MessageSend mess = mapper.readValue(message, MessageSend.class);
Long toID = mess.getToID();
String toName = mess.getToName();
String data = mess.getMessage();
Long userID = (Long) httpSession.getAttribute("id");
String userName = (String) httpSession.getAttribute("name");
this.SaveMessage(userID,toID,data);
System.out.println(mess);
String resultMessage = MessageUtils.getMessage(false, userID,userName,mess);
if(toID!=null) {
onlineUsers.get(toID).session.getBasicRemote().sendText(resultMessage);
} catch (Exception e) {
e.printStackTrace();
@OnClose
public void onClose(Session session) {
Long userID = (Long) httpSession.getAttribute("id");
onlineUsers.remove(userID);
onlineUserNames.remove(userID);
String message = MessageUtils.getMessage(true,null,null,getUsers());
broadcastAllUsers(message);
聊天信息加载
终于到了最后一步了,聊天信息的加载。
这一步主要是靠这玩意
代码如下:
package com.huterox.second.utils;
import com.huterox.second.dao.message.MessageSend;
import com.huterox.second.dao.message.ResultMessage;
import com.huterox.second.dao.pojo.Message;
import com.huterox.second.server.MessageService;
import com.huterox.second.server.TalkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Component
public class LoadMessageUtils {
private static TalkService talkService;
private static MessageService messageService;
@Autowired
public void setTalkService(TalkService talkService){
LoadMessageUtils.talkService = talkService;
@Autowired
public void setMessageService(MessageService messageService){
LoadMessageUtils.messageService = messageService;
public static List<ResultMessage> LoadMessages(Long mytalk,Long shetalk,
String myName,String sheName){
List<ResultMessage> resultMessages= Collections.synchronizedList(new ArrayList<ResultMessage>());
Long mytalkID = talkService.getTalkId(mytalk,shetalk);
Long shetalkID = talkService.getTalkId(shetalk,mytalk);
if(mytalkID!=null){
addMessages(mytalkID,resultMessages,myName,sheName,mytalk,shetalk);
if (shetalkID!=null){
addMessages(shetalkID,resultMessages,sheName,myName,shetalk,mytalk);
return resultMessages;
private static void addMessages(Long talkID,List<ResultMessage> LoadMessages,
String fromName,String toName,Long mytalk,Long shetalk){
List<Message> messagesByTalkID = messageService.getMessagesByTalkID(talkID);
for (Message message : messagesByTalkID) {
ResultMessage resultMessage_ = new ResultMessage();
MessageSend messageSend_ = new MessageSend();
resultMessage_.setFromID(mytalk);
resultMessage_.setFromName(fromName);
messageSend_.setMessage(message.getMessage());
messageSend_.setToID(shetalk);
messageSend_.setToName(toName);
resultMessage_.setMessage(messageSend_);
LoadMessages.add(resultMessage_);
到这里的话,认认真真看这篇博文的话,应该是可以直接复刻这个项目的,毕竟这个底裤都拿出来了,这dome。
然后优化的是有很多优化的。对了这里就不得不说这个一个小bug了,这个是前端的问题,就是这个页面在chrome浏览器是没有啥问题的,但是在Firefox,这个用户名渲染会出现问题。其他的还好说。