基于Netty,從零開發(fā)IM(四):編碼實(shí)踐篇(系統(tǒng)優(yōu)化)

本文由作者“大白菜”分享,有較多修訂和改動(dòng)。注意:本系列是給IM初學(xué)者的文章,IM老油條們還望海涵,勿噴!
1、引言
前兩篇《編碼實(shí)踐篇(單聊功能)》、《編碼實(shí)踐篇(群聊功能)》分別實(shí)現(xiàn)了控制臺(tái)版本的IM單聊和群聊的功能。
通過前兩篇這兩個(gè)小案例來體驗(yàn)的只是Netty在IM系統(tǒng)這種真實(shí)的開發(fā)實(shí)踐,但對(duì)比在真實(shí)的Netty應(yīng)用開發(fā)當(dāng)中,本系列的案例是非常的簡(jiǎn)單的,主要目的其實(shí)是讓大家可以更好地了解其原理,從而寫出更高質(zhì)量的 Netty 代碼。
不過,雖然 Netty 的性能很高,但是也不能保證隨意寫出來的項(xiàng)目就是性能很高的,所以本篇將主要講解幾個(gè)基于Netty的IM系統(tǒng)的優(yōu)化實(shí)戰(zhàn)技術(shù)點(diǎn)。

學(xué)習(xí)交流:
- 移動(dòng)端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動(dòng)端IM》
- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK(備用地址點(diǎn)此)
(本文同步發(fā)布于:http://www.52im.net/thread-3988-1-1.html)
2、寫在前面
建議你在閱讀本文之前,務(wù)必先讀本系列的前三篇《IM系統(tǒng)設(shè)計(jì)篇》、《編碼實(shí)踐篇(單聊功能)》、《編碼實(shí)踐篇(群聊功能)》。
最后,在開始本文之前,請(qǐng)您務(wù)必提前了解Netty的相關(guān)基礎(chǔ)知識(shí),可從本系列首篇《IM系統(tǒng)設(shè)計(jì)篇》中的“知識(shí)準(zhǔn)備”一章開始。
3、系列文章
本文是系列文章的第3篇,以下是系列目錄:
《基于Netty,從零開發(fā)IM(一):IM系統(tǒng)設(shè)計(jì)篇》
《基于Netty,從零開發(fā)IM(二):編碼實(shí)踐篇(單聊功能)》
《基于Netty,從零開發(fā)IM(三):編碼實(shí)踐篇(群聊功能)》
《基于Netty,從零開發(fā)IM(四):編碼實(shí)踐篇(系統(tǒng)優(yōu)化)》(* 本文)
4、基于Netty的IM系統(tǒng)常見優(yōu)化方向
常見優(yōu)化方向腦圖:

我們逐條詳細(xì)解釋一下這些優(yōu)化的目的:
1)心跳檢測(cè):主要是避免連接假死現(xiàn)象;
2)連接斷開:則刪除通道綁定屬性、刪除對(duì)應(yīng)的映射關(guān)系,這些信息都是保存在內(nèi)存當(dāng)中的,如果不刪除則造成資源浪費(fèi);
3)性能問題:用戶 ID 和?Channel?的關(guān)系綁定存在內(nèi)存當(dāng)中,比如:Map,key 是用戶 ID,value 是 Channel,如果用戶量多的情況(客戶端數(shù)量過多),那么服務(wù)端的內(nèi)存將被消耗殆盡;
4)性能問題:每次服務(wù)端往客戶端推送消息,都需從Map里查找到對(duì)應(yīng)的Channel,如果數(shù)量較大和查詢頻繁的情況下如何保證查詢性能;
5)安全問題:HashMap 是線程不安全的,并發(fā)情況下,我們?nèi)绾稳ケWC線程安全;
6)身份校驗(yàn):如何 LoginHandler 是負(fù)責(zé)登錄認(rèn)證的業(yè)務(wù) Handler,AuthHandler 是負(fù)責(zé)每次請(qǐng)求時(shí)校驗(yàn)該請(qǐng)求是否已經(jīng)認(rèn)證了,這些 Handler 在鏈接就緒時(shí)已經(jīng)被添加到 Pipeline 管道當(dāng)中,其實(shí),我們可以采用熱插拔的方式去把一些在做業(yè)務(wù)操作時(shí)用不到的 Handler 給剔除掉。
以上是基于Netty的IM系統(tǒng)開發(fā)當(dāng)中,需要去注意的技術(shù)優(yōu)化點(diǎn),當(dāng)然還有很多其他的細(xì)節(jié),比如:線程池這塊,需要大家慢慢去從實(shí)戰(zhàn)中積累。
5、本篇優(yōu)化方向
本篇主要的優(yōu)化內(nèi)容主要是在第二篇單聊功能和第三篇群聊功能的基礎(chǔ)上繼續(xù)完善幾點(diǎn)。
具體的優(yōu)化方向如下:
1)無論客戶端還是服務(wù)端都分別只有一個(gè) Handler,這樣的話,業(yè)務(wù)越來越多,Handler 里面的代碼就會(huì)越來越臃腫,我們應(yīng)該想辦法把 Handler 拆分成各個(gè)獨(dú)立的 Handler;
2)如果拆分的 Handler 很多,每次有連接進(jìn)來,那么都會(huì)觸發(fā) initChannel () 方法,所有的 Handler 都得被 new 一遍,我們應(yīng)該把這些 Handler 改成單例模式(不需要每次都 new,提高效率);
3)發(fā)送消息時(shí),無論是單聊還是群聊,對(duì)方不在線,則把消息緩存起來,等待其上線再推送給他;
4)連接斷開時(shí),無論是主動(dòng)和被動(dòng),需要?jiǎng)h除 Channel 屬性、刪除用戶和 Channel 映射關(guān)系。
6、業(yè)務(wù)拆分以及單例模式優(yōu)化
6.1 概述
主要優(yōu)化細(xì)節(jié)如下:
1)自定義 Handler 繼承?SimpleChannelInboundHandler,那么解碼的時(shí)候,會(huì)自動(dòng)根據(jù)數(shù)據(jù)格式類型轉(zhuǎn)到相應(yīng)的 Handler 去處理;
2)@Shareable?修飾 Handler,保證 Handler 是可共享的,避免每次都創(chuàng)建一個(gè)實(shí)例。
6.2 登錄Handler優(yōu)化
@ChannelHandler.Sharable
public class ClientLogin2Handler extends SimpleChannelInboundHandler<LoginResBean> {
????//1.構(gòu)造函數(shù)私有化,避免創(chuàng)建實(shí)體
????private ClientLogin2Handler(){}
????//2.定義一個(gè)靜態(tài)全局變量
????public static ClientLogin2Handler instance=null;
????//3.獲取實(shí)體方法
????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 {
?
????????//具體業(yè)務(wù)代碼,參考之前
????}
}
6.3 消息發(fā)送Handler優(yōu)化
@ChannelHandler.Sharable
public class ClientMsgHandler extends SimpleChannelInboundHandler<MsgResBean> {
????//1.構(gòu)造函數(shù)私有化,避免創(chuàng)建實(shí)體
????private ClientMsgHandler(){}
????//2.定義一個(gè)靜態(tài)全局變量
????public static ClientMsgHandler instance=null;
????//3.獲取實(shí)體方法
????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 {
?
????????//具體業(yè)務(wù)代碼,參考之前
????}
}
6.4 initChannel方法優(yōu)化
.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.消息發(fā)送Handler,使用單例獲取
????????ch.pipeline().addLast(ClientMsgHandler.getInstance());
????????//5.編碼器
????????ch.pipeline().addLast(new MyEncoder());
????}
});
6.5 小結(jié)
這種業(yè)務(wù)拆分以及單例模式優(yōu)優(yōu)化是Netty開發(fā)當(dāng)中很常用的,可以更好的維護(hù)基于Netty的代碼并提高應(yīng)用性能。
7、數(shù)據(jù)緩存優(yōu)化
為了提高用戶體驗(yàn),在發(fā)送消息(推送消息)時(shí),如果接收方不在線,則應(yīng)該把消息緩存起來,等對(duì)方上線時(shí),再推送給他。
7.1 數(shù)據(jù)緩存到集合
//1.定義一個(gè)集合存放數(shù)據(jù)(真實(shí)項(xiàng)目可以存放數(shù)據(jù)庫或者redis緩存),這樣數(shù)據(jù)比較安全。
private List<Map<Integer,String>> datas=new ArrayList<Map<Integer,String>>();
?
//2.服務(wù)端推送消息
private void pushMsg(MsgReqBean bean,Channel channel){
????Integer touserid=bean.getTouserid();
????Channel c=map.get(touserid);
?
????if(c==null){//對(duì)方不在線
????????//2.1存放到list集合
????????Map<Integer,String> data=new HashMap<Integer, String>();
????????data.put(touserid,bean.getMsg());
????????datas.add(data);
?
????????//2.2.給消息“發(fā)送人”響應(yīng)
????????MsgResBean res=new MsgResBean();
????????res.setStatus(1);
????????res.setMsg(touserid+">>>不在線");
????????channel.writeAndFlush(res);
?
????}else{//對(duì)方在線
????????//2.3.給消息“發(fā)送人”響應(yīng)
????????MsgResBean res=new MsgResBean();
????????res.setStatus(0);
????????res.setMsg("發(fā)送成功);
????????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.登錄響應(yīng)
????????res.setStatus(0);
????????res.setMsg("登錄成功");
????????res.setUserid(bean.getUserid());
????????channel.writeAndFlush(res);
?
????????//4.根據(jù)user查找是否有尚未推送消息
????????//思路:根據(jù)userid去lists查找.......
?
????}else{
????????res.setStatus(1);
????????res.setMsg("該賬戶目前在線");
????????channel.writeAndFlush(res);
????}
}
8、連接斷開事件處理優(yōu)化
如果客戶端網(wǎng)絡(luò)故障導(dǎo)致連接斷開了(非主動(dòng)下線),那么服務(wù)端就應(yīng)該能監(jiān)聽到連接的斷開,且此時(shí)應(yīng)刪除對(duì)應(yīng)的 map 映射關(guān)系。但是映射關(guān)系如果沒有刪除掉,將導(dǎo)致服務(wù)器資源沒有得到釋放,進(jìn)而影響客戶端的下次同一個(gè)賬號(hào)登錄以及大量的客戶端掉線時(shí)性能。
8.1 正確寫法
實(shí)例:
public class ServerChatGroupHandler extends ChannelInboundHandlerAdapter {
????//映射關(guān)系
????private static Map<Integer, Channel> map=new HashMap<Integer, Channel>();
????//連接斷開,觸發(fā)該事件
????@Override
????public void channelInactive(ChannelHandlerContext ctx) throws Exception {
????????//1.獲取Channel
????????Channel channel=ctx.channel();
?
????????//2.從map里面,根據(jù)Channel找到對(duì)應(yīng)的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.移除標(biāo)識(shí)
????????????ctx.channel().attr(AttributeKey.valueOf("userid")).remove();
????????}
????}
}
8.2 錯(cuò)誤寫法
Channel 斷開,服務(wù)端監(jiān)聽到連接斷開事件,但是此時(shí) Channel 所綁定的屬性已經(jīng)被移除掉了,因此這里無法直接獲取的到 userid。
實(shí)例:
public class ServerChatGroupHandler extends ChannelInboundHandlerAdapter {
????//映射關(guān)系
????private static Map<Integer, Channel> map=new HashMap<Integer, Channel>();
?
????//連接斷開,觸發(fā)該事件
????@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.移除標(biāo)識(shí)
????????????ctx.channel().attr(AttributeKey.valueOf("userid")).remove();
????????}
????}
}
9、本篇小結(jié)
本篇內(nèi)容還是相對(duì)容易理解的,主要是優(yōu)化前面兩篇實(shí)現(xiàn)的IM聊天功能,優(yōu)化內(nèi)容是業(yè)務(wù) Handler 的拆分以及使用單例模式、接受人不在線則緩存數(shù)據(jù)、等其上線再推送、監(jiān)聽連接斷開刪除對(duì)應(yīng)的映射關(guān)系。
限于篇幅,本系列文章文章沒辦法真正講解開發(fā)一個(gè)完整IM系統(tǒng)所涉及的方方面面,如果有興趣,可以繼續(xù)閱讀更有針對(duì)性的IM開發(fā)文章,比如IM架構(gòu)設(shè)計(jì)、IM通信協(xié)議、IM通信安全、群聊優(yōu)化、弱網(wǎng)優(yōu)化、網(wǎng)絡(luò)?;畹?。
10、參考資料
[1]?新手入門:目前為止最透徹的的Netty高性能原理和框架架構(gòu)解析
[2]?理論聯(lián)系實(shí)際:一套典型的IM通信協(xié)議設(shè)計(jì)詳解
[3]?淺談IM系統(tǒng)的架構(gòu)設(shè)計(jì)
[4]?簡(jiǎn)述移動(dòng)端IM開發(fā)的那些坑:架構(gòu)設(shè)計(jì)、通信協(xié)議和客戶端
[5]?一套海量在線用戶的移動(dòng)端IM架構(gòu)設(shè)計(jì)實(shí)踐分享(含詳細(xì)圖文)
[6]?一套原創(chuàng)分布式即時(shí)通訊(IM)系統(tǒng)理論架構(gòu)方案
[7]??一套高可用、易伸縮、高并發(fā)的IM群聊、單聊架構(gòu)方案設(shè)計(jì)實(shí)踐
[8]?一套億級(jí)用戶的IM架構(gòu)技術(shù)干貨(上篇):整體架構(gòu)、服務(wù)拆分等
[9]?從新手到專家:如何設(shè)計(jì)一套億級(jí)消息量的分布式IM系統(tǒng)
[10]?基于實(shí)踐:一套百萬消息量小規(guī)模IM系統(tǒng)技術(shù)要點(diǎn)總結(jié)
[11]?探探的IM長(zhǎng)連接技術(shù)實(shí)踐:技術(shù)選型、架構(gòu)設(shè)計(jì)、性能優(yōu)化
[12]?拿起鍵盤就是干,教你徒手開發(fā)一套分布式IM系統(tǒng)
[13]?萬字長(zhǎng)文,手把手教你用Netty打造IM聊天
[14]?基于Netty實(shí)現(xiàn)一套分布式IM系統(tǒng)
[15]?SpringBoot集成開源IM框架MobileIMSDK,實(shí)現(xiàn)即時(shí)通訊IM聊天功能
(本文同步發(fā)布于:http://www.52im.net/thread-3988-1-1.html)