簡介
Netty是由Jboss提供的一個異步的、基于事件驅動的Java網絡應用框架,用來快速開發高性能、高可靠性的網絡IO程序。
本質上是一個NIO框架,所以想要徹底理解Netty,需要先搞明白什么是NIO
應用場景
用于開發RPC框架
分布式系統中,各個節點之間需要遠程服務調用,高性能的RPC框架是必不可少的,Netty作為異步高性能呢過的通信框架,往往作為基礎通信組件整合到RPC框架中使用。例如阿里的Dubbo就是使用Netty作為基礎通信組件。
游戲行業
Netty作為高性能的基礎通信組件,提供了TCP/UDP和HTTP協議棧,方便定制和開發私有協議棧,可以用來開發賬號登陸服務器、地圖服務器之間的高性能通信。
大數據
Hadoop的高性能通信和序列化組件Avro的RPC框架,默認采用的就是Netty進行跨界點通信,它的Netty Service就是基于Netty框架二次封裝實現。
原生NIO存在的問題
NIO的類庫和API繁雜,使用麻煩,需要熟練掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
需要熟悉Java多線程編程,因為NIO編程涉及到Reactor模式,你必須對多線程和網絡編程非常熟悉,才能編寫高質量的NIO程序。
開發工作量和難度都非常大,例如客戶端斷開重練、網絡閃斷、半包讀寫、失敗緩存、網絡擁塞和異常流的處理等問題。
JDK NIO存在bug,例如Epoll Bug,會導致Selector空輪詢,最終導致CPU100%,知道JDK1.7版本該問題仍舊存在,沒有根本解決。
Nettty的優點
Netty對JDK自帶的NIO的API進行封裝,解決了上述問題。
設計優雅,適用于各種傳輸類型的統一API阻塞和非阻塞Socket,基于靈活且可擴展的事件模型,可以清晰地分離關注點,高度可定制的線程模型,單線程或一個或多個線程池。
高性能、高吞吐量、低延遲、減少資源消耗、最小化不必要的內存復制(零拷貝)
安全,完整SSL/TLS和StartTLS支持
社區活躍、發現Bug能及時修復,同時更多新功能會被加入
Netty版本
Netty版本分別是netty3.x、netty4.x、netty5.x
Netty5出現重大bug,已經被官網廢棄,目前推薦使用Netty4.x穩定版本
Netty架構設計
不同的線程模型,對程序性能有很大影響,目前存在的線程模型有:傳統的阻塞I/O服務模型和Reactor模式。Netty線程模型主要基于Reactor多線程模型做了一定的改進。
傳統阻塞I/O模型
采用阻塞IO模式獲取輸入的數據,每個連接都需要獨立的線程完成數據的輸入、業務處理和數據返回。
問題:
并發量很大,會創建大量的線程,占用很大系統資源。
連接創建后,如果當前線程暫時沒有數據可讀,該線程會阻塞在read操作,造成線程資源浪費。
傳統阻塞IO模型示意圖
Reactor模式
Reactor模式基本設計思想就是I/O復用結合線程池
基于 I/O 復用模型:多個連接共用一個阻塞對象,應用程序只需要在一個阻塞對象等待,無需阻塞等待所有連接。當某個連接有新的數據可以處理時,操作系統通知應用程序,線程從阻塞狀態返回,開始進行業務處理。
基于線程池復用線程資源:不必再為每個連接創建線程,將連接完成后的業務處理任務分配給線程進行處理,一個線程可以處理多個連接的業務。
根據Reactor的數量和處理資源線程的數量不同,Reactor模式可以分為3種不同的實現
單Reactor單線程
單Reactor多線程
主從Reactor多線程
- 單Reactor單線程
1.Reactor對象通過Select監控客戶端請求,收到事件后通過Dispatch進行分發
2.如果是連接請求事件,則由Acceptor通過accept處理連接請求,然后創建一個Handler對象處理連接后的后續業務
3.如果是處理請求事件,則Reactor分發給對應的Handler來處理
4.Handler會完成read、業務處理、send等業務流程
單Reactor單線程示意圖
總結:這種模式,服務端用一個線程通過多路復用搞定了所有的IO操作,代碼簡單,清晰明了,但是所有的事情都在一個線程上處理,無法發揮多核CPU的性能,Handler在處理某個連接上的業務時,整個進程就無法處理其他連接事件,很容易導致性能瓶頸,還有就是如果線程意外終止,就會進入死循環,會導致整個系統通信不可用。
- 單Reactor多線程
1.Reactor對象通過select監控客戶端請求事件,收到事件后,通過dispatch進行轉發
2.如果是連接請求事件,則由Acceptor通過accept處理連接請求,然后創建一個Handler對象處理連接后的后續業務
3.如果是處理請求事件,則Reactor分發給對應的Handler來處理
4.handler只負責響應事件,不做具體的業務處理,通過read讀取數據,然后分發給worker線程池的某個線程處理業務
5.worker線程池會分配獨立的線程完成業務處理,并將結果返回給handler
6.handler收到響應后,通過send將結果返回給客戶端
單Reactor多線程原理示意圖
總結:可以充分利用多核CPU的處理能力,但是多線程之間數據共享和訪問比較復雜,Reactor處理所有的事件的監聽和響應,在高并發場景下容易出現性能瓶頸。
- 主從Reactor多線程
1.Reactor主線程對象通過select監聽連接事件,收到事件后,通過Acceptor處理連接事件
2.當Acceptor處理連接事件后,主Reactor將連接分配給子Reactor
3.子Reactor將連接加入到連接隊列進行監聽,并創建handler進行各種事件的處理
4.當有新事件,子Reactor會調用對應的handler進行處理
5.handler通過read讀取數據,分發給后面的worker線程處理
6.worker線程池分配獨立的worker線程進行業務處理,并返回結果
7.handler收到響應結果后,再通過send將結果返回給client
主從Reactor多線程原理圖
注:一個Reactor主線程可以對應多個Reactor子線程(這個在圖上沒有展示)。
總結:
優點,主線程和子線程職責明確,主線程只接收連接,子線程完成后續業務操作,主線程和子線程數據交互簡單,主線程只需要把新連接傳給子線程,子線程無需返回數據。
缺點,編程復雜度有點高。
這種模式在很多項目中廣泛使用,例如Nginx主從Reactor多進程模型,Memcached主從多線程,Netty主從多線程模型。
Netty模型
1.Netty抽象出兩個線程池BossGroup和WorkerGroup,BossGroup負責接收客戶端的連接,WorkerGroup負責網絡的讀寫,它們的類型都是NioEventLoopGroup。
2.NioEventLoopGroup相當于一個事件循環組,這個組中含有多個事件循環,每一個事件循環都是NioEventLoop
3.NioEventLoop表示一個不斷循環的執行處理任務的線程,每個NioEventLoop都有一個selector,用于監聽綁定在其上的socket的網絡通訊
4.NioEventLoopGroup可以有多個線程,即可以有多個NioEventLoop
5.每個Boss NioEventLoop循環執行3步
①輪詢accept事件
②處理accept事件,與client建立連接,生成NioSocketChannel,并將其注冊到某個worker NioEventLoop上的selector
③處理任務隊列中的任務,即runAllTasks
6.每個Worker NioEventLoop執行步驟
①輪詢read、write事件
②處理I/O事件,即read、write事件,在對應的NioSocketChannel上處理
③處理任務隊列的任務,即runAllTasks
7.每個Worker NioEventLoop處理業務時,會使用pipeline,pipeline中包含了Channel,通過pipeline可以獲取到對應的通道,管道中維護了很多的處理器
Netty模型原理圖
理論知識就整理到這,下面來個入門案例。
入門案例
- 依賴
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.37.Final</version>
</dependency>
- 服務端
public class NettyServer {
public static void main(String[] args) throws Exception {
/**
* 創建兩個線程組bossGroup和workerGroup
* bossGroup只負責連接請求, workerGroup負責處理客戶端業務
* 兩個線程組包含的子線程個數: 默認是cpu核數 * 2
*/
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//服務器啟動對象
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128) //設置線程隊列得到的兩個個數
.childOption(ChannelOption.SO_KEEPALIVE, true) //設置保持活動連接狀態
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
//給pipeline設置處理器
socketChannel.pipeline().addLast(new NettyServerHandler());
}
});
//綁定一個端口并且同步, 生成一個ChannelFuture對象
ChannelFuture cf = bootstrap.bind(10000).sync();
cf.channel().closeFuture().sync();
}finally {
//優雅的關閉
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* @param ctx 上下文對象
* @param msg 客戶端發送的數據, 默認為Object
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服務器線程名: "+Thread.currentThread().getName());
System.out.println("ctx: "+ctx);
ByteBuf buf = (ByteBuf)msg;
System.out.println("客戶端消息: "+buf.toString(CharsetUtil.UTF_8));
System.out.println("客戶端地址: "+ctx.channel().remoteAddress());
}
/**
* 數據讀取完畢
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//將數據寫入緩存并刷新
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客戶端!!!", CharsetUtil.UTF_8));
}
/**
* 處理異常, 一般是需要關閉通道
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
- 客戶端
public class NettyClient {
public static void main(String[] args) throws Exception{
//客戶端一個事件循環組就可以了
EventLoopGroup group = new NioEventLoopGroup();
try {
//啟動類
Bootstrap bootstrap = new Bootstrap();
//設置啟動參數
bootstrap.group(group)
.channel(NioSocketChannel.class) //設置線程組
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new NettyClientHandler());//加入自己的處理器
}
});
//啟動客戶端連接服務器端
ChannelFuture cf = bootstrap.connect("127.0.0.1", 10000).sync();
cf.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 通道就緒觸發該方法
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("hello server", CharsetUtil.UTF_8));
}
/**
* 當通道有讀取事件時, 會觸發
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
System.out.println("服務器:" + ctx.channel().remoteAddress() + " "+ buf.toString(CharsetUtil.UTF_8));
}
/**
* 異常處理
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
- 測試
分別啟動服務端和客戶端
# 服務端輸出
服務器線程名: nioEventLoopGroup-3-1
ctx: ChannelHandlerContext(NettyServerHandler#0, [id: 0x2f14e16a, L:/127.0.0.1:10000 - R:/127.0.0.1:55613])
客戶端消息: hello server
客戶端地址: /127.0.0.1:55613
# 客戶端輸出
服務器:/127.0.0.1:10000 hello, 客戶端!!!