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

先看看这个使用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>

源码

源代码

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java--命令行传参

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

什么情况下需要配戴助听器

以下几种情况需要考虑配戴助听器&#xff1a; 1、听力无波动3个月以上的感音神经性听力障碍。如:先天性听力障碍、老年性听力障碍、噪声性听力障碍、突聋的稳定期等&#xff0c;均可选配合适的助听器。 2、年龄方面。使用助听器没有严格的年龄限制&#xff0c;从出生数周的婴…

【Python】解决Python报错:ValueError: not enough values to unpack (expected 2, got 1)

​​​​ 文章目录 引言1. 错误详解2. 常见的出错场景2.1 函数返回值解包2.2 遍历含有不同长度元组的列表 3. 解决方案3.1 检查和调整返回值3.2 安全的解包操作 4. 预防措施4.1 使用异常处理4.2 单元测试 结语 引言 在Python编程中&#xff0c;ValueError 是一个常见的异常类…

win10重装系统?电脑系统重装一键清晰,干货分享!

在电脑的使用过程中&#xff0c;由于各种原因&#xff0c;我们可能会遇到系统崩溃、运行缓慢或者出现各种难以解决的问题。这时&#xff0c;重装系统往往是一个有效的解决方案。今天&#xff0c;我们就来详细介绍一下如何在Win10环境下进行系统的重装&#xff0c;帮助大家轻松解…

springboot+mqtt使用总结

1.软件的选型 1.1.使用免费版EMQX 1.1.1.下载 百度搜索的目前是会打开官网&#xff0c;这里提供下免费版的使用链接EMQX使用手册 文档很详细&#xff0c;这里不再记录了。 1.2.使用rabbitmq rabbitmq一般做消息队列用&#xff0c;作为mqtt用我没有找到详细资料&#xff0c…

[AIGC] SpringBoot的自动配置解析

下面是一篇关于SpringBoot自动配置的文章&#xff0c;里面包含了一个简单的示例来解释自动配置的原理。 SpringBoot的自动配置解析 Spring Boot是Spring的一个子项目&#xff0c;用于快速开发应用程序。它主要是简化新Spring应用的初始建立以及开发过程。其中&#xff0c;自动…

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

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

批量转换更高效:一键修改TXT后缀名转DOCX,轻松实现文件高效管理!

在日常生活和工作中&#xff0c;我们经常需要处理大量的文件&#xff0c;而文件格式的转换和管理往往是其中一项繁琐的任务。特别是当需要将大量的TXT文件转换为DOCX格式时&#xff0c;传统的逐个手动操作不仅效率低下&#xff0c;还容易出错。然而&#xff0c;现在有了我们这款…

【ArcGIS微课1000例】0117:ArcGIS中如何将kml(kmz)文件转json(geojson)?

文章目录 一、kml获取方式二、kml转图层三、图层转json一、kml获取方式 kml文件是一种很常用的数据格式,可以从谷歌地球(googleearth)获取某一个地区的kml范围文件,如青海湖(做好的kml文件可以从配套实验数据包0117.rar中获取)。 二、kml转图层 打开【KML转图层】工具,…

在线渲染3d怎么用?3d快速渲染步骤设置

在线渲染3D模型是一种高效的技术&#xff0c;它允许艺术家和设计师通过互联网访问远程服务器的强大计算能力&#xff0c;从而加速渲染过程。无论是复杂的场景还是高质量的视觉效果&#xff0c;在线渲染服务都能帮助您节省宝贵的时间。 在线渲染3D一般选择的是&#xff1a;云渲染…

数据结构之初识泛型

目录&#xff1a; 一.什么是泛型 二.引出泛型 三.泛型语法及&#xff0c;泛型类的使用和裸类型(Raw Type) 的了解 . 四.泛型的编译&#xff1a; 五.泛型的上界 六.泛型方法 注意&#xff1a;在看泛型之前可以&#xff0c;回顾一下&#xff0c;包装类&#xff0c;包装类就是服务…

【Python预处理系列】深入理解过采样技术及其Python实现

目录 一、过采样简介 二、过采样的实现方法 三、过采样和欠采样是数据增强吗 四、Python实现SMOTE过采样 &#xff08;一) 生成不平衡数据集 &#xff08;二&#xff09; 将数据集转换为DataFrame&#xff0c;便于展示 &#xff08;三) 应用SMOTE算法进行过采样 &…

命令行打包最简单的android项目从零开始到最终apk文件

准备好需要的工具 AndroidDevTools - Android开发工具 Android SDK下载 Android Studio下载 Gradle下载 SDK Tools下载 jdk的链接我就不发出来,自己选择,我接下来用的是8版本的jdk和android10的sdk sdk的安装和环境变量的配置 sdk tool压缩包打开后是这样子,打开sdk mana…

3-1RT-Thread时钟管理

这里写自定义目录标题 时钟节拍是RT thread操作系统的最小时间单位。 第一个功能&#xff0c;rt tick值自动加1&#xff0c;在RT thread当中通过RT_USING_SMP定义了多核和单核的场景。第二个功能&#xff0c;检查当前线程的时间片&#xff0c;首先获取当前线程&#xff0c;将当…

[数据集][目标检测]室内积水检测数据集VOC+YOLO格式761张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;761 标注数量(xml文件个数)&#xff1a;761 标注数量(txt文件个数)&#xff1a;761 标注类别…

【Text2SQL 论文】PET-SQL:用 Cross-Consistency 的 prompt 增强的两阶段 Text2SQL 框架

论文&#xff1a;PET-SQL: A Prompt-enhanced Two-stage Text-to-SQL Framework with Cross-consistency ⭐⭐⭐ arXiv:2403.09732&#xff0c;商汤 & 北大 Code&#xff1a;GitHub 一、论文速读 论文一开始提出了以往 prompt-based 的 Text2SQL 方法的一些缺点&#xff1…

Linux卸载残留MySQL【带图文命令巨详细】

Linux卸载残留MySQL 1、检查残留mysql2、检查并删除残留mysql依赖3、检查是否自带mariadb库 1、检查残留mysql 如果残留mysql组件&#xff0c;使用命令 rpm -e --nodeps 残留组件名 按顺序进行移除操作 #检查系统是否残留过mysql rpm -qa | grep mysql2、检查并删除残留mysql…

[职场] 关于薪酬需要知道的两个知识点 #知识分享#知识分享

关于薪酬需要知道的两个知识点 薪酬问题是面试过程中比较核心的问题&#xff0c;也是每次面试必问的。如果你进入到面试的后一阶段&#xff0c;这类问题可以让面试官或企业判断求职者的要求是否符合企业的薪酬标准&#xff0c;并进一步判断求职者对自身价值的认可程度。关于薪…

设计模式-六大原则

概述 设计模式体现的是软件设计的思想&#xff0c;而不是软件技术&#xff0c;它重在使用接口与抽象类来解决各种问题。在使用这些设计模式时&#xff0c;应该首先遵守六大原则。 原则含义具体方法开闭原则对扩展开放&#xff0c;对修改关闭多使用抽象类和接口里氏代换原则基…

文件属性与目录

一、Linux 系统中的文件类型 Linux 系统中的文件类型 Linux 下一切皆文件&#xff0c;文件作为 Linux 系统设计思想的核心理念。 1、普通文件 普通文件&#xff08; regular file &#xff09;在 Linux 系统下是最常见的&#xff0c;譬如文本文件、二进制文件&#xff0c…