springboot 使用 webSocket 实现多人聊天 + 单人聊天

# 后端

WebSocket服务端注解事件类型事件描述
@OnOpenonOpen当打开连接后触发
@OnMessageonMessage当接收客户端信息时触发
@OnCloseonClose当连接关闭时触发
@OnErroronError当通信异常时触发

# pom.xml
  <!-- spring Websocket -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
  </dependency>
  <!-- fastjson -->
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.56</version>
  </dependency>


# 开启WebSocket服务端的自动注册

WebSocketConfig.class

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();
    }
}


# 创建消息类

Message.class

import com.alibaba.fastjson.JSON;

import java.util.List;

/**
 * WebSocket 聊天消息类
 */
public class Message {

    public static final String ENTER = "ENTER";
    public static final String SPEAK = "SPEAK";
    public static final String QUIT = "QUIT";

    private String type;  // 消息类型

    private String fromUser; // 发送人

    private String toUser;  // 接收人

    private String msg;  // 发送消息

    private int onlineCount;  // 在线用户数

    private List<String> list;

    /*
     * 聊天消息
     * 没有设置接收人 toUser ,视为群聊
     * 设置了接收人 toUser,视为私聊
     * */
    public static String jsonStr(String type, String fromUser, String toUser, String msg, int onlineCount) {
        return JSON.toJSONString(new Message(type, fromUser, toUser, msg, onlineCount));
    }

    public Message(String type, String fromUser, String toUser, String msg, int onlineCount) {
        this.type = type;
        this.fromUser = fromUser;
        this.toUser = toUser;
        this.msg = msg;
        this.onlineCount = onlineCount;
    }

    public static String jsonStr(String type, String fromUser, String toUser, String msg, int onlineCount, List<String> list) {
        return JSON.toJSONString(new Message(type, fromUser, toUser, msg, onlineCount, list));
    }

    public Message(String type, String fromUser, String toUser, String msg, int onlineCount, List<String> list) {
        this.type = type;
        this.fromUser = fromUser;
        this.toUser = toUser;
        this.msg = msg;
        this.onlineCount = onlineCount;
        this.list = list;
    }

    public static String getENTER() {
        return ENTER;
    }

    public static String getSPEAK() {
        return SPEAK;
    }

    public static String getQUIT() {
        return QUIT;
    }

    public String getType() {
        return type;
    }

    public Message setType(String type) {
        this.type = type;
        return this;
    }

    public String getFromUser() {
        return fromUser;
    }

    public Message setFromUser(String fromUser) {
        this.fromUser = fromUser;
        return this;
    }

    public String getToUser() {
        return toUser;
    }

    public Message setToUser(String toUser) {
        this.toUser = toUser;
        return this;
    }

    public String getMsg() {
        return msg;
    }

    public Message setMsg(String msg) {
        this.msg = msg;
        return this;
    }

    public int getOnlineCount() {
        return onlineCount;
    }

    public Message setOnlineCount(int onlineCount) {
        this.onlineCount = onlineCount;
        return this;
    }

    public List<String> getList() {
        return list;
    }

    public Message setList(List<String> list) {
        this.list = list;
        return this;
    }
}



# 创建WebSocket服务端

WebSocketChatServer.class

import com.alibaba.fastjson.JSON;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
@ServerEndpoint("/chat/{name}")
public class WebSocketChatServer {
    /**
     * 全部在线会话  PS: 基于场景考虑 这里使用线程安全的Map存储会话对象。
     * 以用户姓名为key
     */
    private static Map<String, Session> onlineSessions = new ConcurrentHashMap<>();

    /**
     * 当通信发生异常:打印错误日志
     */
    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
    }

    /**
     * 当客户端打开连接:1.添加会话对象 2.更新在线人数
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("name") String name) {
        onlineSessions.put(name, session);
        sendMessageToAll(Message.jsonStr(Message.ENTER, "系统通知", "", "欢迎“" + name + "”加入群聊", onlineSessions.size(), listUser()));
    }

    /**
     * 当客户端发送消息:1.获取它的用户名和消息 2.发送消息给所有人
     * <p>
     * PS: 这里约定传递的消息为JSON字符串 方便传递更多参数!
     */
    @OnMessage
    public void onMessage(Session session, String jsonStr) {
        Message message = JSON.parseObject(jsonStr, Message.class);

        if (message.getToUser() != null) {
            sendMessageToUser(Message.jsonStr(Message.SPEAK, message.getFromUser(), message.getToUser(), message.getMsg(), onlineSessions.size()));
        } else {
            sendMessageToAll(Message.jsonStr(Message.SPEAK, message.getFromUser(), message.getToUser(), message.getMsg(), onlineSessions.size()));
        }
    }

    /**
     * 当关闭连接:1.移除会话对象 2.更新在线人数
     */
    @OnClose
    public void onClose(Session session, @PathParam("name") String name) {
        onlineSessions.remove(name);
        sendMessageToAll(Message.jsonStr(Message.QUIT, "系统通知", "", "“" + name + "”退出群聊", onlineSessions.size(), listUser()));
    }

    /**
     * 公共方法:发送信息给所有人
     */
    private static void sendMessageToAll(String msg) {
        onlineSessions.forEach((id, session) -> {
            try {
                session.getBasicRemote().sendText(msg);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }

    /**
     * 单独聊天方法:发送信息给指定的人
     */
    private static void sendMessageToUser(String msg) {
        Message message = JSON.parseObject(msg, Message.class);
        if (onlineSessions.get(message.getToUser()) != null) {
            try {
                onlineSessions.get(message.getToUser()).getBasicRemote().sendText(msg);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            try {
                onlineSessions.get(message.getFromUser()).getBasicRemote().sendText(Message.jsonStr(Message.QUIT, "系统通知", message.getFromUser(), "用户不在线", onlineSessions.size()));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private List<String> listUser() {
        List<String> list = new ArrayList<>();

        for (Map.Entry<String, Session> s: onlineSessions.entrySet()) {
            list.add(s.getKey());
        }

        return list;
    }
}



# 前端

WebSocket回调函数事件类型事件描述
webSocket.onopenonOpen当打开连接后触发
webSocket.onmessageonMessage当接收客户端信息时触发
webSocket.oncloseonClose当连接关闭时触发
webSocket.onerroronError当通信异常时触发

# login.vue
<template>
  <div>
    <el-row type="flex" class="row-bg" justify="center">
      <el-col :md="8">
        <el-card shadow="always" style="margin-top: 150px;">
          <h3 class="text-center mb-5">webSocket 聊天室</h3>
          <el-form ref="form" :model="form" :inline="true">
            <el-form-item label="用户名">
              <el-input v-model="form.input" @keyup.enter.native="login()"></el-input>
            </el-form-item>
            <el-form-item>
              <el-button type="primary" style="width: 100%" @click="login()">马上进入</el-button>
            </el-form-item>
          </el-form>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<script>
    export default {
      name: "login",
      data() {
        return {
          form: {
            input: '',
          },
        }
      },
      methods: {
        login() {
          if (this.form.input === '') {
            this.$message.warning('请填写用户名');
          } else {
            let admin = {
              username : this.form.input,
            };
            this.$cookie.setCookie('user', this.form.input);
            this.$router.push({path:'/index'});
          }
        }
      }
    }
</script>

<style scoped>
</style>



# index.vue
<template>
  <div>
    <el-row type="flex" justify="center">
      <el-col :md="14">
        <div>
          <p class="mb-1 ml-2">点击退出登录</p>
          <p class="mt-1"><i class="el-icon-bottom" style="margin-left: 12px;"></i></p>
        </div>
        <el-card :body-style="{ padding: '10px' }" style="background-color: #E8E8E8; border: 1px solid #DDDDDD; border-bottom: 0;">
          <div style="height: 20px;">
            <div style="width: 20px; height: 20px; background-color: #DF7065; border-radius: 50%; float: left;" @click="webSokcetClose()"></div>
            <div style="width: 20px; height: 20px; background-color: #E6BB46; border-radius: 50%; float: left; margin-left: 10px;"></div>
            <div style="width: 20px; height: 20px; background-color: #5BCC8B; border-radius: 50%; float: left; margin-left: 10px;"></div>
          </div>
        </el-card>
        <el-card :body-style="{ padding: '0' }" style="border: 1px solid #DDDDDD; border-top: 0;">
          <el-row>
            <el-col :md="6" style=" height: 500px; border-right: 2px solid #DDDDDD; overflow: auto;">
              <el-card :body-style="{  'padding-top': '11px', 'padding-bottom': '12px', 'padding-left': '10px', 'padding-right': '12px' }" shadow="never">
                <div>
                  <el-input
                    size="mini"
                    placeholder="请输入内容"
                    v-model="input1">
                  </el-input>
                </div>
              </el-card>
              <div v-for="item in listUser" :key="item">
                <a href="javascript:void(0);" @click="toggleChat(item)">
                  <el-card :body-style="{ padding: '5px 10px' }" shadow="never">
                    <el-row>
                      <el-col :span="7">
                        <el-image
                          style="width: 40px; height: 40px; border-radius: 50%;"
                          src="https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg"
                          fit="cover"></el-image>
                      </el-col>
                      <el-col :span="17">
                        <h4 class="m-0" style="color: #666666; font-size: 15px;">{{item}}</h4>
                        <p style="color: #999999; font-size: 12px; margin: 0; margin-top: 3px;" class="limitTitleDirectory">这个家伙很懒,什么也没有留下。</p>
                      </el-col>
                    </el-row>
                  </el-card>
                </a>
              </div>
            </el-col>
            <el-col :md="18">
              <div v-show="toUser !== '游客'">
                <el-card :body-style="{ padding: '15px' }" shadow="never">
                  <div class="text-center">
                    <p class="m-0">{{toUser}}</p>
                  </div>
                </el-card>
                <el-card :body-style="{ padding: '5px' }" shadow="never">
                  <div v-for="item in this.listUser" :key="item" v-show="toUser === item" :id="item" style="height: 327px; overflow: auto;"></div>
                </el-card>
                <el-input type="textarea" :rows="5" v-model="input" @keyup.enter.native="webSokcetSend()"></el-input>
              </div>
              <div v-show="toUser === '游客'">
                <div style="text-align: center; line-height: 500px; background-color: #f3f3f3;">
                  <p style="margin: 0; font-size: 22px; color: #909399;">没有会话消息</p>
                </div>
              </div>
            </el-col>
          </el-row>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<script>
    export default {
      name: "index",=
      data() {
        return {
          input: '',
          input1: '',
          listUser: [],
          webSocket : null,
          fromUser: '游客',
          toUser: '游客',
        }
      },
      mounted() {
        if (document.cookie === '') {
          this.webSokcetClose();
          this.$router.push({path:'/'});
        } else {
          this.fromUser = this.$cookie.getCookie('user');
        }
        this.webSocket = new WebSocket('ws://192.168.1.125:8855/chat/' + this.fromUser);
        this.initWebSocket();
      },
      methods: {
        initWebSocket() {
          this.webSocket.onerror = this.onError;  // 通讯异常
          this.webSocket.onopen = this.onOpen;  // 连接成功
          this.webSocket.onmessage = this.onMessage;  // 收到消息时回调
          this.webSocket.onclose = this.onClose;  // 连接关闭时回调
        },
        onError() {
          /*
          * 通讯异常
          * */
          console.log("通讯异常")
        },
        onOpen() {
          /*
          * 连接成功
          * */
          console.log("通讯开始");
        },
        onMessage(event) {
          /*
          * 收到消息时回调函数
          * */
          let data = JSON.parse(event.data);
          // console.log(data);
          if (data.list !== undefined) {
            let list = data.list;
            for (let i=0; i<list.length; i++) {
              if (list[i] === this.fromUser) {
                list.splice(i, 1);
              }
            }
            this.listUser = list;
          }
          this.messageDiv(event.data);
        },
        onClose() {
          /*
          * 关闭连接时回调函数
          * */
          console.log("通讯关闭");
        },
        webSokcetSend() {
          /*
          * 发送消息
          * */
          let message = JSON.stringify({'fromUser': this.fromUser, 'toUser': this.toUser, 'msg': this.input,});
          this.webSocket.send(message);
          this.input = '';
          this.messageDiv(message)
        },
        webSokcetClose() {
          /*
          * 关闭连接
          * */
          this.webSocket.close();
          this.$cookie.delCookie('user');
          this.$router.push({path:'/'});
        },
        toggleChat(toUser) {
          this.toUser = toUser;
        },
        messageDiv(data1, type) {
          let data = JSON.parse(data1);

          let div = document.createElement('div');
          let p1 = document.createElement('p');
          let p = document.createElement('p');

          p1.innerHTML = data.fromUser;
          p.innerHTML = data.msg;

          if (data.fromUser !== '系统通知') {
            if (data.type !== undefined) {
              /*
              * data.type !== undefined
              * 说明这是接收到消息
              * 把值插到发送者的div
              * */
              let fromUser = document.getElementById(data.fromUser);

              div.style = 'width: 370px; float: left; margin-left: 15px; margin-top: 15px;';
              p1.style = 'font-size: 15px; margin-bottom: 0; margin-top: 0; float: left; font-weight: 500;';
              p.style = 'padding: 10px; background-color: #F1F1F1; margin-top: 25px; margin-bottom: 10px; word-wrap : break-word;';
              div.appendChild(p1);
              div.appendChild(p);

              fromUser.appendChild(div);
              fromUser.scrollTop = fromUser.scrollHeight;
              if (fromUser.style.display === 'none') {
                console.log('有新的消息未查看!');
              }
            } else {
              /*
              * 说明这是发送消息
              * 把值插到接收者的div
              * */
              let toUser = document.getElementById(data.toUser);

              div.style = 'width: 370px; float: right; margin-right: 15px; margin-top: 15px;';
              p1.style = 'font-size: 15px; margin-bottom: 0; margin-top: 0; float: right; font-weight: 500;';
              p.style = 'padding: 10px; background-color: #9FE86C; margin-top: 25px; margin-bottom: 10px; word-wrap : break-word;';
              div.appendChild(p1);
              div.appendChild(p);

              toUser.appendChild(div);
              toUser.scrollTop = toUser.scrollHeight;
            }
          }
        }
      },
      beforeRouteEnter(to, from, next) {
        // 添加背景色
        document.querySelector('body').setAttribute('style', 'background-color: #F9F9F9');
        next()
      },
      beforeRouteLeave(to, from, next) {
        // 去除背景色
        document.querySelector('body').setAttribute('style', '');
        next()
      },
    }
</script>

<style scoped>
  .limitTitleDirectory {
    width: 120px;		/* 限制文本宽度 */
    overflow: hidden;		/* 超出的文本隐藏 */
    text-overflow: ellipsis;	/* 溢出的文本内容用 ... 代替 */
    white-space: nowrap;		/* 溢出不换行*/
  }
  element.style {
    padding-left: 10px;
  }
  .el-menu-item {
    font-size: 14px;
    color: #303133;
    padding: 0 10px;
    cursor: pointer;
    -webkit-transition: border-color .3s,background-color .3s,color .3s;
    transition: border-color .3s,background-color .3s,color .3s;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
  }

  /*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/
  ::-webkit-scrollbar
  {
    width: 5px;  /*滚动条宽度*/
    height: 5px;  /*滚动条高度*/
  }

  /*定义滚动条轨道 内阴影+圆角*/
  ::-webkit-scrollbar-track
  {
    /*-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);*/
    border-radius: 10px;  /*滚动条的背景区域的圆角*/
    /*background-color: red;!*滚动条的背景颜色*!*/
  }

  /*定义滑块 内阴影+圆角*/
  ::-webkit-scrollbar-thumb
  {
    border-radius: 10px;  /*滚动条的圆角*/
    /*-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);*/
    background-color: #b8b8bc;  /*滚动条的背景颜色*/
  }
</style>



# 码云地址

前端源码: https://gitee.com/chenbz2/websocketvue
后端源码: https://gitee.com/chenbz2/websocketspring



参考博客:

  1. SpringBoot 使用WebSocket打造在线聊天室(基于注解)
  2. (十七)Spring Boot 整合 Websocket
  3. WebSocket+SpringBoot聊天室(一)
  4. springboot+websocket构建在线聊天室(群聊+单聊)
  5. springboot+websocket聊天室(多人聊天,单人聊天)
  6. vue2.0使用websocket的简单demo
  • 5
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值