精华内容
下载资源
问答
  • 使用netty+Vue在写一个聊天室的例子的时候, 发现vue 好像没有类似jquery的append实现有先后顺序的插入dom节点的操作。于是查阅资料,发现可以使用v-for读一个数组,然后后台获取的信息再push到数组中,就可以实现...

    使用netty+Vue在写一个聊天室的例子的时候, 发现vue 好像没有类似jquery的append实现有先后顺序的插入dom节点的操作。于是查阅资料,发现可以使用v-for读一个数组,然后后台获取的信息再push到数组中,就可以实现类似于append的操作了。

    前端样式代码:半块菠萝 侵删
    在这里插入图片描述
    使用v-if 或者 v-show 判断类型是别人的消息还是自己发出的消息, 两者只渲染一个

    <div id="msgframe">
    	<div class="pnl-list" id="msgs" v-for="msg in left">
    		<div class="msg robot" v-if="msg.type ? false : true ">
    	    	<div class="msg-left" :worker="msg.name">
    	    		<div class="msg-host photo"></div>
    	    		<div class="msg-ball" :title="msg.time"  v-cloak v-html="msg.content"></div>
    			</div>
    		</div>
    		<div class="msg guest" v-if="msg.type ? true : false ">
    			<div class="msg-right" >
    	    		<div class="msg-host headDefault"></div>
    	    		<div class="msg-ball" :title="msg.time" v-cloak v-html="msg.content"></div>
    	    	</div>
    	    </div>
    	</div>
    </div>
    
    var chats = new Vue({
            el: '#msgframe',
            data: {
                content: '',
                msglist: []
            },
            methods: {
                // 点击按钮发送消息
                sendmsg() {
                    if (this.content === '') {
                        return ;
                    }
                    let list1 = {
                        // 区分自己发的消息还是接收到的消息, true=自己发的消息
                        type: true,
                        time: new Date(),
                        name:  username,
                        content: this.content
                    };
                    // push到所有消息列表中
                    chats.msglist.push(list1);
                    // 发送消息
                    CHAT.socket.send(list1);
                    // 清空输入框中的内容
                    this.content= '';
                }
            }
        });
    

    接收到消息时同理, 只要把数据放在list中 push到msglist数组中, 类型设置为false, 就可以达到以上效果了。

    展开全文
  • vue web在线聊天功能实现

    千次阅读 2020-12-16 17:45:17
    上一篇介绍了vue怎么实现无限滚动窗体,这一篇就具体怎么使用vue实现web在线聊天功能展开深入讨论。 对尚且不清楚怎么实现无限滚动窗体的,可前往这里查看《vue和iview实现无限滚动的正确解法》 先看看最终实现的...

    上一篇介绍了vue怎么实现无限滚动窗体,这一篇就具体怎么使用vue实现web在线聊天功能展开深入讨论。

    对尚且不清楚怎么实现无限滚动窗体的,可前往这里查看《vue和iview实现无限滚动的正确解法》

     先看看最终实现的效果

     实现过程


    无限滚动窗体的实现之前已经介绍过,这里就不在赘述了,不清楚的可以通过文档前文的传送门进行查看。

    实时在线聊天主要功能点

    • 滚动到两天窗体顶部,自动加载历史跟多信息,数据加载的时候,需要有一个loading动画;
    • 发送信息是滚动条自动滑动到窗体底部,并且自己发送的信息出现在聊天窗体中;
    • 收到别人发送信息时,需要判断滚动条处于窗体中的位置,在距离底部一定范围内收到信息需要自动滑动到窗体底部;
    • 收发的信息在聊天状态不能重复显示;
    • 收发的信息在聊天窗体中需要以逆序的方式展示,即离窗体底部越近的信息为最新消息;
    • 授信最好通过WebSocket与后端建立长连接,有新消息由后端主动向前端推送消息方式实现,这里主要介绍前端实现聊天窗体思路,WebSocket部分就不展开了,采用定时器轮询的方式简单实现。

    话不多说,直接上代码


    后端返回数据格式

    我觉得所有的设计和功能实现都是基于数据的基础上去实现的,所以咋们先来看一下后端返回的数据格式:

    {
    	"code": 200, // 响应编码
    	"msg": "OK", // 响应消息
    	"total": 1, 
    	"sysTime": "2020-12-16 15:23:27", // 系统响应时间
    	"data": [{
    		"avatar": "",  // 用户头像
    		"content": "{\"type\":\"txt\",\"msg\":\"你好!\"}", // 消息内容
    		"isRead": 0, // 是否已读
    		"isOneself": 0,  // 是否是自己发送的消息 0否,1是
    		"msgId": 10, // 消息ID,用来去重
    		"nickName": "碧海燕鱼", // 用户昵称
    		"userCode": "202012162030202232" // 用户编码
    	}]
    }

     这里需要说明的是,content字段返回的是一个json格式的字符串数据,content内容格式如下:

    // 文本消息
    {
      "type": "txt",
      "msg":"你好" //消息内容
    }
    
    // 图片消息
    {
      "type": "img",
      "url": "图片地址",
      "ext":"jpg",
      "width":360,    //宽
      "height":480,    //高
      "size": 388245
    }
    
    // 视频消息
    {
      "type": 'video',
      "url": "http://nimtest.nos.netease.com/cbc500e8-e19c-4b0f-834b-c32d4dc1075e",
      "ext":"mp4",
      "width":360,    //宽
      "height":480,    //高
      "size": 388245
    }
    
    // 地理位置消息
    {
      "type": "local",
      "address":"中国 浙江省 杭州市 网商路 599号",    //地理位置
      "longitude":120.1908686708565,        // 经度
      "latitude":30.18704515647036            // 纬度
    }

    HTML代码

    <template>
      <Modal title="在线沟通" v-model="chatVisible"
       draggable
       footer-hide
       :width="580" @on-cancel="cancel">
       <div class="chat">
         <div  class="chat-message-body" id ="chatform" @scroll="scroll"
          >
          <Spin v-if="loading">
            <Icon type="ios-loading" size=18 class="spin-icon-load"></Icon>
          </Spin>
            <div  dis-hover v-for="(item,index) in data"
             :key="index" class="message-card">
             <div :class="item.isOneself == 1?'message-row-right': 'message-row-left'">
               <img :src="item.avatar?item.avatar:defualtAvatar" 
                height="35" width="35" >
                <div class="message-content"> 
                  <div :style="item.isOneself == 1?'text-align:right;display: flex;flex-direction:row-reverse':''">
                    {{item.nickName}}
                    <span class="message-time">
                       {{item.createTime}}</span>
                    </div>
                  <div class="message-body">
                    {{item.content.msg}}
                    </div>
                 </div> 
              </div>
             </div>
          </div>
            <Input
            v-model="form.msg"
            type="textarea"
            style="margin:10px 0;"
            placeholder="主动一点,世界会更大!"
            :rows="4"
          />
         </div>
         <div class="footer-btn">
            <Button @click="cancel" type="text">取消</Button>
            <Button type="primary" @click="sendMsg">发送</Button>
          </div>
      </Modal>
    </template>

     注:自己发的信息和别人发的信息展示样式不一样,所以需要通过isOneself字段进行展示样式的区分。

    JavaScript代码

    <script>
    import {listMsg,sendMsg } from "@/api/index";
    export default {
      name: "chat",
      props: {
        value: {
          type: Boolean,
          default: false
        }
      },
      data() {
        return {
          chatVisible:this.value,
          loading:false,
          defualtAvatar:require('../../assets/defult-avatar.svg'), // 后端没有返回头像默认头像,注意:需要用require请求方式才能动态访问本地文件
          data:[],
          distincData:[], // 消息去重数组
          offsetMax:0, // 最大偏移位,记录当前获取的最大id,往后的定时轮询数据时每次只获取比这个id大的数据
          offsetMin:0,  // 最小偏移位,记录当前获取的最小id,往上滑动时每次只获取比这小id大的数据
          searchForm:{ // 每次定时获取数据或首次加载数据提交的form表单数据
            pageNumber: 1,
            pageSize: 20
          },
          form:{ // 发送数据提交数据表单
            content:"",
            msg:""
          },
          timerSwitch:0 // 定时器开关,默认关闭
        };
      },
      methods: {
        init(){
          
        },
        loadMsg(){ // 窗体打开默认加载一页数据,窗体什么周期中值运行一次
          let that = this;
          this.searchForm.offsetMax = this.offsetMax;
          listMsg(this.searchForm).then(res=>{
            if (res.code == 200) {
              res.data.forEach(e => {
                // 标记最大偏移位
                if(that.offsetMax < e.msgId){
                    that.offsetMax = e.msgId;
                }
                e.content = JSON.parse(e.content);
                that.data.unshift(e)
                that.distincData.push(e.msgId);
                // 标记最大偏移位,后端返回数据是逆序,所以最后一条id最新
                that.offsetMin = e.msgId;
               });
              // 数据加载完成,滚动条滚动到窗体底部
              this.scrollToBottom();
            }
          });
           
            
        },
        show(){ // 打开窗体初始化数据
          // 初始化数据
          this.data =[];
          this.distincData =[];
          this.offsetMax = 0;
          this.offsetMin = 0;
          this.searchForm.pageNumber = 1;
          this.searchForm.pageSize = 20;
          this.form ={
            content:"",
            msg:""
          };
          this.loadMsg();
          this.chatVisible = true;
          // 开启定时器
          this.timerSwitch = 1;
          this.reloadData();
        },
        sendMsg(){ // 发送消息
          if(!this.form.msg){
             this.$Message.warning("不能发送空白信息");
            return;
          }
          let content = { // 封装消息体
            type:"txt",
            msg:this.form.msg
          }; 
          this.form.content = JSON.stringify(content);
          sendOrderMsg(this.form).then(res=>{
            if (res.code == 200) {
              res.data.content = JSON.parse(res.data.content);
              this.data.push(res.data)
              this.form.msg="";
              this.distincData.push(res.data.msgId);
              this.scrollToBottom();
              // 发送信息只返回当前一条,此时可能对方已经发送信息,所以不修改偏移量
            }
          });
        },
        scrollToBottom(){ // 滚动到窗体底部
          this.$nextTick(()=>{
              let chatform = document.getElementById("chatform");
              chatform.scrollTop = chatform.scrollHeight;
          });
        },
        // 滚动到最上方,取历史数据,根据分页参数取。不用修改偏移标记位,但是需要判重
        scroll(){
          let chatform = document.getElementById("chatform");
          let scrollTop = chatform.scrollTop;
          if(scrollTop == 0){
            this.loading =true;
            let that = this;
            this.searchForm.offsetMin = this.offsetMin;
            this.searchForm.offsetMax = "";
            listMsgByOrder(this.searchForm).then(res=>{
               this.loading =false;
                if (res.code == 200) {
                  res.data.forEach(e => {
                    if(that.distincData.indexOf(e.msgId) <0){
                      e.content = JSON.parse(e.content);
                      that.data.unshift(e);
                      that.distincData.push(e.msgId);
                      // 修改最小偏移位
                      if(that.offsetMin > e.msgId){
                          that.offsetMin = e.msgId;
                      }
                    }
                  });
                }
            });
          }
        },
       reloadData(){
        // 判断定时器开关是否开启,如果开启,则执行定时器
        if(this.timerSwitch){
          setTimeout(() => {
            let params = {};
            params.pageNumber = 1;
            params.pageSize = 20;
            params.offsetMax = this.offsetMax;
            let that = this;
            listMsgByOrder(params).then(res=>{
              if (res.code == 200) {
                res.data.forEach(e => {
                  // 修改最大偏移位,放到校验重复之前,防止当前发送信息已经放入消息列表,但是偏移值没该的情况
                  if(that.offsetMax < e.msgId){
                      that.offsetMax = e.msgId;
                  }
                  if(that.distincData.indexOf(e.msgId) <0){
                    e.content = JSON.parse(e.content);
                    that.data.push(e)
                    that.distincData.push(e.msgId);
                    // 收到新消息,判断高度,如果当前滚动条高度距底部小于100,则动滑到底部
                    let chatform = document.getElementById("chatform");
                    let gap = chatform.scrollHeight -chatform.scrollTop;
                    if(gap >0 && gap < 400){
                      this.scrollToBottom();
                    }
                  }
                });
                that.reloadData();
              }
            });
          },1000*2);
        }
        
       },
       cancel(){ // 关闭窗体需要把提示任务开关一起关闭调
         this.chatVisible = false;
         this.timerSwitch = 0;
       }
      },
      mounted() {
      }
    };
    </script>

     

    CSS代码

     

    <style lang="less">
       .message {
            height: 350px;
        }
      .ivu-card-body {
        padding:5px;
      }
      .ivu-modal-body{
        padding: 0px 16px 16px  16px;
      }
      .chat-message-body {
       background-color:#F8F8F6;
       width:545px;
       height: 350px;
       overflow: auto;
      }
      .message-card {
       margin:5px;
      }
      .message-row-left {
       display: flex;
       flex-direction:row;
      }
      .message-row-right {
       display: flex;
       flex-direction:row-reverse;
      }
      .message-content {
        margin:-5px 5px 5px 5px;
        display: flex;
        flex-direction:column;
      }
      .message-body {
        border:1px solid #D9DAD9;
        padding:5px;
        border-radius:3px;
        background-color:#FFF;
      }
      .message-time {
        margin:0 5px;
        font-size:5px;
        color:#D9DAD9;
      }
      .footer-btn {
        float:right;
        margin-bottom: 5px;
      }
      .spin-icon-load {
        animation:ani-spin 1s linear infinite;
      }
      @keyframes ani-spin{
        form{transform: rotate(0deg);}
        50% {transform: rotate(180deg);}
        to  {transform: rotate(360deg);}
      }
    </style>

     

    展开全文
  • 前端使用了vue框架实现用户登录、通讯录列表、联系人搜索以及聊天功能。具体的聊天功能使用了netty+WebSocket技术实现,所有的聊天数据都保存在系统本地,服务器只进行了数据转发。 登录页面 登陆页面(背景是自动...

    系统整体功能描述
    前端使用了vue框架实现用户登录、通讯录列表、联系人搜索以及聊天功能。具体的聊天功能使用了netty+WebSocket技术实现,所有的聊天数据都保存在系统本地,服务器只进行了数据转发。

    登录页面

    登陆页面(背景是自动播放的小视频)
    在这里插入图片描述
    通讯录
    在这里插入图片描述

    联系人搜索
    在这里插入图片描述
    聊天详情
    在这里插入图片描述
    前端主要代码
    1. 对websokcet连接进行封装socket.js

      const defaultData = {
    	  event: 'msg',
    	  username: '',
    	  from: '',
    	  to: "all",
    	  type: 'text',
    	  data: ''
    	}
    	
    	class WSocket {
    	  wss;
    	
    	  constructor() {
    	    if (!("WebSocket" in window)) {
    	      alert("您的浏览器不支持 WebSocket!");
    	    }
    	  }
    	
    	  open (c) {
    	    // 貌似不在短时间内调用onopen 会自动调用,所以在此处new
    	    try {
    	      this.wss = new WebSocket("ws://127.0.0.1:8088/ws");
    	    } catch( err ) {
    	      alert('连接服务器失败');
    	    }
    	    c = Object.assign(defaultData, c);
    	    let _self = this;
    	    this.wss.onopen = function() {
    	      this.send(JSON.stringify(c));
    	      setInterval(function(){
    	        let c = {
    	          event: 'heartbeat',
    	          username: '',
    	          from: '1015',
    	          to: "1015",
    	          type: 'text',
    	          data: '心跳'
    	        }
    	        console.log("心跳")
    	        _self.send(c)
    	      }, 50000);
    	    };
    	    this.wss.onclose = function (e) {
    	      console.log('websocket 断开: ' + e.code + ' ' + e.reason + ' ' + e.wasClean)
    	      console.log(e)
    	    }
    	  }
    	
    	  message (f) {
    	    this.wss.onmessage = function(evt) {
    	      f(evt.data);
    	    };
    	  }
    	  
    	  send (c) {
    	    c = Object.assign(defaultData, c);
    	    this.wss.send(JSON.stringify(c));
    	  }
    	
    	}
    	export default new WSocket();
    

    2.使用vuex(至关重要)

    	import Vue from "vue";
    	import Vuex from "vuex";
    	import ws from "@/net/socket";
    	import { getLocal } from '@/utils/mylocal'
    	
    	
    	
    	Vue.use(Vuex);
    	
    	export default new Vuex.Store({
    	  state: {
    	    // 用户信息
    	    user: getLocal('userInfo') || {},
    	    currentChat: { // 当前窗口信息
    	      who: '',
    	      name: ''
    	    },
    	    isNeedPush: 0, // 控制了解界面刷新显示消息
    	  },
    	  mutations: {
    	    // 初始化连接 注册
    	    initConnect (state, callback) {
    	      const u = {
    	        event: "reg",
    	        from: state.user.userId,
    	        username: state.user.username,
    	        to: "all",
    	        data: state.user.username + '上线啦'
    	      }
    	      ws.open(u);
    	      ws.message((msg)=>{
    	        msg = JSON.parse(msg);
    	        if (msg.event == 'msg') {
    	          // 消息
    	          callback(msg);
    	        }
    	      }); 
    	    },
    	
    	    // 消息推送
    	    send(state, conf) {
    	      let _conf = {
    	        event: "msg",
    	        from: state.user.userId,
    	        username: state.user.username,
    	        to: "all",
    	        data: ""
    	      };
    	      _conf = Object.assign(_conf, conf);
    	      ws.send(_conf);
    	    },
    	
    	    // 保存聊天记录 sessionStorage.historyChat
    	    addChatStorage(state, info) {
    	      let hc = (sessionStorage.historyChat && JSON.parse(sessionStorage.historyChat)) || {};
    	      const _ct = {
    	        sendobj: info.sendobj,   // 谁发的
    	        content: info.content,  // 发送内容
    	        myHeadUrl: info.myHeadUrl,
    	        username: info.username, //发送人名称
    	        headUrl: info.headUrl,
    	        window: info.window, // 聊天窗口对象
    	        date: Number(new Date())
    	      }
    	      // 聊天记录对应某个聊天窗口对象
    	      if (!hc[_ct.window]) {
    	        hc[_ct.window] = [];
    	      }
    	      hc[_ct.window].push(_ct);
    	      sessionStorage.historyChat = JSON.stringify(hc);
    	    },
    	
    	    /**
    	     * 新消息提示
    	     */
    	    newMsg (state, cwindow) {
    	      // 先判断 如果是当前窗口,直接更新 无需加入新消息提示数组
    	      if (cwindow == state.currentChat.who) {
    	        state.isNeedPush++;  // 改变即可
    	      } else {
    	      //  // 没有则加入提示数组
    	      //  if (!state.noRead.includes(String(cwindow))) {
    	      //    state.noRead.push(String(cwindow));
    	      //  }
    	      }
    	   },
    	
    	    setUserInfo (state, data) {
    	      Object.assign(state.user,data)
    	    }
    	  },
    	  actions: {},
    	  modules: {}
    	});
    
    1. app.vue 中进行初始化websocket连接

       <script>
       export default {
         name: 'App',
         mounted () {
           this.$store.commit('initConnect', this.msgCallback)
         },
         methods : {
           /**
            * 服务器msg消息返回处理回调函数
            */
           msgCallback(msg) {
             // 直接将聊天记录存储
             const ct = {
               sendobj: msg.from,
               content: msg.data,
               myHeadUrl: msg.headUrl,
               window: msg.to == 'all' ? 'all' : msg.from,
               headUrl: msg.myHeadUrl
             };
             this.$store.commit('addChatStorage', ct); 
             // 之后更新新消息提示
             this.$store.commit('newMsg', ct.window);
           }
         }
       }
       </script>
      
    2. 聊天详情页面的处理逻辑

       <script>
       import ws from "@/net/socket";
       import { Toast } from 'vant';
       export default {
         name: "ChatArea",
         data() {
           return {
             recordContent:[],
             user_msg: "",
             who: JSON.parse(this.$route.query.user)
           };
         },
         // 监听器 用于监听何时该刷新数据
         watch: {
           // 改变聊天对象
           '$store.state.currentChat.who': {
             handler(curVal, oldVal) {
               this._resetCurrentChatStorage();
             }
           },
           // 控制刷新 新消息时
           '$store.state.isNeedPush': {
             handler(curVal, oldVal) {
               this._resetCurrentChatStorage();
             }
           }
         },
         computed: {},
         // 页面初始化
         mounted () {
           this._resetCurrentChatStorage();
         },
         created () {
           this.$store.state.currentChat.who = this.who.userId
           this.$store.state.currentChat.name = '单聊'
         },
         methods: {
           back() {
             this.$router.go(-1); //返回上一页
           },
           send() {
             let content = this.user_msg;
             content = content.replace(/^\s*/g,"").replace(/\s*$/g, "").substr(0, 200); // max 最大发送200
             if (!content) {
               return;
             }
             const conf = {
               to: this.who.userId, // 发送给当前窗口聊天对象
               data: content,
               myHeadUrl: this.$store.state.user.headimg,
               headUrl: this.who.headimg
             };
             this.$store.commit("send", conf);
             this.$store.commit('addChatStorage', {  // 保存聊天记录 sessionStorage.historyChat
               content,
               sendobj: 'me', //代表自己发的
               myHeadUrl: this.$store.state.user.headimg,
               username: this.$store.state.user.username, //发送人名称
               window: this.who.userId, // 聊天窗口对象
               headUrl: this.who.headimg
             });
             // 重新初始化 再清空输入框
             this._resetCurrentChatStorage();
             this.user_msg = ""
           },
           /**
            * 重新初始化聊天界面 获取聊天记录
            */
           _resetCurrentChatStorage () {
             const hc = (sessionStorage.historyChat && JSON.parse(sessionStorage.historyChat)) || {};
             let recordContent = hc[this.who.userId] ? hc[this.who.userId] : [];
             for(let i=0;i<recordContent.length;i++){
               if(recordContent[i].myHeadUrl == "default1"){
                 recordContent[i].myHead_url = require("../assets/img/default1.jpg")
               }else{
                 recordContent[i].myHead_url = require("../assets/img/default2.jpg")
               }
       
               if(recordContent[i].headUrl == "default1"){
                 recordContent[i].head_url = require("../assets/img/default1.jpg")
               }else{
                 recordContent[i].head_url = require("../assets/img/default2.jpg")
               }
             }
             this.recordContent = recordContent
             // 同时将滚动条放置聊天底部 这里要延时才行,原因不明
             setTimeout(()=> {
               this.$refs.chatbox.scrollTop = this.$refs.chatbox.scrollHeight;
             }, 10);
           }
         }
       };
       </script>
      

    后台实现(springboot+netty+mybatis)

        实现客户端的长连接以及心跳检测的配置等功能
    

    主要代码
    pom.xml依赖

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.32.Final</version>
        </dependency>
        
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>
        
        <dependency>
            <groupId>net.sf.json-lib</groupId>
            <artifactId>json-lib</artifactId>
            <version>2.2.3</version>
            <classifier>jdk15</classifier>
        </dependency>
    
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
    
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
    

    netty及数据库配置

     yml文件的具体配置
    	
    	server:
      		port: 8090
    	netty:
    	   tcp:
    	    server:
    	      host: 127.0.0.1
    	      port: 8088
    	spring:
    	  datasource: #数据库
    	    username: root
    	    password: root
    	    url: jdbc:mysql://127.0.0.1:3306/vue_websocket_netty?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
    	    driver-class-name: com.mysql.jdbc.Driver
    	mybatis:
    	  mapper-locations: classpath:mapping/*Mapper.xml
    	  type-aliases-package: com.netty.chat.entity
    

    nettyServer类:

    package com.netty.chat.chatRoom;
    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.PostConstruct;
    
    @Component
    @Slf4j
    public class NettyServer implements CommandLineRunner {
        private static NettyServer nettyServer;
        private ServerBootstrap server;
        private EventLoopGroup boss;
        private EventLoopGroup woker;
        private ChannelFuture future;
    
    
    
        @PostConstruct
        public void init(){
            nettyServer = this;
        }
    
        @Value("${netty.tcp.server.port}")
        private  int port;
    
        private static class SingletionNettyServer{
            static final NettyServer instance = new NettyServer();
        }
    
        public static NettyServer getInstance(){
            return SingletionNettyServer.instance;
        }
    
    
        public void start() {
    
            // 创建服务类
            server = new ServerBootstrap();
    
            // 创建boss和woker
            boss = new NioEventLoopGroup();
            woker = new NioEventLoopGroup();
    
            try {
                // 设置线程池
                server.group(boss, woker);
    
                // 设置channel工厂
                server.channel(NioServerSocketChannel.class);
    
                // 设置管道
                server.childHandler(new WSServerInitializer());
    
                log.info("通道配置");
    
                // 服务器异步创建绑定
                this.future = server.bind(nettyServer.port);
                log.info("netty server 启动完毕,启动端口为:"+nettyServer.port);
    
                // 等待服务端关闭
                this.future.channel().closeFuture().sync();
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                boss.shutdownGracefully();
                woker.shutdownGracefully();
            }
        }
    
        @Override
        public void run(String... args) throws Exception {
            this.start();
        }
    }
    

    构建初始化类WSServerInitializer:

    package com.netty.chat.chatRoom;
    
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.ChannelPipeline;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.handler.codec.http.HttpObjectAggregator;
    import io.netty.handler.codec.http.HttpServerCodec;
    import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
    import io.netty.handler.stream.ChunkedWriteHandler;
    import io.netty.handler.timeout.IdleStateHandler;
    
    public class WSServerInitializer extends ChannelInitializer<SocketChannel> {
    
        @Override
        protected void initChannel(SocketChannel socketChannel) throws Exception {
    
            ChannelPipeline pipeline = socketChannel.pipeline();
            //编解码
            pipeline.addLast(new HttpServerCodec());
            //大数据流
            pipeline.addLast(new ChunkedWriteHandler());
            //聚合httpMessage 响应或者请求
            pipeline.addLast(new HttpObjectAggregator(1024*1024));
    
            //心跳机制
            pipeline.addLast(new IdleStateHandler(20, 40, 60));
            pipeline.addLast(new HeartBeatHandler());
    
            //webSocket的支持
            pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
    
            pipeline.addLast(new MyServerHandler());
        }
    }
    

    自定义处理类MyServerHandler:

    package com.netty.chat.chatRoom;
    import com.netty.chat.entity.Message;
    import com.netty.chat.entity.UserChannel;
    import io.netty.channel.Channel;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
    import io.netty.channel.group.ChannelGroup;
    import io.netty.channel.group.DefaultChannelGroup;
    import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
    import io.netty.util.concurrent.GlobalEventExecutor;
    import net.sf.json.JSONObject;
    
    public class MyServerHandler extends
            SimpleChannelInboundHandler<TextWebSocketFrame> {
    
        public static ChannelGroup clients = new DefaultChannelGroup(
                GlobalEventExecutor.INSTANCE);
    
        public static  UserChannel userChannel = new UserChannel();
    
        /**
         * 监听客户端注册
         */
        @Override
        public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
            // 新客户端连接,加入队列
            clients.add(ctx.channel());
        }
    
        /**
         * 监听客户端断开
         */
        @Override
        public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
            // 整理队列
            clients.remove(ctx.channel());
        }
    
        /**
         * 读取客户端发过来的消息
         */
        @Override
        public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame tmsg)
                throws Exception {
    
            JSONObject jsonobject = JSONObject.fromObject(tmsg.text());
            Message message= (Message)JSONObject.toBean(jsonobject, Message.class);
    
            UserChannel.put(message.getFrom(),ctx.channel());
    
            if(message.getEvent().equals("heartbeat")){
                //心跳消息不做处理
                System.out.println("心跳");
                return;
            }
    
            if(message.getTo().equals("all")){
                // 处理群聊消息
                System.out.println("群聊");
    
                for (Channel channel : clients) {
                    // 判断是否是当前用户的消息
                    if (channel != ctx.channel()) {
                        channel.writeAndFlush(msgPot(tmsg.text()));
                    } else {
                        // 自己
                    }
                }
            }else{
               //
                System.out.println("单聊");
                Channel targetChannel = UserChannel.get(message.getTo());
               targetChannel.writeAndFlush(msgPot(tmsg.text()));
            }
        }
    
        /**
         * 监听连接异常
         */
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
                throws Exception {
            ctx.close(); // 关闭
        }
    
        /**
         * 封装消息
         *
         * @param msg
         * @return
         */
        public TextWebSocketFrame msgPot(String msg) {
            return new TextWebSocketFrame(msg);
        }
    }
    

    心跳检测类HeartBeatHandler:

    package com.netty.chat.chatRoom;
    import io.netty.channel.Channel;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.ChannelInboundHandlerAdapter;
    import io.netty.handler.timeout.IdleState;
    import io.netty.handler.timeout.IdleStateEvent;
    
    public class HeartBeatHandler extends ChannelInboundHandlerAdapter {
    
        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
            //super.userEventTriggered(ctx, evt);
            // 判断evt是否是idleStateEvent 读空闲/写空闲/读写空闲
            if (evt instanceof IdleStateEvent) {
                IdleStateEvent event = (IdleStateEvent) evt;
    
                if (event.state() == IdleState.READER_IDLE) {
                    System.out.println("读空闲...");
                } else if (event.state() == IdleState.WRITER_IDLE) {
                    System.out.println("写空闲...");
                } else if (event.state() == IdleState.ALL_IDLE) {
                    //System.out.println(CustomChannelHandler.clients.size());
                    Channel channel = ctx.channel();
                    channel.close();
                    //System.out.println(CustomChannelHandler.clients.size());
                }
            }
        }
    }
    

    尾言

    如果有大神觉得可以有更好的设计思路的话,欢迎留言。
    前后台源码 :
    https://gitee.com/smallgrey/netty_websocket_vue
    https://github.com/smallgrey/vue_websocket_netty.git
    
    展开全文
  • 最近小编接了一个新项目,项目需求要实现一个聊天功能,今天小编通过实例代码给大家介绍vue cli使用融云实现聊天功能的方法,感兴趣的朋友跟随小编一起看看吧
  • Vue+express+Socket实现聊天功能实现聊天功能具体功能界面截图项目准备前端 socket安装导入后台socket安装导入项目实现Vue代码HTMl代码js代码express代码后台代码封装 实现聊天功能 具体功能 具体功能 只是为了...

    实现聊天功能

    具体功能

    具体功能

    只是为了实现功能,不对界面进行美化
    1、输入消息点击发送所有用户可以在下方收到消息

    2、输入userid后点击连接,可以连接对应的聊天,另外一个界面输入刚刚那个页面的userid后再输入内容点击发送给指定的人,则刚才那个页面可以打印输出,而其他页面不会收到,实现私聊的功能

    3、没有具体实现私聊的内容显示,但是接收发送消息都可以实现,要实现私聊的内容显示可以再添加一个私聊页面

    界面截图

    在这里插入图片描述

    项目准备

    只对socket准备进行介绍,Vue和express的搭建不进行介绍

    前端 socket

    安装

    npm i vue-socket.io --save

    导入

    import VueSocketIO from 'vue-socket.io'

    后台socket

    安装

    npm i socket.io --save

    导入

    在express-generator生产的bin/www文件夹中加入
    var io = require('socket.io')(server)'

    io.on('connection',  (socket) => {
      socket.on('demining',  (data) => {
        console.log(data);
      });
    });
    

    具体截图如下:
    在这里插入图片描述

    项目实现

    Vue代码

    HTMl代码

    <div class="home">
        userid: <input type="text" v-model="userid">
        昵称:<input type="text" v-model="name">
        消息:<input type="text" v-model="msg" />
        <button @click="send">发送</button>
        <button @click="join">连接</button>
        <button @click="sendmsg">发送给指定的人</button>
    
        <ul>
          <li v-for="(item,index) in chatList" :key="item.name + index">
            {{ item.name }}说:{{ item.msg }}
          </li>
        </ul>
      </div>
    

    js代码

    export default {
      name: "Home",
      data() {
        return {
          users: [],
          msg: "",
          chatList: [],
          name: '',
          userid: ''
        };
      },
      sockets: {
        // 连接后台socket
        connect() {
          console.log('socket connected');
        },
        // 用户后台调用,添加数据
        sendMessage(data) {
          console.log(data);
          this.chatList.push(data)
        },
        // 用户后台调用,打印数据
        receivemsg(data) {
          console.log('receivemsg');
          console.log(data);
        }
      },
      methods: {
        // 发送消息给后台
        send() {
          // 使用emit调用后台的socket中的message方法
          this.$socket.emit("message", {
            userid: 100,
            name: this.name,
            msg: this.msg
          });
        },
        // 建立用户连接
        join() {
          this.$socket.emit("join", {
            userid: this.userid
          });
        },
        // 发送消息给后台 用于私发消息
        sendmsg() {
          this.$socket.emit("sendmsg", {
            userid: this.userid,
            msg: this.msg
          });
        }
      }
    };
    

    express代码

    在刚才的www文件定义的连接中添加一下代码

    // 用于存储每个用户的socket,实现私聊的功能
    let arrAllSocket = {}
    // 穿件socket连接
    io.on('connection', (socket) => {
      console.log('连接上了');
      // console.log(socket);
      // join函数 用于用户连接
      socket.on('join', function (obj) {
        console.log(obj.userid + 'join')
        // 保存每个用户的连接状态 用于私发消息
        arrAllSocket[obj.userid] = socket
      })
      // 接收前台发送的消息 函数名为message
      socket.on('message', (data) => {
        console.log(data);
        // 将消息发送回前台(调用前台定义的方法) 函数名为sendMessage
        io.emit('sendMessage', data);
      });
      // 私发消息
      socket.on('sendmsg', function (data) {
        console.log(data);
        // 查询用户连接
        let target = arrAllSocket[data.userid]
        if (target) {
          //发送信息至指定的人
          target.emit('receivemsg', data)
        }
      })
    })
    

    后台代码封装

    由于www文件不应该写太多代码,所以对这一部分代码进行封装
    1、在项目目录下创建一个io的文件夹,结构如下在这里插入图片描述
    2、将刚才的那部分代码移入io/index.js中
    代码如下

    // 将server作为参数传入
    module.exports = function (server) {
      var io = require('socket.io')(server);
    // 用于存储每个用户的socket,实现私聊的功能
      let arrAllSocket = {}
    // 穿件socket连接
      io.on('connection', (socket) => {
        console.log('连接上了');
        // console.log(socket);
        // join函数 用于用户连接
        socket.on('join', function (obj) {
          console.log(obj.userid + 'join')
          // 保存每个用户的连接状态 用于私发消息
          arrAllSocket[obj.userid] = socket
        })
        // 接收前台发送的消息 函数名为message
        socket.on('message', (data) => {
          console.log(data);
          // 将消息发送回前台(调用前台定义的方法) 函数名为sendMessage
          io.emit('sendMessage', data);
        });
        // 私发消息
        socket.on('sendmsg', function (data) {
          console.log(data);
          // 查询用户连接
          let target = arrAllSocket[data.userid]
          if (target) {
            //发送信息至指定的人
            target.emit('receivemsg', data)
          }
        })
      })
    }
    
    

    最后在www文件中使用如下代码,引入文件

    var io = require('../io')
    io(server)
    

    至此,聊天的基本功能实现。记录一下方便以后使用

    展开全文
  • 主要介绍了vue实现的微信机器人聊天功能,结合实例形式分析了基于vue.js的微信机器人聊天相关界面布局、ajax交互等操作技巧,并附带源码供读者下载参考,需要的朋友可以参考下
  • 最近学习了一下websocket的即时通信,感觉非常的强大,这里我用node启动了一个服务进行websocket链接,然后再vue的view里面进行了链接,进行通信,废话不多说,直接上代码吧
  • 主要介绍了Vue Cli 3项目 使用融云IM实现聊天功能,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
  • vue+ssh框架实现在线聊天功能 主要实现心理咨询师和用户的聊天,比较复杂的地方在于位置关系,左边还是右边,还有双方头像的位置
  • 预览 艾特开始 ...3.要实现艾特提示,输入框显示的是“@某某某”,实际发出去的消息,把@某某某转换为我们限定的某字符串,然后收到消息通过字符串匹配即可 下面,我们来一步步实现功能吧 ...
  • 基于vue,websocket实现在线聊天功能

    万次阅读 2018-04-15 18:15:05
    最近项目中一直在使用vue作为前端框架,可是用到的只有很少一部分的功能,特别是vuex,为了更加深入了解vue框架,在工作之余开发了一款基于vue的在线聊天工具,一下是部分功能效果图1.登录注册2.添加好友&amp;...
  • egg框架(通过egg-socket.io建立即时通讯服务,实现聊天功能) 1.安装插件 npm install socket.io-client npm install vue-socket.io-extended 其实一开始我是用vue-socket.io的,毕竟教程多,但是我装完后...
  • websocket的即时通信非常的强大,这里我用node启动了一个服务进行websocket链接,然后再vue的view里面进行了链接,进行通信,废话不多说,直接上代码吧, 首先,我需要用到node的nodejs-websocket模块 使用yarn...
  • Nodejs + WebSocket简单介绍及示例 - 第一章 Nodejs + WebSocket + Vue 实现多人聊天室WebIM功能 前后端实现多人聊天,写的太棒了,保存一下,可以下次在看
  • 前言在《Nodejs + WebSocket简单介绍及示例 - 第一章》中简单的介绍了,Nodejs + WebSocket的使用方法及作用,今天就用它来搭建一个简单的聊天功能。1、Nodejs+WebSocket创建后台服务器功能2、Vue视图层,接收后台...

空空如也

空空如也

1 2 3 4 5 ... 16
收藏数 315
精华内容 126
关键字:

vue实现聊天功能

vue 订阅