2021-03-30

IM系统(代码实现+设计思路)

设计思路

项目名称:IM系统

项目功能:实现一个类似微信聊天功能web网站

技术栈

  1. 后端:netty、springboot、MySQL、mybatis-Plus
  2. 前端:websocket、webRTC、vue.js、element-ui

MVC架构(基于Springmvc实现)

M — model — 数据管理模块
基于MySQL数据库进行数据管理

V — controller — 服务控制管理模块(业务逻辑模块)
搭建http服务器针对不同的请求提供不同的服务(博客页面的获取以及博客数据的增删查改)

C — 前端界面模块 基于html+css+js实现前端界面的展示

2021-03-30

代码实现

一、数据库表实现

1.MySQL数据库

注意事项 :

  1. MySQL数据库对语句中大小写不敏感
  2. 库名, 表名, 表中字段不能使用关键字
  3. 每条语句最后都要以英文分号结尾

2.数据库各类表设计

2021-03-30
2021-03-30
2021-03-30

3.SQL文件

sql文件

二、业务逻辑实现

1.单点登录功能(JWT+拦截器+redis)

代码实现

package com.xky.imchat.util;
//JWT工具类
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.xky.imchat.entity.vo.Admin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;
import java.util.HashMap;
import java.util.Stack;

public class JwtUtil {

    //设置过期时间
    private static final long EXPIRE_TIME = 15*60*1000;
    private static Logger logger = LoggerFactory.getLogger(JwtUtil.class);

    //token私钥,尽量还是不统一为好
    private static  final  String TOKEN_SECRET = "8ae0d24822ef59d9e75745449b3501bc";
//    生成签名,有效时间为15分钟
    public static  String sign(String username,String userId){
        //设置过期时间
        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        //私钥加密算法
        Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
        //设置头信息
        HashMap<String,Object> header= new HashMap<>(2);
        header.put("type","jwt");
        header.put("alg","HS256");
        return JWT.create()
                .withAudience(userId)
                .withClaim("username",username)
                .withClaim("userId",userId)
                .withExpiresAt(date)
                .sign(algorithm);
    }

//校验token是否正确
    public static  boolean verity(String token){
        try{
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            JWTVerifier build = JWT.require(algorithm).build();
            DecodedJWT verify = build.verify(token);
            return true;
        }catch (Exception e){
            return  false;
        }
    }

    //获取签发对象
    public static  String getAudience(String token){
        String audience = null;
        try{
            audience = JWT.decode(token).getAudience().get(0);
        }catch (Exception e){

        }
        return  audience;
    }

    //获取载荷的内容
    public static Claim getClaimByName(String token,String name){
        return  JWT.decode(token).getClaim(name);
    }

    //提取相应的内容
    public static Admin getContent(String token){
      try{
          Algorithm algorithm=Algorithm.HMAC256(TOKEN_SECRET);
          JWTVerifier verifier=JWT.require(algorithm).build();
          DecodedJWT jwt=verifier.verify(token);
         Admin a = new Admin();
         a.setUserID(jwt.getClaim("userId").asString());
         a.setUserName(jwt.getClaim("username").asString());
          System.out.println(a);
         return a;
      }catch (Exception e){
          e.printStackTrace();
          logger.info("token出错了");
      }
      return null;
    }
//刷新token对象
    public static  String refresh(String username,String userId){
        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME*2);
        //私钥加密算法
        Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
        //设置头信息
        HashMap<String,Object> header= new HashMap<>(2);
        header.put("type","jwt");
        header.put("alg","HS256");
        return JWT.create()
                .withAudience(userId)
                .withClaim("username",username)
                .withClaim("userId",userId)
                .withExpiresAt(date)
                .sign(algorithm);
    }
    public static String refresh(String token){
        Admin content = JwtUtil.getContent(token);
        String refresh = JwtUtil.sign(content.getUserName(), content.getUserID());
        return refresh;
    }
}

后端拦截器实现(带令牌刷新功能及异端登录挤掉功能)

import com.xky.imchat.annotation.PassToken;
import com.xky.imchat.entity.vo.Admin;
import com.xky.imchat.util.JwtUtil;
import com.xky.imchat.util.RedisUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
//后端拦截器实现
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

public class JwtInterceptor implements HandlerInterceptor {
    Logger logger  = LoggerFactory.getLogger(JwtInterceptor.class);
   private  RedisUtil redisUtil;
   public JwtInterceptor(RedisUtil redisUtil){
       this.redisUtil = redisUtil;
   }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //从请求头里面获取token
        String token = request.getHeader("Authorization");
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod)handler;
        Method method = handlerMethod.getMethod();
        //检查是否有passtoken注解
        if(method.isAnnotationPresent(PassToken.class)){
            PassToken annotation = method.getAnnotation(PassToken.class);
            if(annotation.required()){
                return  true;
            }
        }else{
            logger.info("要进行token检验");
            if(token==null){
                logger.info("token无效,需要重新登录");
                return false;
            }

            if(!JwtUtil.verity(token)){

                if(redisUtil.hasKey(token)){
                    response.setStatus(201);
                    String new_token =(String) redisUtil.get(token);
                    //移除原来的缓存
                    redisUtil.delete(token);
                    logger.info(new_token);
                    String refresh = JwtUtil.refresh(new_token);
                    response.setHeader("new_token",refresh);
                    //更新缓存
                    logger.info("开始更新缓存");
                    Admin content = JwtUtil.getContent(new_token);
                    redisUtil.set(content.getUserName(),refresh,900);
                    String access_token = JwtUtil.refresh(content.getUserName(), content.getUserID());
                    System.out.println(redisUtil.hasKey(content.getUserName()));
                    redisUtil.set(refresh,access_token,1800);
                    return true;
                }else{
                    response.setStatus(202);
                    return false;
                }
            }else{
                //token还在有效期,但是其他客户端登录了要退出登录
                Admin content = JwtUtil.getContent(token);

              String new_token = (String)redisUtil.get(content.getUserName());
                System.out.println("开始排挤对方");
                logger.info(token);
                logger.info(new_token);
                System.out.println(new_token.equals(token));
              if(!new_token.equals(token)){
                  logger.info("开始挤掉对方");
                  response.setStatus(203);
                  return false;
              }

            }
        }
        return JwtUtil.verity(token);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

前端拦截器实现

axios.interceptors.request.use(
    config => {
    	//请求成功时,对请求作拦截处理
    	if (localStorage.getItem('Authorization')) {
      		config.headers.Authorization = localStorage.getItem('Authorization');
    		console.log("拦截请求,添加token")
    	}else{
    		console.log('拦截请求,没有token')
    	}
        return config;
    },
    error => {//请求失败时
        console.log(error);
        return Promise.reject();
    }
);

axios.interceptors.response.use(
    response => {
        if (response.data.code ===200 ) {
			console.log(response)
           return response.data;
		   }else{
				console.log(response)
			   if(response.status==202){
				     localStorage.removeItem('Authorization');
					 //直接跳转到登录页面
				  router.push({
				        path: "/login"
				      });
			   }else if(response.status==201){
				   localStorage.setItem('Authorization',response.headers.new_token)
			   }else if(response.status==203){
				 
					router.push({
					      path: "/login",
						  params:{
						        id:203
						      }
					    });
			   }
			   return response
		   }
		   
     
    },
    error => {
        // console.log(error);
        return Promise.reject();
    }
);

2.集成阿里云oss实现文件上传功能

代码实现

axios.interceptors.request.use(
    config => {
    	//请求成功时,对请求作拦截处理
    	if (localStorage.getItem('Authorization')) {
      		config.headers.Authorization = localStorage.getItem('Authorization');
    		console.log("拦截请求,添加token")
    	}else{
    		console.log('拦截请求,没有token')
    	}
        return config;
    },
    error => {//请求失败时
        console.log(error);
        return Promise.reject();
    }
);

axios.interceptors.response.use(
    response => {
        if (response.data.code ===200 ) {
			console.log(response)
           return response.data;
		   }else{
				console.log(response)
			   if(response.status==202){
				     localStorage.removeItem('Authorization');
					 //直接跳转到登录页面
				  router.push({
				        path: "/login"
				      });
			   }else if(response.status==201){
				   localStorage.setItem('Authorization',response.headers.new_token)
			   }else if(response.status==203){
				 
					router.push({
					      path: "/login",
						  params:{
						        id:203
						      }
					    });
			   }
			   return response
		   }
		   
     
    },
    error => {
        // console.log(error);
        return Promise.reject();
    }
);

2.基于netty+websocket网络通信功能

主要逻辑处理代码实现

@ChannelHandler.Sharable
@Component
@Slf4j
public class ImHandler extends SimpleChannelInboundHandler<Object> {
    //方便群组
   public  static ChannelGroup group = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    //本类静态对象
   private static ImHandler handler;
   @PostConstruct
   public  void init(){
       handler=this;
   }
    @Autowired
    UserMessageService usermessageservice;

    @Autowired
    GroupNumberService groupNumberService;

    @Autowired
    GroupMessageService groupMessageService;
    
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        log.info("有用户连接");
        group.add(ctx.channel());
    }
//检测通道是否活跃
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
    }

    //主要逻辑处理
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {

        //文本消息
        if(msg instanceof TextWebSocketFrame ){
            String message = ((TextWebSocketFrame) msg).text();
            log.info(message);
            ChatVo chat = null;
            try{
                JSONObject Msg;
                //将消息转换成json
                Msg = JSONObject.parseObject(message);
                chat = JSON.toJavaObject(Msg, ChatVo.class);
                System.out.println(chat);

                //-1代表用户刚刚连接,0代表私聊消息,1代表群聊消息
                switch (chat.getType()){
                    case -1 :
                        UserChannelUtil.put(chat.getUserId(),ctx.channel());
                        //将离线之后的聊天消息发给
                        log.info(chat.getUserId()+"登录了");
                        break;
                    case 0 :
                        //看好友是否在线
                        boolean contain = UserChannelUtil.isContain(chat.getFriendId());
                        System.out.println(contain);
                        if(contain){
                            Channel channel = UserChannelUtil.getChannel(chat.getFriendId());
                            String string = JSON.toJSONString(chat);
                            channel.writeAndFlush(new TextWebSocketFrame(string));
                        }else{
                            //好友不在线,将消息保存到数据库,格式化时间
                            System.out.println(1111);
                            UserMessage uMsg = new UserMessage();
                            uMsg.setUserId(chat.getUserId());
                            uMsg.setContent(chat.getContent());
                            uMsg.setFriendId(chat.getFriendId());
                            uMsg.setType(chat.getType());
                            handler.usermessageservice.add(uMsg);
                            log.info("好友不在线插入数据库");
                        }
                        break;
                    case 1 :
                    //群聊消息

                        List<GroupNumber> list = handler.groupNumberService.getAll(chat.getUserId(),chat.getFriendId());//根据该群聊的所有用户
                        System.out.println(list);
                        //转发给除发送者之外的所有用户
                        for(GroupNumber g :list){
                            boolean b = UserChannelUtil.isContain(g.getNumberId());
                            if(b){
                                //用户在线,转发消息
                                log.info("成功转发消息");
                                Channel c = UserChannelUtil.getChannel(g.getNumberId());
                                String s = JSON.toJSONString(chat);
                                System.out.println(s);
                                c.writeAndFlush(new TextWebSocketFrame(s));
                            }else {
                                //消息保存到数据库,用户上线就推送
                                GroupMessage gMsg = new GroupMessage();
                                gMsg.setContent(chat.getContent());
                                gMsg.setGroupId(chat.getFriendId());
                                gMsg.setUserId(g.getNumberId());
                                System.out.println(gMsg);
                                handler.groupMessageService.save(gMsg);
                                log.info("插入数据库成功");
                            }
                        }
                        break;
                    case 2:
                        //视频语音通话
                        //看好友是否在线 video-offer
                       Boolean contains =  UserChannelUtil.isContain(chat.getFriendId());
                       //好友存在则发送video-offer,不在线则告诉用户好友不在线
                       if(contains){
                           Channel channel = UserChannelUtil.getChannel(chat.getFriendId());
                           String string = JSON.toJSONString(chat);
                           channel.writeAndFlush(new TextWebSocketFrame(string));
                       }else{
                           Channel channel = UserChannelUtil.getChannel(chat.getUserId());
                           channel.writeAndFlush(new TextWebSocketFrame("用户不在线"));
                       }
                       break;
                    default:
                        //video_answer
                        Boolean contain3 =  UserChannelUtil.isContain(chat.getFriendId());
                        //好友存在则发送video-offer,不在线则告诉用户好友不在线
                        if(contain3){
                            Channel channel = UserChannelUtil.getChannel(chat.getFriendId());
                            String string = JSON.toJSONString(chat);
                            channel.writeAndFlush(new TextWebSocketFrame(string));
                        }
                        break;
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        //二进制消息
        if(msg instanceof BinaryWebSocketFrame){
            BinaryWebSocketFrame binaryWebSocketFrame = new BinaryWebSocketFrame(Unpooled.buffer().writeBytes("现在还没有实现这个功能".getBytes()));
            ctx.channel().writeAndFlush(binaryWebSocketFrame);
        }
        //ping消息
        if (msg instanceof PongWebSocketFrame){
            log.info("ping成功");
        }
        //关闭消息
        if(msg instanceof CloseWebSocketFrame){
            log.info("客户端关闭");
            ctx.channel().close();
        }
    }

    //异常处理
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        group.remove(ctx.channel());
    }
//心跳检测
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
            if(evt instanceof IdleStateEvent){
                IdleStateEvent event = (IdleStateEvent)evt;
                String type=null;
                switch (event.state()){
                    case READER_IDLE:
                        type="读空闲";
                        break;
                    case WRITER_IDLE:
                        type="写空闲";
                        break;
                    case ALL_IDLE:
                        type="读写空闲";
                }
                if(type.equals("读写空闲")){
                    ctx.channel().close();
                }
            }
    }
}

3.基于WebRTC的视频通话功能

4.个人聊天+群组聊天功能实现

5.好友添加+群组添加功能实现

6.基于webRTC的实时文件功能实现(待续)

三、前端页面实现

1.基于vue.js实现页面的功能

2.基于element-ui实现页面的美观

3.前端页面实现模块

四、代码地址

前端代码

前端代码

后端代码

后端代码

上一篇:php – 在网页和android之间实现聊天


下一篇:SpringBoot + WebSocket实现简易聊天室