netty+springboot+vue聊天室(需要了解netty)-二、代码实现

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>

源码

源代码

上一篇:自然资源-国土空间规划术语集合


下一篇:进入mysql报错:bash:/bin/mysql:没有那个文件或目录