IM - 優(yōu)化
1. 前言
前面兩個(gè)章節(jié)分別實(shí)現(xiàn)控制臺(tái)版本的單聊和群聊的功能實(shí)現(xiàn),通過(guò)這兩個(gè)小案例來(lái)體驗(yàn) Netty 的真實(shí)開(kāi)發(fā)場(chǎng)景,其實(shí)在真實(shí) Netty 開(kāi)發(fā)當(dāng)中,是非常的簡(jiǎn)單的,但是前面花那么多的篇幅去講解 Netty 的核心組件和理論,主要目的是讓大家可以了解其原理,才能寫(xiě)出高質(zhì)量的 Netty 代碼。雖然 Netty 的性能很高,但是不代表作出來(lái)的項(xiàng)目是性能很高的,本節(jié)主要講解如何去優(yōu)化 Netty。
2. 常見(jiàn)優(yōu)化方案
2.1 優(yōu)化架構(gòu)圖
2.2 優(yōu)化細(xì)節(jié)說(shuō)明
常見(jiàn)的細(xì)節(jié)優(yōu)化
- 心跳檢測(cè),主要是避免連接假死現(xiàn)象;
- 連接斷開(kāi),則刪除通道綁定屬性、刪除對(duì)應(yīng)的映射關(guān)系,這些信息都是保存在內(nèi)存當(dāng)中的,如果不刪除則造成資源浪費(fèi);
- 性能問(wèn)題,用戶 ID 和 Channel 的關(guān)系綁定存在內(nèi)存當(dāng)中,比如:Map<Integer,Channel>,key 是用戶 ID,value 是 Channel,如果用戶量多的情況(客戶端數(shù)量過(guò)多),那么服務(wù)端的內(nèi)存將被消耗殆盡;
- 性能問(wèn)題,每次服務(wù)端往客戶端推送消息,都需要從 Map 里面查找到對(duì)應(yīng)的 Channel,如果數(shù)量比較大和查詢頻繁的情況下如何保證查詢性能;
- 安全性問(wèn)題,HashMap 是線程不安全的,并發(fā)情況下,我們?nèi)绾稳ケWC線程安全;
- 身份校驗(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 給剔除掉。
以上是開(kāi)發(fā)當(dāng)中,需要去注意的點(diǎn),當(dāng)然還有很多其他的細(xì)節(jié),比如:線程池這塊,需要大家慢慢去從實(shí)戰(zhàn)中積累。
本節(jié)主要的內(nèi)容主要是在單聊和群聊基礎(chǔ)上完善幾點(diǎn)內(nèi)容,具體如下:
- 無(wú)論客戶端還是服務(wù)端都分別只有一個(gè) Handler,這樣的話,業(yè)務(wù)越來(lái)越多,Handler 里面的代碼就會(huì)越來(lái)越臃腫,我們應(yīng)該想辦法把 Handler 拆分成各個(gè)獨(dú)立的 Handler;
- 如何拆分的 Handler 很多,每次有連接進(jìn)來(lái),那么都會(huì)觸發(fā) initChannel () 方法,所有的 Handler 都得被 new 一遍,我們應(yīng)該把這些 Handler 改成單例模式。 不需要每次都 new,提高效率;
- 發(fā)送消息,無(wú)論是單聊還是群聊,對(duì)方不在線,則把消息緩存起來(lái),等待其上線再推送給他;
- 連接斷開(kāi),無(wú)論是主動(dòng)和被動(dòng),需要?jiǎng)h除 Channel 屬性、刪除用戶和 Channel 映射關(guān)系。
3. 業(yè)務(wù)拆分以及單例模式
主要優(yōu)化細(xì)節(jié)如下:
- 自定義 Handler 繼承
SimpleChannelInboundHandler
,那么解碼的時(shí)候,會(huì)自動(dòng)根據(jù)數(shù)據(jù)格式類型轉(zhuǎn)到相應(yīng)的 Handler 去處理; - @Shareable 修飾 Handler,保證 Handler 是可共享的,避免每次都創(chuàng)建一個(gè)實(shí)例。
3.1 登錄 Handler
@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ù)代碼,參考之前
}
}
3.2 消息發(fā)送 Handler
@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ù)代碼,參考之前
}
}
3.3 initChannel 方法
.handler(new ChannelInitializer<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());
}
});
總結(jié),這種模式是開(kāi)發(fā)當(dāng)中常用的,可以更好的維護(hù)代碼以及提高應(yīng)用性能。
4. 數(shù)據(jù)緩存
為了提高用戶體驗(yàn),在發(fā)送消息(推送消息)時(shí),如果接收方不在線,則應(yīng)該把消息緩存起來(lái),等對(duì)方上線時(shí),再推送給他。
4.1 數(shù)據(jù)緩存到集合
//1.定義一個(gè)集合存放數(shù)據(jù)(真實(shí)項(xiàng)目可以存放數(shù)據(jù)庫(kù)或者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);
}
}
4.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);
}
}
5. 連接斷開(kāi)事件處理
如果客戶端網(wǎng)絡(luò)故障導(dǎo)致連接斷開(kāi)了(非主動(dòng)下線),那么服務(wù)端就會(huì)監(jiān)聽(tīng)到連接的斷開(kāi),此時(shí)應(yīng)該刪除對(duì)應(yīng)的 map 映射關(guān)系,否則影響客戶端的下次同一個(gè)賬號(hào)登錄,以及大量的客戶端掉線,但是映射關(guān)系沒(méi)有刪除掉,導(dǎo)致服務(wù)器資源沒(méi)有得到釋放。
5.1 正確寫(xiě)法
實(shí)例:
public class ServerChatGroupHandler extends ChannelInboundHandlerAdapter {
//映射關(guān)系
private static Map<Integer, Channel> map=new HashMap<Integer, Channel>();
//連接斷開(kāi),觸發(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();
}
}
}
5.2 錯(cuò)誤寫(xiě)法
Channel 斷開(kāi),服務(wù)端監(jiān)聽(tīng)到連接斷開(kāi)事件,但是此時(shí) Channel 所綁定的屬性已經(jīng)被移除掉了,因此這里無(wú)法直接獲取的到 userid。
實(shí)例:
public class ServerChatGroupHandler extends ChannelInboundHandlerAdapter {
//映射關(guān)系
private static Map<Integer, Channel> map=new HashMap<Integer, Channel>();
//連接斷開(kāi),觸發(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();
}
}
}
6. 小結(jié)
本節(jié)內(nèi)容還是相對(duì)容易理解的,主要是優(yōu)化前面實(shí)現(xiàn)的聊天功能,主要優(yōu)化是業(yè)務(wù) Handler 的拆分以及使用單例模式、接受人不在線則緩存數(shù)據(jù),等其上線再推送、監(jiān)聽(tīng)連接斷開(kāi)刪除對(duì)應(yīng)的映射關(guān)系。