IM系统(代码实现+设计思路)
设计思路
项目名称:IM系统
项目功能:实现一个类似微信聊天功能web网站
技术栈:
- 后端:netty、springboot、MySQL、mybatis-Plus
- 前端:websocket、webRTC、vue.js、element-ui
MVC架构(基于Springmvc实现)
M — model — 数据管理模块
基于MySQL数据库进行数据管理
V — controller — 服务控制管理模块(业务逻辑模块)
搭建http服务器针对不同的请求提供不同的服务(博客页面的获取以及博客数据的增删查改)
C — 前端界面模块 基于html+css+js实现前端界面的展示
代码实现
一、数据库表实现
1.MySQL数据库
注意事项 :
- MySQL数据库对语句中大小写不敏感
- 库名, 表名, 表中字段不能使用关键字
- 每条语句最后都要以英文分号结尾
2.数据库各类表设计
3.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();
}
}
}
}