認識Netty

簡介

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, 客戶端!!!
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370