共计 7613 个字符,预计需要花费 20 分钟才能阅读完成。
本文由作者“大白菜”分享,有较多订正和改变。留神:本系列是给 IM 初学者的文章,IM 老油条们还望海涵,勿喷!
1、引言
前两篇《编码实际篇(单聊性能)》、《编码实际篇(群聊性能)》别离实现了控制台版本的 IM 单聊和群聊的性能。通过前两篇这两个小案例来体验的只是 Netty 在 IM 零碎这种实在的开发实际,但比照在实在的 Netty 利用开发当中,本系列的案例是十分的简略的,次要目标其实是让大家能够更好地理解其原理,从而写出更高质量的 Netty 代码。不过,尽管 Netty 的性能很高,然而也不能保障随便写进去的我的项目就是性能很高的,所以本篇将次要解说几个基于 Netty 的 IM 零碎的优化实战技术点。
学习交换:- 挪动端 IM 开发入门文章:《新手入门一篇就够:从零开发挪动端 IM》- 开源 IM 框架源码:https://github.com/JackJiang2…(备用地址点此)(本文同步公布于:http://www.52im.net/thread-39…)
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.Sharablepublic 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.Sharablepublic 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-39…)