基于Netty,从零开发IM(四):编码实践篇(系统优化)

本文由作者“大白菜”分享,有较多修订和改动。注意:本系列是给IM初学者的文章,IM老油条们还望海涵,勿喷!

1、引言

前两篇《编码实践篇(单聊功能)》、《编码实践篇(群聊功能)》分别实现了控制台版本的IM单聊和群聊的功能。

通过前两篇这两个小案例来体验的只是Netty在IM系统这种真实的开发实践,但对比在真实的Netty应用开发当中,本系列的案例是非常的简单的,主要目的其实是让大家可以更好地了解其原理,从而写出更高质量的 Netty 代码。

不过,虽然 Netty 的性能很高,但是也不能保证随意写出来的项目就是性能很高的,所以本篇将主要讲解几个基于Netty的IM系统的优化实战技术点。

(本文同步发布于:http://www.52im.net/thread-3988-1-1.html

2、写在前面

建议你在阅读本文之前,务必先读本系列的前三篇《IM系统设计篇》、《编码实践篇(单聊功能)》、《编码实践篇(群聊功能)》。

最后,在开始本文之前,请您务必提前了解Netty的相关基础知识,可从本系列首篇《IM系统设计篇》中的“知识准备”一章开始。

3、系列文章

本文是系列文章的第3篇,以下是系列目录:

基于Netty,从零开发IM(一):IM系统设计篇

基于Netty,从零开发IM(二):编码实践篇(单聊功能)

基于Netty,从零开发IM(三):编码实践篇(群聊功能)

基于Netty,从零开发IM(四):编码实践篇(系统优化)》(* 本文

4、基于Netty的IM系统常见优化方向

常见优化方向脑图:

我们逐条详细解释一下这些优化的目的:

1)心跳检测:主要是避免连接假死现象;

2)连接断开:则删除通道绑定属性、删除对应的映射关系,这些信息都是保存在内存当中的,如果不删除则造成资源浪费;

3)性能问题:用户 ID 和 Channel 的关系绑定存在内存当中,比如:Map,key 是用户 ID,value 是 Channel,如果用户量多的情况(客户端数量过多),那么服务端的内存将被消耗殆尽;

4)性能问题:每次服务端往客户端推送消息,都需从Map里查找到对应的Channel,如果数量较大和查询频繁的情况下如何保证查询性能;

5)安全问题:HashMap 是线程不安全的,并发情况下,我们如何去保证线程安全;

6)身份校验:如何 LoginHandler 是负责登录认证的业务 Handler,AuthHandler 是负责每次请求时校验该请求是否已经认证了,这些 Handler 在链接就绪时已经被添加到 Pipeline 管道当中,其实,我们可以采用热插拔的方式去把一些在做业务操作时用不到的 Handler 给剔除掉。

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

5、本篇优化方向

本篇主要的优化内容主要是在第二篇单聊功能第三篇群聊功能的基础上继续完善几点。

具体的优化方向如下:

1)无论客户端还是服务端都分别只有一个 Handler,这样的话,业务越来越多,Handler 里面的代码就会越来越臃肿,我们应该想办法把 Handler 拆分成各个独立的 Handler;

2)如果拆分的 Handler 很多,每次有连接进来,那么都会触发 initChannel () 方法,所有的 Handler 都得被 new 一遍,我们应该把这些 Handler 改成单例模式(不需要每次都 new,提高效率);

3)发送消息时,无论是单聊还是群聊,对方不在线,则把消息缓存起来,等待其上线再推送给他;

4)连接断开时,无论是主动和被动,需要删除 Channel 属性、删除用户和 Channel 映射关系。

6、业务拆分以及单例模式优化

6.1 概述

主要优化细节如下:

1)自定义 Handler 继承 SimpleChannelInboundHandler,那么解码的时候,会自动根据数据格式类型转到相应的 Handler 去处理;

2)@Shareable 修饰 Handler,保证 Handler 是可共享的,避免每次都创建一个实例。

6.2 登录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 {


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

    }

}

6.3 消息发送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 {


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

    }

}

6.4 initChannel方法优化

.handler(newChannelInitializer<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());

    }

});

6.5 小结

这种业务拆分以及单例模式优优化是Netty开发当中很常用的,可以更好的维护基于Netty的代码并提高应用性能。

7、数据缓存优化

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

7.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);

    }

}

7.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);

    }

}

8、连接断开事件处理优化

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

8.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();

        }

    }

}

8.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();

        }

    }

}

9、本篇小结

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

限于篇幅,本系列文章文章没办法真正讲解开发一个完整IM系统所涉及的方方面面,如果有兴趣,可以继续阅读更有针对性的IM开发文章,比如IM架构设计IM通信协议IM通信安全群聊优化弱网优化网络保活等。

10、参考资料

[1] 新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析

[2] 理论联系实际:一套典型的IM通信协议设计详解

[3] 浅谈IM系统的架构设计

[4] 简述移动端IM开发的那些坑:架构设计、通信协议和客户端

[5] 一套海量在线用户的移动端IM架构设计实践分享(含详细图文)

[6] 一套原创分布式即时通讯(IM)系统理论架构方案

[7]  一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践

[8] 一套亿级用户的IM架构技术干货(上篇):整体架构、服务拆分等

[9] 从新手到专家:如何设计一套亿级消息量的分布式IM系统

[10] 基于实践:一套百万消息量小规模IM系统技术要点总结

[11] 探探的IM长连接技术实践:技术选型、架构设计、性能优化

[12] 拿起键盘就是干,教你徒手开发一套分布式IM系统

[13] 万字长文,手把手教你用Netty打造IM聊天

[14] 基于Netty实现一套分布式IM系统

[15] SpringBoot集成开源IM框架MobileIMSDK,实现即时通讯IM聊天功能

(本文同步发布于:http://www.52im.net/thread-3988-1-1.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容