章节索引 :

IM - 优化

1. 前言

前面两个章节分别实现控制台版本的单聊和群聊的功能实现,通过这两个小案例来体验 Netty 的真实开发场景,其实在真实 Netty 开发当中,是非常的简单的,但是前面花那么多的篇幅去讲解 Netty 的核心组件和理论,主要目的是让大家可以了解其原理,才能写出高质量的 Netty 代码。虽然 Netty 的性能很高,但是不代表作出来的项目是性能很高的,本节主要讲解如何去优化 Netty。

2. 常见优化方案

2.1 优化架构图

图片描述

2.2 优化细节说明

常见的细节优化

  1. 心跳检测,主要是避免连接假死现象;
  2. 连接断开,则删除通道绑定属性、删除对应的映射关系,这些信息都是保存在内存当中的,如果不删除则造成资源浪费;
  3. 性能问题,用户 ID 和 Channel 的关系绑定存在内存当中,比如:Map<Integer,Channel>,key 是用户 ID,value 是 Channel,如果用户量多的情况(客户端数量过多),那么服务端的内存将被消耗殆尽;
  4. 性能问题,每次服务端往客户端推送消息,都需要从 Map 里面查找到对应的 Channel,如果数量比较大和查询频繁的情况下如何保证查询性能;
  5. 安全性问题,HashMap 是线程不安全的,并发情况下,我们如何去保证线程安全;
  6. 身份校验,如何 LoginHandler 是负责登录认证的业务 Handler,AuthHandler 是负责每次请求时校验该请求是否已经认证了,这些 Handler 在链接就绪时已经被添加到 Pipeline 管道当中,其实,我们可以采用热插拔的方式去把一些在做业务操作时用不到的 Handler 给剔除掉。

以上是开发当中,需要去注意的点,当然还有很多其他的细节,比如:线程池这块,需要大家慢慢去从实战中积累。

本节主要的内容主要是在单聊和群聊基础上完善几点内容,具体如下:

  1. 无论客户端还是服务端都分别只有一个 Handler,这样的话,业务越来越多,Handler 里面的代码就会越来越臃肿,我们应该想办法把 Handler 拆分成各个独立的 Handler;
  2. 如何拆分的 Handler 很多,每次有连接进来,那么都会触发 initChannel () 方法,所有的 Handler 都得被 new 一遍,我们应该把这些 Handler 改成单例模式。 不需要每次都 new,提高效率;
  3. 发送消息,无论是单聊还是群聊,对方不在线,则把消息缓存起来,等待其上线再推送给他;
  4. 连接断开,无论是主动和被动,需要删除 Channel 属性、删除用户和 Channel 映射关系。

3. 业务拆分以及单例模式

主要优化细节如下:

  1. 自定义 Handler 继承 SimpleChannelInboundHandler,那么解码的时候,会自动根据数据格式类型转到相应的 Handler 去处理;
  2. @Shareable 修饰 Handler,保证 Handler 是可共享的,避免每次都创建一个实例。

3.1 登录 Handler

@ChannelHandler.Sharable
public class ClientLogin2Handler extends SimpleChannelInboundHandler<LoginResBean> {
    //1.构造函数私有化,避免创建实体
   	private ClientLogin2Handler(){}
    //2.定义一个静态全局变量
    public static ClientLogin2Handler instance=null;
    //3.获取实体方法
    public static ClientLogin2Handler getInstance(){
        if(instance==null){
            synchronized (ClientLogin2Handler.class){
                if(instance==null){
                    instance=new ClientLogin2Handler();
                }
            }
        }
        return instance;
    }
    
    protected void channelRead0(
        ChannelHandlerContext channelHandlerContext, 
        LoginResBean loginResBean) throws Exception {
        
        //具体业务代码,参考之前
    }
}

3.2 消息发送 Handler

@ChannelHandler.Sharable
public class ClientMsgHandler extends SimpleChannelInboundHandler<MsgResBean> {
    //1.构造函数私有化,避免创建实体
   	private ClientMsgHandler(){}
    //2.定义一个静态全局变量
    public static ClientMsgHandler instance=null;
    //3.获取实体方法
    public static ClientMsgHandler getInstance(){
        if(instance==null){
            synchronized (ClientMsgHandler.class){
                if(instance==null){
                    instance=new ClientMsgHandler();
                }
            }
        }
        return instance;
    }
    
    protected void channelRead0(
        ChannelHandlerContext channelHandlerContext, 
        MsgResBean msgResBean) throws Exception {

        //具体业务代码,参考之前
    }
}

3.3 initChannel 方法

.handler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) {
        //1.拆包器
        ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,5,4));
		//2.解码器
        ch.pipeline().addLast(new MyDecoder());
        //3.登录Handler,使用单例获取
        ch.pipeline().addLast(ClientLogin2Handler.getInstance());
        //4.消息发送Handler,使用单例获取
        ch.pipeline().addLast(ClientMsgHandler.getInstance());
        //5.编码器
        ch.pipeline().addLast(new MyEncoder());
    }
});

总结,这种模式是开发当中常用的,可以更好的维护代码以及提高应用性能。

4. 数据缓存

为了提高用户体验,在发送消息(推送消息)时,如果接收方不在线,则应该把消息缓存起来,等对方上线时,再推送给他。

4.1 数据缓存到集合

//1.定义一个集合存放数据(真实项目可以存放数据库或者redis缓存),这样数据比较安全。
private List<Map<Integer,String>> datas=new ArrayList<Map<Integer,String>>();

//2.服务端推送消息
private void pushMsg(MsgReqBean bean,Channel channel){
    Integer touserid=bean.getTouserid();
    Channel c=map.get(touserid);

    if(c==null){//对方不在线
        //2.1存放到list集合
        Map<Integer,String> data=new HashMap<Integer, String>();
        data.put(touserid,bean.getMsg());
		datas.add(data);
        
        //2.2.给消息“发送人”响应
        MsgResBean res=new MsgResBean();
        res.setStatus(1);
        res.setMsg(touserid+">>>不在线");
        channel.writeAndFlush(res);
        
    }else{//对方在线
        //2.3.给消息“发送人”响应
        MsgResBean res=new MsgResBean();
        res.setStatus(0);
        res.setMsg("发送成功);
        channel.writeAndFlush(res);
        
        //2.4.给接收人推送消息
        MsgRecBean res=new MsgRecBean();
        res.setFromuserid(bean.getFromuserid());
        res.setMsg(bean.getMsg());
        c.writeAndFlush(res);
    }
}

4.2 上线推送

private void login(LoginReqBean bean, Channel channel){
    Channel c=map.get(bean.getUserid());
    LoginResBean res=new LoginResBean();
    if(c==null){
        //1.添加到map
        map.put(bean.getUserid(),channel);
        //2.给通道赋值
        channel.attr(AttributeKey.valueOf("userid")).set(bean.getUserid());
        //3.登录响应
        res.setStatus(0);
        res.setMsg("登录成功");
        res.setUserid(bean.getUserid());
        channel.writeAndFlush(res);
        
        //4.根据user查找是否有尚未推送消息
		//思路:根据userid去lists查找.......
        
    }else{
        res.setStatus(1);
        res.setMsg("该账户目前在线");
        channel.writeAndFlush(res);
    }
}

5. 连接断开事件处理

如果客户端网络故障导致连接断开了(非主动下线),那么服务端就会监听到连接的断开,此时应该删除对应的 map 映射关系,否则影响客户端的下次同一个账号登录,以及大量的客户端掉线,但是映射关系没有删除掉,导致服务器资源没有得到释放。

5.1 正确写法

实例:

public class ServerChatGroupHandler extends ChannelInboundHandlerAdapter {
    //映射关系
    private static Map<Integer, Channel> map=new HashMap<Integer, Channel>();
    
    //连接断开,触发该事件
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //1.获取Channel
        Channel channel=ctx.channel();

        //2.从map里面,根据Channel找到对应的userid
        Integer userid=null;
        for(Map.Entry<Integer, Channel> entry : map.entrySet()){
            Integer uid=entry.getKey();
            Channel c=entry.getValue();
            if(c==channel){
                userid=uid;
            }
        }
		//3.如果userid不为空,则需要做以下处理
        if(userid!=null){
            //3.1.删除映射
            map.remove(userid);
            //3.2.移除标识
            ctx.channel().attr(AttributeKey.valueOf("userid")).remove();
        }
    }
}

5.2 错误写法

Channel 断开,服务端监听到连接断开事件,但是此时 Channel 所绑定的属性已经被移除掉了,因此这里无法直接获取的到 userid。

实例:

public class ServerChatGroupHandler extends ChannelInboundHandlerAdapter {
    //映射关系
    private static Map<Integer, Channel> map=new HashMap<Integer, Channel>();
    
    //连接断开,触发该事件
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //1.获取Channel绑定的userid
        Object userid=channel.attr(AttributeKey.valueOf("userid")).get();

        //2.如果userid不为空
        if(userid!=null){
            //1.删除映射
            map.remove(userid);
            //2.移除标识
            ctx.channel().attr(AttributeKey.valueOf("userid")).remove();
        }
    }
}

6. 小结

本节内容还是相对容易理解的,主要是优化前面实现的聊天功能,主要优化是业务 Handler 的拆分以及使用单例模式、接受人不在线则缓存数据,等其上线再推送、监听连接断开删除对应的映射关系。