Netty ChannelHandler 性能優(yōu)化
1. 前言
本節(jié)我們主要來繼續(xù)講解 ChannelHandler 的其它特性,主要講解如何去進行 ChannelHandler 業(yè)務(wù)鏈表的常見性能優(yōu)化。
2. 優(yōu)化途徑
通常情況下為了提高自定義業(yè)務(wù) Handler 的性能需要進行一定的優(yōu)化策略,常見的優(yōu)化方案分別是縮短傳播路徑、Handler 單利等。
- 傳播路徑: 如果業(yè)務(wù)很復雜的情況,由很多的 Handler 組成的時候,鏈條過長會消耗性能,因此,一般都是動態(tài)的刪除一些沒用的 Handler。
- Handler 單利: 每個客戶端進來,都會為每個 Channel 創(chuàng)建一輪 Handler 并且加入到 Pipeline 進行管理,new 的過程是消耗性能的。
3. 熱插拔
上節(jié)我們學習了 ChannelHandler 的生命周期,其中有一個關(guān)鍵的方法是 handlerRemoved (),在 handler 被移除的時候觸發(fā)該事件,針對該事件,其實我們可以靈活的擴展自己的業(yè)務(wù)功能。
需求:客戶端和服務(wù)端之間通信,必須需要先認證。
實例:
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
//1.登錄認證Handler
ch.pipeline().addLast(new LoginHandler());
//2.其他業(yè)務(wù)Handler
ch.pipeline().addLast(new OtherHandler());
}
});
通過以上的代碼,我們就能很好的解決了客戶端登錄認證問題,但是我們會發(fā)現(xiàn),在登錄認證成功之后,客戶端發(fā)起其他類型請求的時候,每次請求 LoginHandler 都會被執(zhí)行,那么應(yīng)該怎么去解決這個問題呢?
解決思路:在客戶端第一次連接服務(wù)端時,進行賬號認證,認證成功之后,把 LoginHandler 給移除掉。
實例:
public class LoginHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//1.省略了部分代碼(轉(zhuǎn)換ByteBuf,對象流反序列化)
//2.獲取Map
Map<String,String> map=(Map<String,String>)iss.readObject();
//3.認證賬號、密碼,并且響應(yīng)
String username=map.get("username");
String password=map.get("password");
if(username.equals("admin")&&password.equals("123456")){
//3.1.給客戶端響應(yīng)
ctx.channel().writeAndFlush(Unpooled.copiedBuffer("success".getBytes()));
//3.2.移除該Handler,這樣下次請求就不會再執(zhí)行該Handler了
ctx.pipeline().remove(this);
}else{
ctx.channel().writeAndFlush(Unpooled.copiedBuffer("error".getBytes()));
ctx.channel().closeFuture();
}
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
System.out.println("LoginHandler被移除");
}
}
總結(jié),動態(tài)新增和移除 Handler,也稱之為熱插拔,在真實項目開發(fā)當中其實非常的有用。
4. Handler 單利
4.1 @Shareable
ch.pipeline().addLast(new LoginHandler());
添加鏈表節(jié)點的時候,我們是手工 new 一個對象,其實也就是說,每個客戶端連接進來的時候,都需要組建一條雙向鏈表,并且都是 new 每個節(jié)點的對象,我們都知道每次 new 性能肯定是不高。
Spring 的 IOC 其實就是解決手工 new 對象的,項目啟動的時候把所有對象創(chuàng)建完放到 Spring 容器,后面每次使用的時候無需再創(chuàng)建,而是直接從容器里面獲取,這種方式可以提高性能。同樣道理,Netty 也提供類似的功能,那就是 @Shareable
注解修飾的 Handler,只要用該注解修飾之后,那么該 Handler 就會變成共享,也就是說被所有的客戶端所共享,無需每次都創(chuàng)建,自然性能會得到提升。
實例:
//使用注解修飾
@ChannelHandler.Sharable
public class ServerLoginHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
}
}
public class NettyServer {
public static void main(String[] args) {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
//提前創(chuàng)建好
final ServerLoginHandler serverLoginHandler=new ServerLoginHandler();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
//這里無需再創(chuàng)建,只需要傳遞實例即可
ch.pipeline().addLast(serverLoginHandler);
}
});
serverBootstrap.bind(80);
}
}
4.2 @Shareable 線程不安全
對于共享的 Handler,很容易就會出現(xiàn)線程安全問題,多個線程同時訪問同一個對象不會出現(xiàn)任何的線程安全問題,但是有讀有寫,則就會產(chǎn)生線程安全問題,因此需要特別注意,因此,如果使用了 @Shareable 修飾了 Handler,那么千萬不要包含全局變量、全局靜態(tài)變量,否則就會出現(xiàn)線程安全問題。
實例:
@ChannelHandler.Sharable
public class ServerLoginHandler extends ChannelInboundHandlerAdapter {
//全局變量
private int count;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//遞增
count++;
}
}
疑問:為什么以上的代碼在并發(fā)情況下是不安全的呢?
原因是,每個線程內(nèi)部都會開辟一個內(nèi)存空間,從主內(nèi)存中拷貝 count 值,在線程中遞增之后,再把結(jié)果寫到主內(nèi)存當中。并發(fā)情況下,多個線程之間可能取得的值是一樣,然后線程之間又不可見性,因此就會導致線程不安全。
解決:如果開發(fā)過程中遇到類似的問題,應(yīng)該如何解決呢?
直接使用 AtomicXxx
去代替,AtomicXxx 是 J.U.C 下提供的工具類,底層是通過 CAS 無鎖機制去控制,保證線程安全。
4.3 集成 Spring 容器
其實,在真實開發(fā)項目當中,一般都是把 Handler 直接交給 Spring 容器進行管理,也就是說在 Handler 類上添加 Spring 提供的 @Component 注解即可。
主要目的:
- 統(tǒng)一把 Handler 交給 Spring 來管理;
- Handler 一般都是需要和底層的數(shù)據(jù)庫進行交互的,真實項目當中一般都是使用 Spring 來管理 ORM 組件,如果 Handler 不交給 Spring 管理,那么操作數(shù)據(jù)庫的時候就會相對麻煩。
實例:
//交給Spring容器管理
@Component
public class ServerLoginHandler extends ChannelInboundHandlerAdapter {
//注入dao
@Autowired
private UserDao userDao;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
}
}
@Autowired
private ServerLoginHandler serverLoginHandler;
//這里無需再創(chuàng)建,只需要傳遞實例即可
ch.pipeline().addLast(serverLoginHandler);
5. 小結(jié)
本內(nèi)容主要是從兩個方面去進行業(yè)務(wù) Handler 性能上面的優(yōu)化,分別是
- 熱插拔: 在執(zhí)行過程中動態(tài)的刪除無用的 Handler, 縮短 Handler 的傳播距離;
- 單例: 避免每個客戶端的連接進來時都重復創(chuàng)建 Handler,使用單利的集中方式以及線程安全問題。