netty+springboot+vue聊天室(需要了解netty)

news2025/1/19 2:51:58

先看看这个使用websocket实现的聊天室,因为前端是使用websocket,和下面的demo的前端差不多就不解释实现原理,所以建议还是看看(要是会websocket的大佬请忽略)

springboot+websocket+vue聊天室

目录

  • 一、实现内容
  • 二、代码实现
    • 1.后端
    • 2.前端
    • 源码

一、实现内容

  1. http://localhost:8080/netty?uid=1

在这里插入图片描述

  1. http://localhost:8080/netty?uid=2

在这里插入图片描述

  1. http://localhost:8080/netty?uid=3

在这里插入图片描述

二、代码实现

1.后端

在这里插入图片描述

  • netty服务端
@Component("NettyChatServer")
public class NettyChatServer {
    //主线程池:处理连接请求
    private static NioEventLoopGroup boss = new NioEventLoopGroup(2);
    //工作线程池:接收主线程发过来的任务,完成实际的工作
    private static NioEventLoopGroup worker = new NioEventLoopGroup(6);
    //创建一个服务器端的启动对象
    ServerBootstrap serverBootstrap=null;

    @Autowired
    //自定义handler、处理客户端发送过来的消息进行转发等逻辑
    MyTextWebSocketFrameHandler myTextWebSocketFrameHandler = new MyTextWebSocketFrameHandler();

    public void run() {
        serverBootstrap= new ServerBootstrap().group(boss, worker)
                .channel(NioServerSocketChannel.class)
                //连接的最大线程数
                .option(ChannelOption.SO_BACKLOG, 128)
                //长连接,心跳机制
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        //因为基于http协议,使用http的编码和解码器
                        nioSocketChannel.pipeline().addLast(new HttpServerCodec());
                        //是以块方式写,添加ChunkedWriteHandler处理器
                        nioSocketChannel.pipeline().addLast(new ChunkedWriteHandler());
                        /**
                         * 说明
                         *   1. http数据在传输过程中是分段, HttpObjectAggregator ,就是可以将多个段聚合
                         *   2. 这就就是为什么,当浏览器发送大量数据时,就会发出多次http请求
                         */
                        nioSocketChannel.pipeline().addLast(new HttpObjectAggregator(8192));
                        /**
                         * 说明
                         *    1. 对应websocket ,它的数据是以帧(frame,基于TCP)形式传递
                         *    2. 可以看到WebSocketFrame下面有六个子类
                         *    3. 浏览器请求时 ws://localhost:8888/wechat 表示请求的uri
                         *    4. WebSocketServerProtocolHandler 核心功能是将 http协议升级为 ws协议 , 保持长连接
                         *    5. 是通过一个 状态码 101
                         */
                        nioSocketChannel.pipeline().addLast(new WebSocketServerProtocolHandler("/wechat"));
                        //自定义handler、处理客户端发送过来的消息进行转发等逻辑
                        nioSocketChannel.pipeline().addLast(myTextWebSocketFrameHandler);
                    }
                });
        //server监听接口
        try {
            ChannelFuture channelfuture = serverBootstrap.bind(8888).sync();
            // 添加注册监听,监控关心的事件,当异步结束后就会回调监听逻辑
            channelfuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    if (channelFuture.isSuccess()){
                        System.out.println("监听端口8888成功");
                    }else{
                        System.out.println("监听端口8888失败");
                    }
                }
            });
            //关闭通道和关闭连接池(不是真正关闭,只是设置为关闭状态)
            channelfuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //EventLoop停止接收任务、任务结束完毕停掉线程池
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

  • 自定义handler,处理业务逻辑
@Component
@ChannelHandler.Sharable
public class MyTextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    //记录客户端和channel的绑定
    private static Map<Integer, Channel> channelMap=new ConcurrentHashMap<Integer, Channel>();

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
        //将发过来的内容进行解析成 自定义的Message
        Message message = JSON.parseObject(textWebSocketFrame.text(), Message.class);
        //绑定对应用户和channel
        if (!channelMap.containsKey(message.getFromUid())){
            channelMap.put(message.getFromUid(),channelHandlerContext.channel());
        }else{
            channelMap.replace(message.getFromUid(),channelHandlerContext.channel());
        }
        //发送给对应的客户端对应的channel
        if(channelMap.containsKey(message.getToUid())){
            //因为连接成功会发送一次注册消息(注册消息message.getToUid()== message.getFromUid())
            if(message.getToUid()!= message.getFromUid()){
                //不能重用之前的textWebSocketFrame
                channelMap.get(message.getToUid()).writeAndFlush(new TextWebSocketFrame(textWebSocketFrame.text()));
            }
        }else{
            //该用户暂未在线,先将消息存进数据库(这里没实现)
            System.out.println("该用户暂未在线,先将消息存进数据库");
        }
        //计数-1(计数法来控制回收内存)
        channelHandlerContext.fireChannelRead(textWebSocketFrame.retain());
    }
}
  • netty整合到springboot
@SpringBootApplication
public class OnlinechatApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(OnlinechatApplication.class, args);
        NettyChatServer nettyChatServer = (NettyChatServer)context.getBean("NettyChatServer");
        nettyChatServer.run();
    }

}

2.前端

  • 和weocket的demo区别发送的ws协议uri不同,ws://localhost:8888/wechat
  • 还有就是,websocket建立连接之后就先发送一次绑定消息到服务器端(将用户和channel的关系对应起来)
<template>
  <div class="bg">
    <el-container class="wechat">
      <el-aside width="35%" style="border-right: 1px solid #fff">
        <!-- 自己 -->
        <div class="item">
          <el-avatar
            :size="46"
            :src="user.avatarUrl"
            style="float: left; margin-left: 2px"
          ></el-avatar>
          <div class="name">
            {{ user.nickname
            }}<el-tag style="margin-left: 5px" type="success">本人</el-tag>
          </div>
        </div>
        <!-- 在线用户 -->
        <div
          class="item"
          v-for="(item1, index) in userlist"
          :key="item1.uid"
          @click="selectUser(index)"
        >
          <!-- 新数消息 -->
          <el-badge
            :value="new_message_num[index]"
            :max="99"
            :hidden="!new_message_num[index] > 0"
            style="float: left; margin-left: 2px"
          >
            <el-avatar :size="46" :src="item1.avatarUrl"></el-avatar>
          </el-badge>
          <div class="name">{{ item1.nickname }}</div>
        </div>
      </el-aside>
      <el-main>
        <el-container class="wechat_right">
          <!-- 右边顶部 -->
          <el-header class="header">{{
            anotherUser != null && anotherUser.uid > 0
              ? anotherUser.nickname
              : "未选择聊天对象"
          }}</el-header>
          <!-- 聊天内容 -->
          <el-main class="showChat">
            <div v-for="item2 in messageList[index]" :key="item2.msg">
              <!-- 对方发的 -->
              <div class="leftBox" v-if="item2.FromUid == anotherUser.uid">
                <span style="font-size: 4px">{{ item2.time }}</span
                >{{ item2.msg }}
              </div>
              <div class="myBr" v-if="item2.FromUid == anotherUser.uid"></div>
              <!-- 自己发的 -->
              <div class="rightBox" v-if="item2.FromUid == user.uid">
                <span style="font-size: 4px">{{ item2.time }}</span
                >{{ item2.msg }}
              </div>
              <div class="myBr" v-if="item2.FromUid == user.uid"></div>
            </div>
          </el-main>
          <!-- 输入框 -->
          <el-main class="inputValue">
            <textarea v-model="inputValue" id="chat" cols="26" rows="5">
            </textarea>
            <!-- 发送按钮 -->
            <el-button
              v-if="
                anotherUser != null && anotherUser.uid > 0 && inputValue != ''
              "
              type="success"
              size="mini"
              round
              id="send"
              @click="senMessage"
              >发送</el-button
            >
          </el-main>
        </el-container>
      </el-main>
    </el-container>
  </div>
</template>

<script>
export default {
  data() {
    return {
      //自己
      user: {},
      //要私信的人
      anotherUser: {},
      //在线的用户
      userlist: [],
      //要私信的人在userlist的索引位置
      index: 0,
      //消息队列集合 [本人和第一个人之间的消息集合、本人和第二个人之间的消息集合、...]
      messageList: [],
      //新消息个数集合
      new_message_num: [],
      //将要发送的内容
      inputValue: "",
      //websocket
      websocket: null,
    };
  },
  methods: {
    //获取自己被分配的信息
    getYourInfo(uid) {
      let params = new URLSearchParams();
      this.$axios
        .post("/user/getYourInfo/" + uid, params)
        .then((res) => {
          this.user = res.data.data;
          if (res.data.code == 200) {
            //获取在线用户
            this.getUserList();
          }
        })
        .catch((err) => {
          console.error(err);
        });
    },
    //获取在线用户
    getUserList() {
      let params = new URLSearchParams();
      this.$axios
        .post("/user/getUserList", params)
        .then((res) => {
          this.userlist = res.data.data.filter(
            //去掉自己
            (user) => user.uid !== this.user.uid
          );
          //填充消息数据 messagelist:[[]、[]...]  并且将新消息队列置为0
          for (let i = 0; i < this.userlist.length; i++) {
            this.messageList.push([]);
            this.new_message_num.push(0);
          }
          //将当前的客户端和服务端进行连接,并定义接收到消息的处理逻辑
          this.init(this.user.uid);
        })
        .catch((err) => {
          console.error(err);
        });
    },
    //选择聊天对象
    selectUser(index) {
      this.anotherUser = this.userlist[index];
      this.index = index;
      //将新消息置为0
      this.new_message_num[index] = 0;
    },
    //将当前的客户端和服务端进行连接,并定义接收到消息的处理逻辑
    init(uid) {
      var self = this;
      if (typeof WebSocket == "undefined") {
        console.log("您的浏览器不支持WebSocket");
        return;
      }
      //清除之前的记录
      if (this.websocket != null) {
        this.websocket.close();
        this.websocket = null;
      }
      //-----------------------连接服务器-----------------------
      let socketUrl = "ws://localhost:8888/wechat";
      //开启WebSocket 连接
      this.websocket = new WebSocket(socketUrl);

      //指定连接成功后的回调函数
      this.websocket.onopen = function () {
        console.log("websocket已打开");
        //发送一次注册消息(使后端先绑定channel和用户的关系,以至于找到对应的channel转发消息)
        let message = {
          FromUid: uid,
          ToUid: uid,
          msg: uid + "的绑定消息",
          time: new Date().toLocaleTimeString(),
        };
        self.websocket.send(JSON.stringify(message));
      };
      //指定连接失败后的回调函数
      this.websocket.onerror = function () {
        console.log("websocket发生了错误");
      };
      //指定当从服务器接受到信息时的回调函数
      this.websocket.onmessage = function (msg) {
        //消息体例如{"FromUid":1,"ToUid":2,"msg":"你好","time":"00:07:03"} => message对象
        let data = JSON.parse(msg.data);
        //添加到对应的消息集合中
        let index = data.FromUid > uid ? data.FromUid - 2 : data.FromUid - 1;
        self.messageList[index].push(data);
        //新消息数+1
        self.new_message_num[index]++;
      };
      //指定连接关闭后的回调函数
      this.websocket.onclose = function () {
        console.log("websocket已关闭");
      };
    },
    //发送信息
    senMessage() {
      //消息体例如{"FromUid":1,"ToUid":2,"msg":"你好","time":"00:07:03"}
      let message = {
        FromUid: this.user.uid,
        ToUid: this.anotherUser.uid,
        msg: this.inputValue,
        time: new Date().toLocaleTimeString(),
      };
      //将消息插进消息队列,显示在前端
      this.messageList[this.index].push(message);
      //将消息发送至服务器端再转发到对应的用户
      this.websocket.send(JSON.stringify(message));
      //清空一下输入框内容
      this.inputValue = "";
    },
  },
  created() {
    let uid = this.$route.query.uid;
    if (uid != undefined) {
      //获取被分配的用户信息
      this.getYourInfo(uid);
    }
  },
};
</script>

<style>
/*改变滚动条 */
::-webkit-scrollbar {
  width: 3px;
  border-radius: 4px;
}

::-webkit-scrollbar-track {
  background-color: inherit;
  -webkit-border-radius: 4px;
  -moz-border-radius: 4px;
  border-radius: 4px;
}

::-webkit-scrollbar-thumb {
  background-color: #c3c9cd;
  -webkit-border-radius: 4px;
  -moz-border-radius: 4px;
  border-radius: 4px;
}
.bg {
  background: url("https://s1.ax1x.com/2022/06/12/Xgr9u6.jpg") no-repeat top;
  background-size: cover;
  background-attachment: fixed;
  width: 100%;
  height: 100%;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}
.wechat {
  width: 60%;
  height: 88%;
  margin: 3% auto;
  border-radius: 20px;
  background-color: rgba(245, 237, 237, 0.3);
}
/*聊天框左侧 */
.item {
  position: relative;
  width: 94%;
  height: 50px;
  margin-bottom: 3%;
  border-bottom: 1px solid #fff;
}
.item .name {
  line-height: 50px;
  float: left;
  margin-left: 10px;
}
/*聊天框右侧 */

.wechat_right {
  position: relative;
  width: 100%;
  height: 100%;
}
.header {
  text-align: left;
  height: 50px !important;
}
.showChat {
  width: 100%;
  height: 65%;
}
.inputValue {
  position: relative;
  margin: 0;
  padding: 0;
  width: 100%;
  height: 50%;
}
.inputValue #chat {
  font-size: 18px;
  width: 96%;
  height: 94%;
  border-radius: 20px;
  resize: none;
  background-color: rgba(245, 237, 237, 0.3);
}
#send {
  position: absolute;
  bottom: 12%;
  right: 6%;
}
/*展示区 */
.leftBox {
  float: left;
  max-width: 60%;
  padding: 8px;
  position: relative;
  font-size: 18px;
  border-radius: 12px;
  background-color: rgba(40, 208, 250, 0.76);
}
.rightBox {
  float: right;
  max-width: 60%;
  padding: 8px;
  font-size: 18px;
  border-radius: 12px;
  position: relative;
  background-color: rgba(101, 240, 21, 0.945);
}
.myBr {
  float: left;
  width: 100%;
  height: 20px;
}
.leftBox > span {
  left: 3px;
  width: 120px;
  position: absolute;
  top: -16px;
}
.rightBox > span {
  width: 120px;
  position: absolute;
  right: 3px;
  top: -16px;
}
</style>

源码

源代码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1801959.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java基础语法整理 ----- 上

java基础语法 一、变量二、数据类型三、标识符四、键盘录入五、判断语句1. 三种格式2. 练习题 六、switch语句七、循环八、循环控制语句九、方法 一、变量 1.什么是变量&#xff1a; 在程序运行过程中&#xff0c;其值可以发生改变的量从本质上讲&#xff0c;变量是内存中的一…

Java--命令行传参

1.有时你希望运行一个程序时再传递给它消息&#xff0c;这要靠传递命令行参数给main&#xff08;&#xff09;函数实现 2.选中文件右键找到如图选项并打开 3.在文件地址下输入cmd空格符号&#xff0c;再按回车调出命令窗口 4.如图一步步进行编译&#xff0c;在向其传入参数&…

Web自动化测试-掌握selenium工具用法,使用WebDriver测试Chrome/FireFox网页(Java

目录 一、在Eclipse中构建Maven项目 1.全局配置Maven 2.配置JDK路径 3.创建Maven项目 4.引入selenium-java依赖 二、Chrome自动化脚本编写 1.创建一个ChromeTest类 2.测试ChromeDriver 3.下载chromedriver驱动 4.在脚本中通过System.setProperty方法指定chromedriver的…

PowerDesigner遍历导出所有表结构到Excel

PowerDesigner遍历导出所有表到Excel 1.打开需要导出表结构到Excel的pdm文件 2.点击Tools|Execute Commands|Edit/Run Script菜单或按下快捷键Ctrl Shift X打开脚本窗口&#xff0c;输入示例VBScript脚本&#xff0c;修改其中的Excel模板路径及工作薄页签&#xff0c;点Run…

【嵌入式】智能系统优化:【C++】驱动的【机器学习】与【数据挖掘】技术

目录 一、嵌入式系统简介 二、C在嵌入式系统中的优势 三、机器学习在嵌入式系统中的挑战 四、C实现机器学习模型的基本步骤 五、实例分析&#xff1a;使用C在嵌入式系统中实现手写数字识别 1. 数据准备 2. 模型训练与压缩 3. 模型部署 六、优化与分析 1. 模型优化 模…

Windows UAC权限详解以及因为权限不对等引发的若干问题排查

目录 1、什么是UAC&#xff1f; 2、微软为什么要设计UAC&#xff1f; 3、标准用户权限与管理员权限 4、程序到底以哪种权限运行&#xff1f;与哪些因素有关&#xff1f; 4.1、给程序设置以管理员权限运行的属性 4.2、当前登录用户的类型 5、案例1 - 无法在企业微信聊天框…

力扣爆刷第148天之贪心算法五连刷(区间合并)

力扣爆刷第148天之贪心算法五连刷&#xff08;区间合并&#xff09; 文章目录 力扣爆刷第148天之贪心算法五连刷&#xff08;区间合并&#xff09;一、406. 根据身高重建队列二、452. 用最少数量的箭引爆气球三、435. 无重叠区间四、763. 划分字母区间五、56. 合并区间六、738.…

【Threejs进阶教程-优化篇】3. 卡顿与内存管理(二)

贴图纹理材质光影对内存性能的影响 学习ThreeJS的捷径烘培模型简介如何优化烘培模型贴图处理贴图质量切换 为什么光源要限制数量阴影质量的影响阴影本身也可以理解为是一种贴图 学习ThreeJS的捷径 本段内容会写在0篇以外所有的&#xff0c;本人所编写的Threejs教程中 对&…

贪心算法学习二

例题一 解法&#xff08;贪⼼&#xff09;&#xff1a; 贪⼼策略&#xff1a; 由于只能交易⼀次&#xff0c;所以对于某⼀个位置 i &#xff0c;要想获得最⼤利润&#xff0c;仅需知道前⾯所有元素的最⼩ 值。然后在最⼩值的位置「买⼊」股票&#xff0c;在当前位置「卖出」…

数据挖掘--引论

数据挖掘--引论 数据挖掘--认识数据 数据挖掘--数据预处理 数据挖掘--数据仓库与联机分析处理 数据挖掘--挖掘频繁模式、关联和相关性&#xff1a;基本概念和方法 数据挖掘--分类 数据挖掘--聚类分析&#xff1a;基本概念和方法 1.2什么是数据挖掘 数据挖掘是从大量数据…

武汉理工大学 云计算与服务计算 期末复习

云计算与的定义 长定义是&#xff1a;“云计算是一种商业计算模型。它将计算任务分布在大量计算机构成的资源池上&#xff0c;使各种应用系统能够根据需要获取计算力、存储空间和信息服务。” 短定义是&#xff1a;“云计算是通过网络按需提供可动态伸缩的廉价计算服务。 云计…

Pytorch语义分割(1)-----加载数据

一、数据标注 &#xff08;1&#xff09;使用labelme来进行分割标注&#xff0c;标注完之后会得到一个json&#xff0c;数据格式如下&#xff1a; 二、获取数据信息 读取json文件来得到标签信息&#xff0c;读取jpg文件获取图像。在语义分割中用到的数据无非就是原始图片&…

Redis-sentinel(哨兵模式)的搭建步骤及相关知识

1、什么是redis-sentinel&#xff0c;和redis主从复制相比&#xff0c;它具有什么优势 1.1、redis主从复制 Redis主从复制是一种用于数据冗余和可伸缩性的机制&#xff0c;它将一台Redis服务器的数据复制到其他Redis服务器。在这种模式下&#xff0c;数据会实时地从一个主节点…

SwiftUI六组合复杂用户界面

代码下载 应用的首页是一个纵向滚动的地标类别列表&#xff0c;每一个类别内部是一个横向滑动列表。随后将构建应用的页面导航&#xff0c;这个过程中可以学习到如果组合各种视图&#xff0c;并让它们适配不同的设备尺寸和设备方向。 下载起步项目并跟着本篇教程一步步实践&a…

wx 生命周期

以下内容你不需要立马完全弄明白&#xff0c;不过以后它会有帮助。 下图说明了页面 Page 实例的生命周期。

记录jenkins pipeline ,git+maven+sonarqube+打包镜像上传到阿里云镜像仓库

1、阶段视图&#xff1a; 2、准备工作 所需工具与插件 jdk&#xff1a;可以存在多版本 maven&#xff1a;可以存在多版本 sonar-scanner 凭证令牌 gitlab&#xff1a;credentialsId sonarqube:配置在sonarqube208服务中 3、jenkinsfile pipeline {agent anystages {stage(从…

GSS7000卫星导航模拟器结合RTKLIB 接收NTRIP网络RTCM数据以输出RS232

本文聚焦&#xff0c;使用GSS7000仿真GNSS NTRIP&#xff0c;利用开源工具RTKLIB 作为NTRIP Client 接受GSS7000仿真的RTCM数据&#xff0c; 并通过STRSVR将收到的RTCM数据通过USB-RS232数据线吐出&#xff0c;并转给DUT&#xff0c;让其获得RTK -FIXED 固定解。 废话不多说&a…

微信小程序 导航navigation-bar

属性类型默认值必填说明最低版本titlestring否导航条标题2.9.0loadingbooleanfalse否是否在导航条显示 loading 加载提示2.9.0front-colorstring否导航条前景颜色值&#xff0c;包括按钮、标题、状态栏的颜色&#xff0c;仅支持 #ffffff 和 #0000002.9.0background-colorstring…

如何提高网站收录?

GSI服务就是专门干这个的&#xff0c;这个服务用的是光算科技自己研发的GPC爬虫池系统。这个系统通过建立一个庞大的站群和复杂的链接结构&#xff0c;来吸引谷歌的爬虫。这样一来&#xff0c;你的网站就能更频繁地被谷歌的爬虫访问&#xff0c;从而提高被收录的机会。 说到效…