嘿从零开始基于SpringBoot 打造在线聊天室(4.4W字最长博文)

前言


两年前的存货,前几天有同学需要,csdn上设置了付费,这里免费开放


失踪人口回归兄弟们,差不多连续5天没有发布博文了,许久不见。当然也是最近确实不在状态而且比较忙,所以就没有去更新博文,加上最近作业时真的多,各种大作业顶不住。

那么今天也是给大家展示一个小dome,基于SpringBoot + mybatis + websocket 做的在线聊天器。本来是要做在线群聊的,但是这个前端我是真的不想调了,本来是想嫖的页面的,结果怎么说,还是自己做吧,可控。

代码也比较简单,后端搭建写起来其实也就两三个小时,主要是前端,那玩意零零散散花了两天,然后还有这个调试,总体代码从星期一开始写,到星期三写完了,后面没空不想写,于是拖到今天调测完,砍了不少功能。

ok,我们先来看看这个效果图哈。

效果

主页面

消息提示

聊天页面

大概就这样,当然还有登录,注册。

登录注册

大概的话就是这个样子。

OK,现在呢,咱们就进入到这个代码阶段。

前端

首先我们先看到前端。

项目构建

既然是从0开始,那么我们就从o开始说。咱们这个是使用vue2 + elementui 写的,为什么是vue2,简单,我没升级嘛。能跑就行,我也不是专门写前端的。

首先省略 使用 vue-cli 开始项目哈。

依赖

我这里就说我这里使用到的依赖。

elementUI axios websocket

就这几个主要的。

项目结构

这里其实就三个页面。

登录注册

在此之前,我们先来看看这个路由设计哈。

java 复制代码
 routes: [
    {
      path: '/',
      name: 'mian',
      component: main
    },
    {
      path: '/login',
      name: 'login',
      component: login
    },
    {
      path: '/register',
      name: 'register',
      component: register
    }
  ],
  mode: "history"

就非常的简单哈。

验证码部分

在这里我们先来说说,这个验证码部分吧。 这个呢,是由前端生成的。是这个组件在干活

html 复制代码
<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 ,到这里咱们就能够放代码了

html 复制代码
<template>
  <div>
    <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'){
          //设置token七天过期
          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>

注册页面

html 复制代码
<template>
  <div>
    <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,这个比较主要的点说完了,我们来看看这个完整代码。

html 复制代码
<template>
<div>
  <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>
                  <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}}
                        </i>
                      </el-badge>
                      <el-badge v-else>
                        <i>{{user.username}}</i>
                      </el-badge>
                    </p>
                    <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 >
<!--            这部分是咱们的对话内容-->
            <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">
                <br>
                <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>
          <br>
          <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
      })

    },
  //  这个是咱们websocket的方法
    //建立连接,初始化weosocket
    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 {
        // 实例化socket
        this.socket = new WebSocket("ws://localhost:8000/chat");
        // 监听socket连接
        this.socket.onopen = this.open;
        // 监听socket错误信息
        this.socket.onerror = this.error;
        // 监听socket消息
        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) {
      //socket是链接的实例,close就是关闭链接
      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 复制代码
<?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/> <!-- lookup parent from repository -->
    </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作业,所以不方便直接给项目文件)

完整的项目结构长这个样子

配置

yaml 复制代码
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.*  #springbean监控
      filters: stat,wall,slf4j  #所有开启的功能

      stat-view-servlet: #监控页配置
        enabled: true
        login-username: admin
        login-password: admin
        resetEnable: false

      web-stat-filter: #web监控
        enabled: true
        urlPattern: /*
        exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'


      filter:
        stat: #sql监控
          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类

Dao层及其mapper

然后是写各种增删查改,由于是myabtis所以要那啥,写sql,还是mp香呀。然后为了方便管理,所以我的sql语句都是在xml文件里面的。

由于都是对得到的,所以我们这边就直接把xml文件展示出来

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.huterox.second.dao.mapper.FriendMapper">

    <!--sql-->
    <select id="getAllFriends" resultType="com.huterox.second.dao.pojo.Friend">
        select * from friend
    </select>
</mapper>
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.huterox.second.dao.mapper.MessageMapper">
    <insert id="addMessage">
        INSERT INTO message (message,talkid) VALUES (#{message},#{talkid})
    </insert>

    <!--sql-->
    <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 复制代码
<?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>

    <!--sql-->
    <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 复制代码
<?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>

    <!--sql-->
    <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相关的服务

服务就三个

java 复制代码
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);
    };
}
java 复制代码
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();
        }
    }
}
java 复制代码
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;
    }
}

登录注册实现

交互信息

在此之前,我们还需要明确一下这个后端会返回给前端的数据。

这个很重要

登录注册

ok,前面铺垫了那么多终于到了登录注册这种服务层面了。

首先这个业务层面没什么好说的

java 复制代码
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){
//            这里我们把用户的id给前端,后面我们验证这个id就好了
            String token = tokenProccessor.createToken(String.valueOf(user.getId()));

            loginMessage.setSuccess(1);
            loginMessage.setToken(token);
        }else {
            loginMessage.setSuccess(-1);
        }

        return loginMessage;
    }
}
java 复制代码
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来做的。首先这里封装了一个工具类。

java 复制代码
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; //过期时间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 {
            //token过期后,会抛出ExpiredJwtException 异常,通过这个来判定token过期,
            claims = Jwts.parser().setSigningKey(KEY).parseClaimsJws(token).getBody();
        }catch (ExpiredJwtException e){
            return 2;
        }
        //从token中获取用户名,当用户查询通过后即可
        String id = claims.getSubject();
//        User user = userService.selectUserById(Integer.parseInt(id));

        if(id != null){
            return 1;
        }else{
            return 0;
        }
    }
    public String GetIdByToken(String token){
        Claims claims = null;
        try {
            //token过期后,会抛出ExpiredJwtException 异常,通过这个来判定token过期,
            claims = Jwts.parser().setSigningKey(KEY).parseClaimsJws(token).getBody();
        }catch (ExpiredJwtException e){
            return null;
        }
        //从token中获取用户名,当用户查询通过后即可
        String id = claims.getSubject();
        return id;
    }

}

拦截器

之后是拦截器

java 复制代码
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 addCorsMappings(CorsRegistry registry) {
//        registry.addMapping("/**")
//                .allowCredentials(true)
//                .allowedHeaders("*")
//                .allowedMethods("*")
//                .allowedOrigins("*");
//
//    }
    // 跨域,我们前端做了跨域所以的话后端不用,也是为了安全,不能什么阿猫阿狗都过来访问

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        List<String> excludePath = new ArrayList<>();
        //排除拦截,除了注册登录(此时还没token),其他都拦截
        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);
    }


}
java 复制代码
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){
//                System.out.println("通过拦截器");
                return true;
            }

        }
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        try{

            TokenInMessage tokenInMessage = new TokenInMessage();
            tokenInMessage.setSuccess(result);//0表示验证失败,2表示过期
            response.getWriter().append(JSON.toJSONString(tokenInMessage));
//            System.out.println("认证失败,未通过拦截器");
        }catch (Exception e){
            e.printStackTrace();
            response.sendError(500);
            return false;
        }
        return false;
    }
}

到这里的话一个完整的用户登录流程是开发好了。 那么接下来就是基于这个破玩意来干活了

聊天室

现在进入我们的另一个大头。 首先是配置。

java 复制代码
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
    //注入ServerEndpointExporter bean对象,自动注册使用注解@ServerEndpoint的bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

进入聊天室

进入这个聊天室的话,需要给上session,这样才能辨别用户。

java 复制代码
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){
//            如果有人恶意修改session,我也没辙,反正到时候恶意修改后数据可能是拿不到的
            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,这样好进行聊天标记
                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的玩意

java 复制代码
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 = (HttpSession) request.getHttpSession();
        sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
    }
}

还有一个是转化信息的工具类

java 复制代码
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);
            }
            //把字符串转成json格式的字符串
            ObjectMapper mapper = new ObjectMapper();
            return mapper.writeValueAsString(result);
        }catch (JsonProcessingException e){
            e.printStackTrace();
        }
        return null;
    }
}

当然还有存储的服务,不过这个在service里面做了。

完整代码

ok,现在看看完整代码

java 复制代码
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;
    }

    //用来存储每个用户客户端对象的ChatEndpoint对象
    private static Map<Long,ChatEndpoint> onlineUsers = new ConcurrentHashMap<>();

    private static Map<Long,String> onlineUserNames = new ConcurrentHashMap<>();
//    用来存储talkID 的这样只需要查询一次数据库就可以拿到talkID了,加快速度
    private static Map<String,Long> talkID = new ConcurrentHashMap<>();
    //声明session对象,通过对象可以发送消息给指定的用户
    private Session session;

    //声明HttpSession对象,我们之前在HttpSession对象中存储了用户id
    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);

        //将当前在线用户的用户名推送给所有的客户端
        //1 获取消息

        String message = MessageUtils.getMessage(true, null, null,getUsers());
        //2 调用方法进行系统消息的推送
        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();
        }
    }

    //返回在线用户ID
    private Set<Long> getId(){
        return onlineUsers.keySet();
    }

    //返回在线用户名
//    {"id":4,"username":"小敏"},
    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) {
//                发送的数据长这个样子
//                {"isSystem": false,
//                        "fromID": 2,
//                        "message":{"toID":1,"toName":"Futerox","message":"Hello"}
//                }
                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);
    }}

聊天信息加载

终于到了最后一步了,聊天信息的加载。 这一步主要是靠这玩意 代码如下:

java 复制代码
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){

//        我们的写操作要比读操作更多,所以不用CopyOnWriteArrayList<>();
        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,这个用户名渲染会出现问题。其他的还好说。

相关推荐
艾伦~耶格尔2 小时前
Spring Boot 三层架构开发模式入门
java·spring boot·后端·架构·三层架构
man20172 小时前
基于spring boot的篮球论坛系统
java·spring boot·后端
攸攸太上3 小时前
Spring Gateway学习
java·后端·学习·spring·微服务·gateway
罗曼蒂克在消亡4 小时前
graphql--快速了解graphql特点
后端·graphql
潘多编程4 小时前
Spring Boot与GraphQL:现代化API设计
spring boot·后端·graphql
大神薯条老师4 小时前
Python从入门到高手4.3节-掌握跳转控制语句
后端·爬虫·python·深度学习·机器学习·数据分析
2401_857622665 小时前
Spring Boot新闻推荐系统:性能优化策略
java·spring boot·后端
知否技术5 小时前
为什么nodejs成为后端开发者的新宠?
前端·后端·node.js
AskHarries5 小时前
如何优雅的处理NPE问题?
java·spring boot·后端
计算机学姐6 小时前
基于SpringBoot+Vue的高校运动会管理系统
java·vue.js·spring boot·后端·mysql·intellij-idea·mybatis