基于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篇,以下是系列目录:
- 《基于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高性能原理和框架架构解析
[3] 浅谈IM系统的架构设计
[4] 简述移动端IM开发的那些坑:架构设计、通信协议和客户端
[5] 一套海量在线用户的移动端IM架构设计实践分享(含详细图文)
[7] 一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践
[8] 一套亿级用户的IM架构技术干货(上篇):整体架构、服务拆分等
[9] 从新手到专家:如何设计一套亿级消息量的分布式IM系统
[10] 基于实践:一套百万消息量小规模IM系统技术要点总结
[11] 探探的IM长连接技术实践:技术选型、架构设计、性能优化
[14] 基于Netty实现一套分布式IM系统
[15] SpringBoot集成开源IM框架MobileIMSDK,实现即时通讯IM聊天功能
(本文同步发布于:http://www.52im.net/thread-3988-1-1.html)
基于Netty,从零开发IM(四):编码实践篇(系统优化)的更多相关文章
- 【Qt编程】基于Qt的词典开发系列<四>--无边框窗口的缩放与拖动
在现在,绝大多数软件都向着简洁,时尚发展.就拿有道的单词本和我做的单词本来说,绝大多数用户肯定喜欢我所做的单词本(就单单界面,关于颜色搭配和布局问题,大家就不要在意了). 有道的单词本: 我所做的单词 ...
- C#/ASP.NET MVC微信公众号接口开发之从零开发(四) 微信自定义菜单(附源码)
C#/ASP.NET MVC微信接口开发文章目录: 1.C#/ASP.NET MVC微信公众号接口开发之从零开发(一) 接入微信公众平台 2.C#/ASP.NET MVC微信公众号接口开发之从零开发( ...
- 适合新手:从零开发一个IM服务端(基于Netty,有完整源码)
本文由“yuanrw”分享,博客:juejin.im/user/5cefab8451882510eb758606,收录时内容有改动和修订. 0.引言 站长提示:本文适合IM新手阅读,但最好有一定的网络 ...
- 基于Netty的私有协议栈的开发
基于Netty的私有协议栈的开发 书是人类进步的阶梯,每读一本书都使自己得以提升,以前看书都是看了就看了,当时感觉受益匪浅,时间一长就又还回到书本了!所以说,好记性不如烂笔头,以后每次看完一本书都写一 ...
- 基于GBT28181:SIP协议组件开发-----------第四篇SIP注册流程eXosip2实现(一)
原创文章,引用请保证原文完整性,尊重作者劳动,原文地址http://www.cnblogs.com/qq1269122125/p/3945294.html. 上章节讲解了利用自主开发的组件SIP组件l ...
- 一款基于Netty开发的WebSocket服务器
代码地址如下:http://www.demodashi.com/demo/13577.html 一款基于Netty开发的WebSocket服务器 这是一款基于Netty框架开发的服务端,通信协议为We ...
- 如何基于IM即时通讯SDK从零开发仿微信聊天交友功能
IM即时通讯技术的发展 IM即时通讯(Instant Messaging)是一种基于互联网的即时交流消息的业务. 实时聊天交互功能是市面上主流APP的重要功能之一,人们所熟悉的就是微信,QQ的聊天消息 ...
- Android 基于Netty的消息推送方案之对象的传递(四)
在上一篇文章中<Android 基于Netty的消息推送方案之字符串的接收和发送(三)>我们介绍了Netty的字符串传递,我们知道了Netty的消息传递都是基于流,通过ChannelBuf ...
- 带你从零学ReactNative开发跨平台App开发(四)
ReactNative跨平台开发系列教程: 带你从零学ReactNative开发跨平台App开发(一) 带你从零学ReactNative开发跨平台App开发(二) 带你从零学ReactNative开发 ...
- 转: 基于netty+ protobuf +spring + hibernate + jgroups开发的游戏服务端
from: http://ybak.iteye.com/blog/1853335 基于netty+ protobuf +spring + hibernate + jgroups开发的游戏服务端 游戏服 ...
随机推荐
- Git项目提交规范结合Husky + commitlint使用
一.前置条件 为了更好地 GIT 提交,加入了代码提交规范和规范校验,优雅的提交: 方便团队协作和快速定位问题,采取 Husky + commitlint 辅助项目做约定. npm install ...
- Mips单周期CPU设计(logisim实现)
Logisim单周期cpu设计文档与思考题 设计文档 支持指令集 指令 格式 描述(RTL) 机器码 OPCODE/FUNCT add add rd rs rt GPR[rd] <- GPR[r ...
- C240817C. 团队协作:二分答案+贪心
C240817C. 团队协作 二分显然,但是被check难住了. 以为只能把运动员按速度分成两类,然后二分图找最大匹配,但显然做不动. 然后考场上就被卡住了--- 看了题解突然勾起了对一道题远古的记忆 ...
- chrome浏览器自定义安装方法
chrome浏览器有很多比较好的方面,也是很多人首选的浏览器.对于想自定义安装chrome浏览器的小伙伴来说,关于chrome浏览器默认安装到C盘让人很无奈.网上有说直接将C盘的文件夹放到想安装的文件 ...
- 很干,但实用——4G模组供电设计及其选型推荐
4G模组的外部电源供电设计十分重要,对系统稳定.射频性能都有直接影响. 怎么让工程师朋友们在应用开发中少走弯路呢? 我将以Air780E为例,陆续分享系列实用干货.无论你是专家还是菜鸟,无论你是否 ...
- Mysql篇-语句执行计划详解(explain)
概述 使用 explain 输出 SELECT 语句执行的详细信息,包括以下信息: 表的加载顺序 sql 的查询类型 可能用到哪些索引,实际上用到哪些索引 读取的行数 Explain 执行计划包含字段 ...
- php orm的C扩展 ycdatabase
背景:昨天看了ice框架后,感觉运行效率非常高,与其差不多的就是yaf了,然后因为yaf没有orm有点遗憾,不过我就这样去找了找orm,产生了这样的感想 今天又看了一下yaf框架,确实和ice差不多, ...
- Ansible常用功能说明 [异步、并发、委托等]
文章目录 Ansible的同步模式与异步模式 Ansible的异步和轮询 [async.poll] Ansible的并发限制 [serial.max_fail_percentage] Ansible的 ...
- java——棋牌类游戏五子棋(singlewzq1.0)之一
这是本人最近一段时间写的五子棋的java代码,大体框架都实现了,一些细节还需要优化. package basegame; import java.awt.Color; import java.awt. ...
- PHP5.2-5.6不同版本新特性
温故而知新, 时常复习下之前的东西 还是会有一些收获 本文目录:PHP5.2 以前:autoload, PDO 和 MySQLi, 类型约束PHP5.2:JSON 支持PHP5.3:弃用的功能,匿名函 ...