当前位置:   article > 正文

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

springboot netty websocket vue

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

springboot+websocket+vue聊天室

一、实现内容

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 自定义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());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 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();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351

源码

源代码

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/正经夜光杯/article/detail/892555
推荐阅读
相关标签
  

闽ICP备14008679号