基于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

目录
相关文章
|
2月前
|
数据采集 监控 机器人
浅谈网页端IM技术及相关测试方法实践(包括WebSocket性能测试)
最开始转转的客服系统体系如IM、工单以及机器人等都是使用第三方的产品。但第三方产品对于转转的业务,以及客服的效率等都产生了诸多限制,所以我们决定自研替换第三方系统。下面主要分享一下网页端IM技术及相关测试方法,我们先从了解IM系统和WebSocket开始。
72 4
|
1天前
|
自然语言处理 Prometheus 监控
基于DeepSeek的智能客服系统实战:从开发到部署
本文详细介绍如何将基于DeepSeek的智能客服系统从开发到部署,涵盖服务器选择、环境配置、代码部署及Web服务器设置。通过具体案例和代码示例,讲解系统上线步骤,并介绍使用Prometheus、Grafana等工具进行性能监控的方法。此外,针对高并发、API调用失败等常见问题提供解决方案,确保系统的稳定运行。最后强调数据安全与隐私保护的重要性,帮助读者全面掌握智能客服系统的部署与维护。
|
14天前
|
存储 SQL 监控
转转平台IM系统架构设计与实践(二):详细设计与实现
以转转IM架构为起点,介绍IM相关组件以及组件间的关系;以IM登陆和发消息的数据流转为跑道,介绍IM静态数据结构、登陆和发消息时的动态数据变化;以IM常见问题为风景,介绍保证IM实时性、可靠性、一致性的一般方案;以高可用、高并发为终点,介绍保证IM系统稳定及性能的小技巧。
28 6
|
1月前
|
存储 消息中间件 小程序
转转平台IM系统架构设计与实践(一):整体架构设计
本文描述了转转IM为整个平台提供的支撑能力,给出了系统的整体架构设计,分析了系统架构的特性。
78 10
|
6月前
|
数据采集 监控 测试技术
大型IM稳定性监测实践:手Q客户端性能防劣化系统的建设之路
本文以iOS端为例,详细分享了手 Q 客户端性能防劣化系统从0到1的构建之路,相信对业界和IM开发者们都有较高的借鉴意义。
203 3
|
2月前
|
存储 自然语言处理 关系型数据库
基于阿里云通义千问开发智能客服与问答系统
在企业的数字化转型过程中,智能客服系统已成为提高客户满意度和降低运营成本的重要手段。阿里云的通义千问作为一款强大的大语言模型,具有自然语言理解、对话生成、知识检索等能力,非常适合用来开发智能客服与问答系统。 通过本博客,我们将演示如何基于阿里云的通义千问模型,结合阿里云相关产品如函数计算(FC)、API网关、RDS等,搭建一个功能齐全的智能客服系统。
334 5
|
4月前
|
前端开发 JavaScript PHP
Thinkphp在线客服系统源码多语言外贸版_PHP客服系统源码Uniapp开发搭建+论文设计
Thinkphp在线客服系统源码多语言外贸版_PHP客服系统源码Uniapp开发搭建+论文设计
|
6月前
|
小程序 前端开发 Java
携程技术分享:亿级流量的办公IM及开放平台技术实践
本文总结了携程办公IM这些年的发展历程及未来的演进方向,并着重从高可用、高性能和可扩展的角度,探讨开放式平台的技术实现及发展方向。
116 0
携程技术分享:亿级流量的办公IM及开放平台技术实践
|
6月前
|
API 开发者
Netty运行原理问题之Netty实现低开发门槛的问题如何解决
Netty运行原理问题之Netty实现低开发门槛的问题如何解决
|
6月前
|
编解码 NoSQL Redis
(十一)Netty实战篇:基于Netty框架打造一款高性能的IM即时通讯程序
关于Netty网络框架的内容,前面已经讲了两个章节,但总归来说难以真正掌握,毕竟只是对其中一个个组件进行讲解,很难让诸位将其串起来形成一条线,所以本章中则会结合实战案例,对Netty进行更深层次的学习与掌握,实战案例也并不难,一个非常朴素的IM聊天程序。
147 3