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

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

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

1、引言

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

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

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

学习交流:

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK备用地址点此

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

2、写在前面

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

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

3、系列文章

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

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

目录
相关文章
|
1月前
|
网络协议 程序员 调度
即时通讯技术文集(第33期):IM开发综合技术合集(Part6) [共12篇]
为了更好地分类阅读 52im.net 总计1000多篇精编文章,我将在每周三推送新的一期技术文集,本次是第33 期。
50 0
|
1月前
|
移动开发 编解码 Java
Netty编码器和解码器
Netty从底层Java通道读到ByteBuf二进制数据,传入Netty通道的流水线,随后开始入站处理。在入站处理过程中,需要将ByteBuf二进制类型解码成Java POJO对象。这个解码过程可以通过Netty的Decoder解码器去完成。在出站处理过程中,业务处理后的结果需要从某个Java POJO对象编码为最终的ByteBuf二进制数据,然后通过底层 Java通道发送到对端。在编码过程中,需要用到Netty的Encoder编码器去完成数据的编码工作。
|
1月前
|
Java Unix Linux
【Netty技术专题】「原理分析系列」Netty强大特性之Native transports扩展开发实战
当涉及到网络通信和高性能的Java应用程序时,Netty是一个强大的框架。它提供了许多功能和组件,其中之一是JNI传输。JNI传输是Netty的一个特性,它为特定平台提供了高效的网络传输。 在本文中,我们将深入探讨Netty提供的特定平台的JNI传输功能,分析其优势和适用场景。我们将介绍每个特定平台的JNI传输,并讨论其性能、可靠性和可扩展性。通过了解这些特定平台的JNI传输,您将能够更好地选择和配置适合您应用程序需求的网络传输方式,以实现最佳的性能和可靠性。
83 7
【Netty技术专题】「原理分析系列」Netty强大特性之Native transports扩展开发实战
|
12天前
|
资源调度 JavaScript 前端开发
IM跨平台技术学习(十一):环信基于Electron打包Web IM桌面端的技术实践
这次借着论证 Web IM端 SDK 是否可以在 Electron 生成的桌面端正常稳定使用,我决定把官方新推出的 webim-vue3-demo,打包到桌面端,并记录了这次验证的过程以及所遇到的问题和解决方法。
23 2
|
1月前
|
移动开发 网络协议 Java
即时通讯技术文集(第38期):IM代码入门实践(Part2) [共15篇]
为了更好地分类阅读 52im.net 总计1000多篇精编文章,我将在每周三推送新的一期技术文集,本次是第38 期。
46 1
|
1月前
|
安全 Java 数据库
即时通讯技术文集(第37期):IM代码入门实践(Part1) [共16篇]
为了更好地分类阅读 52im.net 总计1000多篇精编文章,我将在每周三推送新的一期技术文集,本次是第37 期。
35 2
|
1月前
|
监控 Devops Java
大型IM工程重构实践:企业微信Android端的重构之路
本文将探讨我们在大型IM工程实践中采用的一些行之有效的重构方法和实例,以及如何让一个大型软件系统持续保持活力。
57 0
|
1月前
|
Rust 监控 JavaScript
抖音技术分享:飞鸽IM桌面端基于Rust语言进行重构的技术选型和实践总结
本文将介绍飞鸽IM前端团队如何结合Rust对飞鸽客户端接待能力进行的技术提升,一步步从概念验证、路径分解到分工开发,再到最后上线收益论证,并分享了其中遇到的技术挑战与经验总结等。
82 1
|
1月前
|
存储 NoSQL Redis
陌陌技术分享:陌陌IM在后端KV缓存架构上的技术实践
在本文中,陌陌数据库负责人冀浩东将聚焦探讨陌陌的 KV 系统架构选型思路,深入解析如何进行此类系统的甄选决策,同时进一步分享陌陌团队在采用 OceanBase(OBKV)过程中所经历的探索与实践经验。
57 0
|
1月前
|
编解码 负载均衡 网络协议
即时通讯技术文集(第29期):IM开发技术合集(Part2) [共18篇]
为了更好地分类阅读 52im.net 总计1000多篇精编文章,我将在每周三推送新的一期技术文集,本次是第29 期。
53 1

热门文章

最新文章