vue实现在线聊天功能(基于websoket、netty)

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

登录页面

登陆页面(背景是自动播放的小视频)
vue实现在线聊天功能(基于websoket、netty)_第1张图片
通讯录
vue实现在线聊天功能(基于websoket、netty)_第2张图片

联系人搜索
vue实现在线聊天功能(基于websoket、netty)_第3张图片
聊天详情
vue实现在线聊天功能(基于websoket、netty)_第4张图片
前端主要代码
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连接

     
    
  2. 聊天详情页面的处理逻辑

     
    

后台实现(springboot+netty+mybatis)

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

主要代码
pom.xml依赖

    
        io.netty
        netty-all
        4.1.32.Final
    
    
    
        org.projectlombok
        lombok
        1.18.12
    
    
    
        net.sf.json-lib
        json-lib
        2.2.3
        jdk15
    

    
        org.mybatis.spring.boot
        mybatis-spring-boot-starter
        1.3.2
    

    
        mysql
        mysql-connector-java
        runtime
    

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 {

    @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 {

    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

你可能感兴趣的:(spring,boot,后端,spring,boot,websocket,vue)